2

Suppose I have an object that is observable by other objects:

struct Object
{
    struct Listener
    {
        virtual void fire() = 0;
    }

    Object(std::vector<Listener *> &listeners) :
        listeners_(listeners)
    {}

    void fire() 
    {
        for(auto *l : listeners_)
            l->fire(); 
    }

private:
    std::vector<Listener *> listeners_;
};

Now, I would like to do the same thing using templates. Here's a skeleton of what I mean:

template<typename ... Listeners>
struct Object
{
    Object(Listeners&&...listeners)
    {
        // How do I store each of the differently-typed references?
    }

    void fire()
    {
        // How do I iterate over the list of listeners?
    }
};

Note that the key thing here is that I'm trying to avoid virtual function calls. I don't want my Listeners (in the templated code)to have to subclass a pure virtual class or anything like that.

user5406764
  • 1,627
  • 2
  • 16
  • 23

2 Answers2

2

You can use a std::tuple to store heterogeneous objects.

To iterate over it, here's some template magic. This will call the functor template for each object (thus, the functor will adapt to different types). SFINAE is used to determine when to stop the iteration. Variables From and To define the range, and on each iteration From is incremented. When they are equal, the iteration has to stop, so that's why there must be an empty function in that case.

#include <tuple>
#include <type_traits>
#include <cstddef>

template <template <typename> class Functor, typename Tuple, std::size_t From, std::size_t To>
typename std::enable_if<From == To, void>::type for_each(const Tuple &t) {}

template <template <typename> class Functor, typename Tuple, std::size_t From = 0, std::size_t To = std::tuple_size<Tuple>::value>
typename std::enable_if<From < To, void>::type for_each(const Tuple &t) {
    Functor<typename std::tuple_element<From, Tuple>::type> op;
    op(std::get<From>(t));
    for_each<Functor, Tuple, From + 1, To>(t);
}

You need to write a functor template which you pass to it, like:

template <typename T>
struct Functor {
    void operator()(const T &x) {
        // ...
    }
}

That will be called for each object.


In your code it would be something like this (not tested):
template <typename Listener>
struct FireFunctor {
    void operator()(const Listener &x) {
        x.fire();
    }
}

template<typename ... Listeners>
struct Object
{
    std::tuple<Listeners...> store;

    Object(Listeners&&...listeners)
    {
        store = std::make_tuple(listeners...);
    }

    void fire()
    {
        for_each<FireFunctor>(store);
    }
};
Franko Leon Tokalić
  • 1,457
  • 3
  • 22
  • 28
2

I would advise against this. Your initial design seems great for having Observers - it decouples the two different parts of the design very well. Introducing templates means that everybody needs to know all the listeners that your Object holds. So make sure you really want to do this first.


That said, you're looking for a heterogeneous container of types - std::tuple. Storage is just:

template<typename... Listeners>
struct Object
{
    std::tuple<Listeners...> listeners;

    Object(Listeners const&... ls)
    : listeners(ls...)
    { }
};

Firing involves the use of the index_sequence trick (this class was only introduced in C++14 but can be implemented using C++11). Here is a good answer explaining what is going on.

public:
    void fire() {
        fire(std::index_sequence_for<Listeners...>{});
    }

private:
    template <size_t... Is>
    void fire(std::index_sequence<Is...> ) { 
        using swallow = int[];
        (void)swallow{0, 
            (void(std::get<Is>(listeners).fire()), 0)...
            };
    }

With C++14 generic lambdas, it's easier to write a generic for_each_tuple:

template <class Tuple, class F, size_t... Is>
void for_each_tuple(Tuple&& tuple, F&& func, std::index_sequence<Is...> ) {
    using swallow = int[];
    (void)swallow{0,
        (void(std::forward<F>(func)(std::get<Is>(std::forward<Tuple>(tuple)))), 0)...
        };
}

template <class Tuple, class F>
void for_each_tuple(Tuple&& tuple, F&& func) {
    for_each_tuple(std::forward<Tuple>(tuple), std::forward<F>(func),
        std::make_index_sequence<std::tuple_size<std::decay_t<Tuple>>::value>{}
        );
}

And now your fire() becomes:

void fire() {
    for_each_tuple(listeners, [](auto& l) { l.fire(); });
}
Community
  • 1
  • 1
Barry
  • 286,269
  • 29
  • 621
  • 977