使用 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(); });
}
假设我有一个可以被其他对象观察到的对象:
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(); });
}