Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Is there an idiomatic way to create a collection of delegates in C++?

I want to store functions with similar signature in a collection to do something like this:

f(vector<Order>& orders, vector<Function>& functions) {
    foreach(process_orders in functions) process_orders(orders);
}

I thought of function pointers:

void GiveCoolOrdersToBob(Order);
void GiveStupidOrdersToJohn(Order);

typedef void (*Function)(Order);
vector<Function> functions;
functions.push_back(&GiveStupidOrdersToJohn);
functions.push_back(&GiveCoolOrdersToBob);

Or polymorphic function objects:

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());
like image 286
Anton Daneyko Avatar asked Mar 18 '13 17:03

Anton Daneyko


2 Answers

Premise:

The design you propose will work, but using regular function pointers will limit you considerably in the kind of callbacks you can register, and although more powerful, the approach based on inheritance from a fixed interface is more verbose and requires more work for a client to define callbacks.

In this answer I will first show some examples of how to use std::function for this purpose. The examples will pretty much speak for themselves, showing how and why using std::function brings advantages as opposed to the kind of solutions you outlined.

However, a naive approach based on std::function will also have limitations of its own, which I am going to list. This is why I eventually suggest you to have a look at Boost.Signals2: it is a pretty powerful and easy-to-use library. I will address Boost.Signals2 at the end of this answer. Hopefully, understanding a simple design based on std::function first will make it easier for you to grasp the more complex aspects of signals and slots later on.


Solution based on std::function<>

Let's introduce a couple of simple classes and prepare the ground for some concrete examples. Here, an order is something which has an id and contains several items. Each item is described by a type (for simplicity, here it can be either a book a dvd), and a 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;
};

The heart of the solution I am going to outline is the following class order_repository, and its internal usage of std::function to hold callbacks registered by clients.

Callbacks can be registered through the register_callback() function, and (quite intuitively) unregistered through the unregister_callback() function by providing the cookie returned by registered_callback() upon registration:

The function than has a place_order() function for placing orders, and a process_order() function that triggers the processing of all orders. This will cause all of the registered handlers to be invoked sequentially. Each handler receives a reference to the same vector of placed orders:

#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;
};

The strength of this solution comes from the use of std::function to realize type erasure and allow encapsulating any kind of callable object.

The following helper function, which we will use to generate and place some orders, completes the set up (it simply creates four orders and adds a few items to each order):

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);
}

Now let's see what kinds of callback we can provide. For starter, let's have a regular callback function that prints all of the orders:

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";
}

And a simple program that uses it:

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();
}

Here is the live example showing the output of this program.

Quite reasonably, you are not limited to registering regular functions only: any callable object can be registered as a callback, including a functor holding some state information. Let's rewrite the above function as a functor which can either print the same detailed list of orders as function print_all_orders() above, or a shorter summary that does not include order items:

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;
};

Here is how this could be used in a small test program:

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();
}

And here is the corresponding output shown in this live example.

Thanks to the flexibility offered by std::function, we can also register the result of std::bind() as a callback. To demonstrate this with an example, let's introduce a further class 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;
};

Class person has a member function receive_order(). Invoking receive_order() on a certain person object models the fact that a particular order has been delivered to that person.

We could use the class definition above to register a callback function that dispatches all the orders to one person (which can be determined at run-time!):

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";
}

At this point we could write the following program, that register two callbacks: the same function for printing orders we have used before, and the above function for dispatching orders to a certain instance of Person. Here is how we do it:

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();
}

The output of this program is shown in this live example.

And of course one could use lambdas as callbacks. The following program builds on the previous ones to demonstrate the usage of a lambda callback that dispatches small orders to one person, and large orders to another person:

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();
}

Once again, this live example shows the corresponding output.


Beyond std::function<> (Boost.Signals2)

The above design is relatively simple, quite flexible, and easy to use. However, there are many things it does not allow to do:

  • it does not allow to easily freeze and resume the dispatching of events to a particular callback;
  • it does not encapsulate sets of related callbacks into an event class;
  • it does not allow grouping callbacks and ordering them;
  • it does not allow callbacks to return values;
  • it does not allow combining those return values.

All these feature, together with many others, are provided by full-fledged libraries such as Boost.Signals2, which you may want to have a look at. Being familiar with the above design, it will be easier for you to understand how it works.

For instance, this is how you define a signal and register two simple callbacks, and call them both by invoking the signal's call operator (from the linked documentation page):

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();
}

As usual, here is a live example for the above program.

like image 99
Andy Prowl Avatar answered Oct 15 '22 10:10

Andy Prowl


You might want to look into std::function, your vector would then look like this:

std::vector< std::function< void( Order ) > > functions;

But be aware that std::function has a small overhead. For the instances, drop the new:

function.push_back(GiveStupidOrdersToJohn());
like image 1
Daniel Frey Avatar answered Oct 15 '22 11:10

Daniel Frey