使用模板而不是虚拟方法的管道模式
A pipe pattern using templates instead of virtual methods
我正在尝试在没有虚拟方法的情况下创建管道模式,以便类C
的对象将调用对象类B
的方法,将调用对象类的方法A
, ...(反过来通过不同的方法(
如果这有效,那么它将像管道模式一样工作,StartChain::next
调用C::next
调用B::next
调用A::next
调用EndChain::next
,并且prev
从EndChain::prev
>StartChain::prev
到不同的结构。
但是 - 我无法找出正确的语法来允许这种情况发生。
template<typename P>
struct EndChain
{
P *p;
void next ()
{
}
void prev ()
{
p->prev();
}
} ;
template<typename N, typename P>
struct A
{
N *n;
P *p;
void next ()
{
n->next();
}
void prev ()
{
p->prev();
}
} ;
template<typename N, typename P>
struct B
{
N *n;
P *p;
void next ()
{
n->next();
}
void prev ()
{
p->prev();
}
} ;
template<typename N, typename P>
struct C
{
N *n;
P *p;
void next ()
{
n->next();
}
void prev ()
{
p->prev();
}
} ;
template<typename N>
struct StartChain
{
N *n;
void next ()
{
n->next();
}
void prev ()
{
}
} ;
因为using Chain = StartChain<C<B<A<EndChain<B<A< ...
显然不起作用。
这是...一段旅程。我甚至不得不休息一下,然后回来真正理解我刚刚写的内容。
这个想法是每个管道节点(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>>;
这些有点复杂的 typedefs 中的每一个都会检查我们是否是管道中的第一个(或最后一个(节点,然后检索包含我们上一个(或下一个(节点的tlift
。然后将此tlift
与已有的Pipeline
和相邻Index
一起解开包装,以生成完整的节点类型。如果这个邻居不存在,则tlift
包含std::void_t
,它将在解包时忽略其参数并返回void
。
一旦这种类型的体操完成,我们可以为我们的两个邻居存储两个指针:
private:
Prev *_prev;
Next *_next;
注意:第一个和最后一个Context
分别包含其不存在的邻居的未使用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
返回时发生的复制省略。但是,您不应该进一步复制它(正确的复制预防作为练习(。
仅此而已,伙计们。在科里鲁现场观看
按照昆汀的回答,使用完整的管道是要走的路。 但是prev
/next
对您的使用来说似乎是多余的,然后可以简化代码。
template <typename ... Nodes>
class pipeline
{
public:
explicit pipeline(const std::tuple<Nodes...>& nodes) : nodes(nodes) {}
template <typename ... Ts>
void traverse(Ts&&... args) {
std::apply([&](auto&&... flatNodes){ (flatNodes(args...), ...); }, nodes);
}
template <typename ... Ts>
void rev_traverse(Ts&&... args) {
rev_traverse_impl(std::index_sequence_for<Nodes...>(), std::forward<Ts>(args)...);
}
private:
template <typename ... Ts, std::size_t ... Is>
void rev_traverse_impl(std::index_sequence<Is...>, Ts&&...args)
{
constexpr auto size = sizeof...(Nodes);
(std::get<size - 1 - Is>(nodes)(args...), ...);
}
private:
std::tuple<Nodes...> nodes;
};
节点类似于:
class A
{
public:
A(/*...*/);
void operator()() const { /*..*/ }
};
和用法:
pipeline<A, B, B, C> p({A{}, B{0}, B{1}, C{}});
p.traverse();
p.rev_traverse();
演示
甚至使用 lambda:
pipeline p(std::tuple(A{}, B{0}, B{1}, [](){ std::cout << "Lambda"; }));
演示
假设我们可以按照您想要的方式实例化这些模板,例如Start
->A
->End
。
在中间,我们将需要一个实例化A
,特别是
A<Start<*>, End<*>>
除了我们没有要放入*
的类型,因为这是我们尝试实例化的类型。我们有一个没有基本情况的递归定义。
您要求的内容无法在C++类型中表达
- 用常见虚拟函数实现的任意组合来实现派生类的正确方法是什么
- 在模板基类中为继承类中的可选重写生成虚拟方法
- 跨 DLL 边界访问虚拟方法是否安全/可能?
- 是否可以使用基类非虚拟方法中的派生类虚拟方法?
- 如何编写 operator= 用于使用虚拟方法与非平凡成员的匿名联合
- 让编译器告诉什么确切的纯虚拟方法使结构抽象?
- 使用模板而不是虚拟方法的管道模式
- 派生类调用父类的方法,该方法调用重写的虚拟方法调用错误的方法
- 从纯虚拟类 (A) 派生的指针无法访问来自纯类 (B) 的重载方法
- 为什么调用没有正文的纯虚拟方法不会导致链接器错误?
- 出于什么目的,非虚拟方法将与C++一起使用?
- 为什么使用存储在虚拟方法表中的地址调用虚拟函数的函数会返回垃圾?
- 如何重写继承的嵌套类中存在的虚拟方法
- 私有虚拟方法有什么用?
- 派生类中纯虚拟基方法的专业化
- 基类可以声明虚拟方法但不定义它吗?仍然在派生类中定义
- googletest:测试基类具有纯虚拟方法的派生类时的核心转储
- 确保模拟的 GTest 方法覆盖虚拟方法
- 使用回调函数从构造函数调用虚拟/派生方法的替代方法?
- 如何调用孩子的方法:虚拟关键字不起作用