使用C++可变模板,我如何存储一组异构类型的对象并对它们进行迭代

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

本文关键字:对象 类型 异构 一组 迭代 何存储 使用 存储 C++      更新时间:2023-10-16

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

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

请注意,这里的关键是我试图避免虚拟函数调用。我不希望我的Listeners(在模板化代码中)必须为纯虚拟类或类似的类创建子类。

我建议不要这样做。你最初的设计似乎很适合有观察者——它很好地将设计的两个不同部分解耦。引入模板意味着每个都需要了解Object拥有的所有侦听器。所以,要确保你真的想先做这件事。


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

template<typename... Listeners>
struct Object
{
    std::tuple<Listeners...> listeners;
    Object(Listeners const&... ls)
    : listeners(ls...)
    { }
};

激发涉及使用index_sequence技巧(此类仅在C++14中引入,但可以使用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(); });
}

您可以使用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);
    }
};