这是……一段旅程。我什至不得不休息一下,然后回来真正理解我刚刚写的内容。
这个想法是每个管道节点(A、B、C)都是一个具有一个类型参数的类模板。此参数包含有关整个管道的信息,并且是节点类也必须继承的策略。由于我们不想陷入无限递归,我们将节点类型作为模板处理,直到必要时才实例化它们(这是在第 2 阶段查找,所有内容都已正确定义)。走吧:
首先我们定义一组工具,一些简单的元函数:
// Stores a class template to be instantiated later
template <template <class...> class T>
struct tlift {
// Instantiate the template
template <class... Args>
using apply = T<Args...>;
};
// Identity function
template <class T>
struct identity {
using type = T;
};
... 以及一组具有其功能的类模板:
// Pack of class templates
template <template <class> class...>
struct tpack { };
// Get the Nth element
template <class Pack, std::size_t N>
struct tpack_at;
template <template <class> class P0, template <class> class... P, std::size_t N>
struct tpack_at<tpack<P0, P...>, N> : tpack_at<tpack<P...>, N - 1> { };
template <template <class> class P0, template <class> class... P>
struct tpack_at<tpack<P0, P...>, 0> {
using type = tlift<P0>;
};
// Get the size of the pack
template <class Pack>
struct tpack_size;
template <template <class> class... P>
struct tpack_size<tpack<P...>>
: std::integral_constant<std::size_t, sizeof...(P)> { };
请注意,由于模板不能裸露,tpack_at 返回一个包含实际模板的tlift。
然后是解决方案的核心:策略类,最初命名为 Context。首先,我们四处寻找我们的邻居是谁:
// Base class and template parameter for pipeline nodes
template <class Pipeline, std::size_t Index>
struct Context {
// Type of the previous node, or void if none exists
using Prev = typename std::conditional_t<
Index == 0,
identity<tlift<std::void_t>>,
tpack_at<Pipeline, Index - 1>
>::type::template apply<Context<Pipeline, Index - 1>>;
// Type of the next node, or void if none exists
using Next = typename std::conditional_t<
Index == tpack_size<Pipeline>::value - 1,
identity<tlift<std::void_t>>,
tpack_at<Pipeline, Index + 1>
>::type::template apply<Context<Pipeline, Index + 1>>;
每个这些有点复杂的 typedef 都会检查我们是否是管道中的第一个(或最后一个)节点,然后检索包含我们之前(或下一个)节点的 tlift。这个tlift 然后用我们已经拥有的Pipeline 和相邻的Index 展开,以生成完整的节点类型。如果这个邻居不存在,tlift 包含std::void_t,它会在展开时忽略其参数并返回void。
一旦这种类型的体操完成,我们可以为我们的两个邻居存储两个指针:
private:
Prev *_prev;
Next *_next;
注意:第一个和最后一个 Contexts 每个都包含一个未使用的 void * 到它们不存在的邻居。我没有花时间优化它们,但也可以这样做。
然后我们实现两个将被节点继承的函数,并允许它在其邻居上调用prev 和next。因为它没有增加复杂性,而且无论如何我都需要一个 if constexpr 的模板,所以我在混合中添加了参数转发:
// Call the previous node's prev() function with arguments
template <class... Args>
void callPrev(Args &&... args) {
if constexpr(!std::is_void_v<Prev>)
_prev->prev(std::forward<Args>(args)...);
}
// Call the next node's next() function with arguments
template <class... Args>
void callNext(Args &&... args) {
if constexpr(!std::is_void_v<Next>)
_next->next(std::forward<Args>(args)...);
}
最后,Context 的构造函数需要一个对所有节点元组的引用,并将从内部挑选其邻居:
// Construction from the actual tuple of nodes
template <class... T>
Context(std::tuple<T...> &pipeline) {
if constexpr(std::is_void_v<Prev>) _prev = nullptr;
else _prev = &std::get<Index - 1>(pipeline);
if constexpr(std::is_void_v<Next>) _next = nullptr;
else _next = &std::get<Index + 1>(pipeline);
}
剩下要做的就是将我们需要的奇怪初始化包装到一个 maker 函数中:
template <template <class> class... Nodes, std::size_t... Idx>
auto make_pipeline(std::index_sequence<Idx...>) {
using Pack = tpack<Nodes...>;
std::tuple<Nodes<Context<Pack, Idx>>...> pipeline{{((void)Idx, pipeline)}...}; // (1)
return pipeline;
}
template <template <class Context> class... Nodes>
auto make_pipeline() {
return make_pipeline<Nodes...>(std::make_index_sequence<sizeof...(Nodes)>{});
}
注意(1) 处的递归点,其中pipeline 会将其自己的引用传递给各个节点的构造函数,以便它们各自将其转发给它们的Context。 ((void)Idx, pipeline) 的技巧是让表达式依赖于模板参数包,这样我就可以实际打包-展开它。
最后,一个节点可以这样定义:
template <class Context>
struct NodeA : Context {
// Forward the context's constructor, or implement yours
using Context::Context;
void prev() {
// Do something
Context::callPrev();
}
void next() {
// Do something
Context::callNext();
}
};
... 用法如下:
int main() {
auto pipeline = make_pipeline<NodeA, NodeB, NodeC>();
std::get<0>(pipeline).next(); // Calls the whole chain forward
std::get<2>(pipeline).prev(); // Calls the whole chain backwards
}
请注意,管道内的指针仍然有效,这要归功于从make_pipeline 返回时发生的复制省略。但是,您不应该进一步复制它(正确的防止复制留作练习)。
就是这样,伙计们。 See it live on Coliru