Я хочу хранить функции с похожей подписью в коллекции, чтобы сделать что-то вроде этого:
f(vector<Order>& orders, vector<Function>& functions) {
foreach(process_orders in functions) process_orders(orders);
}
Я думал о указателях функций:
void GiveCoolOrdersToBob(Order);
void GiveStupidOrdersToJohn(Order);
typedef void (*Function)(Order);
vector<Function> functions;
functions.push_back(&GiveStupidOrdersToJohn);
functions.push_back(&GiveCoolOrdersToBob);
Или объекты полиморфной функции:
struct IOrderFunction {
virtual void operator()(Order) = 0;
}
struct GiveCoolOrdersToBob : IOrderFunction {
...
}
struct GiveStupidOrdersToJohn : IOrderFunction {
...
}
vector<IOrderFunction*> functions;
functions.push_back(new GiveStupidOrdersToJohn());
functions.push_back(new GiveCoolOrdersToBob());
Предложенный вами дизайн будет работать, но использование регулярных указателей на функции значительно ограничит количество обратных вызовов, которые вы можете зарегистрировать, и, хотя он более мощный, подход, основанный на наследовании от фиксированного интерфейса, является более многословным и требует больше работы для клиента. определить обратные вызовы.
В этом ответе я сначала покажу несколько примеров того, как использовать std::function
для этого. Примеры в значительной степени говорят сами за себя, показывая, как и зачем использовать std::function
приносит преимущества по сравнению с теми решениями, которые вы наметили.
Тем не менее, наивный подход, основанный на std::function
также будет иметь свои собственные ограничения, которые я собираюсь перечислить. Вот почему я в конечном итоге предлагаю вам взглянуть на Boost.Signals2Это довольно мощная и простая в использовании библиотека. Я буду обращаться к Boost.Signals2 в конце этого ответа. Надеюсь, понимание простого дизайна на основе std::function
Во-первых, вам будет легче понять более сложные аспекты сигналов и слотов.
Давайте введем пару простых классов и подготовим почву для некоторых конкретных примеров. Здесь order
это то, что имеет id
и содержит несколько item
s. каждый item
описывается type
(для простоты здесь это может быть как книга DVD), так и name
:
#include <vector>
#include <memory>
#include <string>
struct item // A very simple data structure for modeling order items
{
enum type { book, dvd };
item(type t, std::string const& s) : itemType(t), name(s) { }
type itemType; // The type of the item
std::string name; // The name of the item
};
struct order // An order has an ID and contains a certain number of items
{
order(int id) : id(id) { }
int get_id() const { return id; }
std::vector<item> const& get_items() const { return items; }
void add_item(item::type t, std::string const& n)
{ items.emplace_back(t, n); }
private:
int id;
std::vector<item> items;
};
Суть решения, которое я собираюсь изложить, — следующий класс order_repository
и его внутреннее использование std::function
проводить обратные вызовы, зарегистрированные клиентами.
Обратные вызовы могут быть зарегистрированы через register_callback()
функции, и (довольно интуитивно) незарегистрированный через unregister_callback()
функция путем предоставления cookie, возвращенного registered_callback()
при регистрации:
Функция, чем имеет place_order()
функция для размещения заказов, и process_order()
функция, которая запускает обработку всех заказов. Это приведет к тому, что все зарегистрированные обработчики будут вызываться последовательно. Каждый обработчик получает ссылку на один и тот же вектор размещенных ордеров:
#include <functional>
using order_ptr = std::shared_ptr<order>; // Just a useful type alias
class order_repository // Collects orders and registers processing callbacks
{
public:
typedef std::function<void(std::vector<order_ptr>&)> order_callback;
template<typename F>
size_t register_callback(F&& f)
{ return callbacks.push_back(std::forward<F>(f)); }
void place_order(order_ptr o)
{ orders.push_back(o); }
void process_all_orders()
{ for (auto const& cb : callbacks) { cb(orders); } }
private:
std::vector<order_callback> callbacks;
std::vector<order_ptr> orders;
};
Сила этого решения заключается в использовании std::function
осознать стирание типа а также позволяют инкапсулировать любой вызываемый объект.
Следующая вспомогательная функция, которую мы будем использовать для генерации и размещения некоторых заказов, завершает настройку (она просто создает четыре заказа и добавляет несколько элементов к каждому заказу):
void generate_and_place_orders(order_repository& r)
{
order_ptr o = std::make_shared<order>(42);
o->add_item(item::book, "TC++PL, 4th Edition");
r.place_order(o);
o = std::make_shared<order>(1729);
o->add_item(item::book, "TC++PL, 4th Edition");
o->add_item(item::book, "C++ Concurrency in Action");
r.place_order(o);
o = std::make_shared<order>(24);
o->add_item(item::dvd, "2001: A Space Odyssey");
r.place_order(o);
o = std::make_shared<order>(9271);
o->add_item(item::dvd, "The Big Lebowski");
o->add_item(item::book, "C++ Concurrency in Action");
o->add_item(item::book, "TC++PL, 4th Edition");
r.place_order(o);
}
Теперь посмотрим, какие обратные вызовы мы можем предоставить. Для начала давайте возьмем обычную функцию обратного вызова, которая печатает все заказы:
void print_all_orders(std::vector<order_ptr>& orders)
{
std::cout << "Printing all the orders:\n=========================\n";
for (auto const& o : orders)
{
std::cout << "\torder #" << o->get_id() << ": " << std::endl;
int cnt = 0;
for (auto const& i : o->get_items())
{
std::cout << "\t\titem #" << ++cnt << ": ("<< ((i.itemType == item::book) ? "book" : "dvd")
<< ", " << "\"" << i.name << "\")\n";
}
}
std::cout << "=========================\n\n";
}
И простая программа, которая использует это:
int main()
{
order_repository r;
generate_and_place_orders(r);
// Register a regular function as a callback...
r.register_callback(print_all_orders);
// Process the order! (Will invoke all the registered callbacks)
r.process_all_orders();
}
Здесь живой пример показывая вывод этой программы.
Вполне разумно, что вы не ограничены только регистрацией обычных функций: любой вызываемый объект может быть зарегистрирован как обратный вызов, в том числе функтор держа некоторую информацию о состоянии. Давайте перепишем вышеупомянутую функцию как функтор, который может либо напечатать тот же подробный список заказов, что и функция print_all_orders()
выше, или более короткое резюме, которое не включает элементы заказа:
struct print_all_orders
{
print_all_orders(bool detailed) : printDetails(detailed) { }
void operator () (std::vector<order_ptr>& orders)
{
std::cout << "Printing all the orders:\n=========================\n";
for (auto const& o : orders)
{
std::cout << "\torder #" << o->get_id();
if (printDetails)
{
std::cout << ": " << std::endl;
int cnt = 0;
for (auto const& i : o->get_items())
{
std::cout << "\t\titem #" << ++cnt << ": ("<< ((i.itemType == item::book) ? "book" : "dvd")
<< ", " << "\"" << i.name << "\")\n";
}
}
else { std::cout << std::endl; }
}
std::cout << "=========================\n\n";
}
private:
bool printDetails;
};
Вот как это можно использовать в небольшой тестовой программе:
int main()
{
using namespace std::placeholders;
order_repository r;
generate_and_place_orders(r);
// Register one particular instance of our functor...
r.register_callback(print_all_orders(false));
// Register another instance of the same functor...
r.register_callback(print_all_orders(true));
r.process_all_orders();
}
А вот соответствующий вывод, показанный в этот живой пример.
Благодаря гибкости, предлагаемой std::function
мы также можем зарегистрировать результат std::bind()
в качестве обратного вызова. Чтобы продемонстрировать это на примере, давайте представим еще один класс person
:
#include <iostream>
struct person
{
person(std::string n) : name(n) { }
void receive_order(order_ptr spOrder)
{ std::cout << name << " received order " << spOrder->get_id() << std::endl; }
private:
std::string name;
};
Учебный класс person
имеет функцию-член receive_order()
, Вызов receive_order()
на определенный person
объект моделирует тот факт, что конкретный order
был доставлен к этому person
,
Мы могли бы использовать приведенное выше определение класса, чтобы зарегистрировать функцию обратного вызова, которая отправляет все заказы одному человеку (который может быть определен во время выполнения!):
void give_all_orders_to(std::vector<order_ptr>& orders, person& p)
{
std::cout << "Dispatching orders:\n=========================\n";
for (auto const& o : orders) { p.receive_order(o); }
orders.clear();
std::cout << "=========================\n\n";
}
На данный момент мы могли бы написать следующую программу, этот регистр два обратные вызовы: та же функция для печати заказов, которую мы использовали ранее, и вышеупомянутая функция для отправки заказов на определенный экземпляр Person
, Вот как мы это делаем:
int main()
{
using namespace std::placeholders;
order_repository r;
generate_and_place_orders(r);
person alice("alice");
r.register_callback(print_all_orders);
// Register the result of binding a function's argument...
r.register_callback(std::bind(give_all_orders_to, _1, std::ref(alice)));
r.process_all_orders();
}
Выход этой программы показан в этот живой пример.
И, конечно, можно использовать лямбды как обратные вызовы. Следующая программа основывается на предыдущих, чтобы продемонстрировать использование лямбда-обратного вызова, который отправляет небольшие заказы одному человеку и крупные заказы другому человеку:
int main()
{
order_repository r;
generate_and_place_orders(r);
person alice("alice");
person bob("bob");
r.register_callback(print_all_orders);
r.register_callback([&] (std::vector<order_ptr>& orders)
{
for (auto const& o : orders)
{
if (o->get_items().size() < 2) { bob.receive_order(o); }
else { alice.receive_order(o); }
}
orders.clear();
});
r.process_all_orders();
}
Снова, этот живой пример показывает соответствующий вывод.
Вышеуказанная конструкция является относительно простой, довольно гибкой и простой в использовании. Однако есть много вещей, которые он не позволяет делать:
Все эти функции, наряду со многими другими, предоставляются полноценными библиотеками, такими как Boost.Signals2, на который вы можете захотеть взглянуть. Будучи знакомым с вышеуказанным дизайном, вам будет легче понять, как он работает.
Например, вот как вы определяете сигнал, регистрируете два простых обратных вызова и вызываете их оба, вызывая оператор вызова сигнала (со страницы связанной документации):
struct Hello
{
void operator()() const
{
std::cout << "Hello";
}
};
struct World
{
void operator()() const
{
std::cout << ", World!" << std::endl;
}
};
int main()
{
boost::signals2::signal<void ()> sig;
sig.connect(Hello());
sig.connect(World());
sig();
}
Как обычно, вот живой пример для вышеуказанной программы.
Вы можете посмотреть в std::function
, ваш вектор будет выглядеть так:
std::vector< std::function< void( Order ) > > functions;
Но знайте, что std::function
имеет небольшие накладные расходы. Для случаев, бросьте new
:
function.push_back(GiveStupidOrdersToJohn());
Boost.Signal решает именно вашу проблему. Вы должны посмотреть на это. Если у вас нет особых требований. В частности, boost.signal и boost.function и / или std :: function
использовать методы стирания типа. Таким образом, вы можете иметь вектор вызываемых вещей с указанной подписью. Не имеет значения, являются ли ваши сущности простыми C-функциями (как у вас в вашем примере) или объектами-функциями или функциями-членами в целом. Вы можете смешать их все.