使用 C++ 可变参数模板,如何存储一组异构类型的对象并对其进行迭代?

Using C++ variadic templates, how can I store a group of heterogeneously-typed objects AND iterate over them?

假设我有一个可以被其他对象观察到的对象:

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

现在,我想使用模板做同样的事情。这是我的意思的框架:

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

请注意,这里的关键是我试图避免虚函数调用。我不希望我的听众(在模板代码中)必须 subclass 纯虚拟 class 或类似的东西。

您可以使用std::tuple来存储异构对象。

要迭代它,这里有一些模板魔术。这将为每个对象调用仿函数模板(因此,仿函数将适应不同的类型)。 SFINAE 用于确定何时停止迭代。变量 From 和 To 定义范围,并且在每次迭代中 From 递增。当它们相等时,迭代必须停止,所以这就是为什么在这种情况下必须有一个空函数。

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

您需要编写一个传递给它的仿函数模板,例如:

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

每个对象都会被调用。


在您的代码中,它将是这样的(未测试):

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

我反对这样做。您的初始设计似乎非常适合拥有 Observers - 它很好地分离了设计的两个不同部分。引入模板意味着 每个人 都需要知道您的 Object 持有的所有听众。因此,请先确定您真的想这样做。


就是说,您正在寻找一个异构类型的容器 - std::tuple。存储只是:

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

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

开火涉及使用 index_sequence trick (this class was only introduced in C++14 but can be implemented using C++11). 是解释发生了什么的一个很好的答案。

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

使用 C++14 泛型 lambda,编写泛型更容易 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>{}
        );
}

现在你的 fire() 变成了:

void fire() {
    for_each_tuple(listeners, [](auto& l) { l.fire(); });
}