使用模板而不是虚拟方法的管道模式

时间:2018-09-30 17:42:37

标签: c++ templates adapter

我正在尝试在不使用虚拟方法的情况下创建管道模式,以便类C的对象将调用对象类B的方法,将调用对象类{{1 }},...(以及通过另一种方法相反)

如果此方法有效,则它将像管道模式一样工作,其中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 () { } } ; 显然无效。

3 个答案:

答案 0 :(得分:3)

这是一次旅程。我什至不得不休息一下,然后回来才能真正理解我刚刚写的内容。

这个想法是,每个管道节点(ABC)都是具有一个类型参数的类模板。此参数包含有关整个管道的信息,并且是节点类也必须从中继承的策略。由于我们不想陷入无限递归中,因此我们将节点类型作为模板来处理,而无需实例化它们直到必要(这是在阶段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。然后将此tliftPipeline和我们已经拥有的相邻Index展开,以产生完整的节点类型。如果此邻居不存在,则tlift包含std::void_t,它将在展开后忽略其参数并返回void

完成这种类型的体操后,我们可以为两个邻居存储两个指针:

private:
    Prev *_prev;
    Next *_next;

请注意:前Context的第一个和最后一个void *与其不存在的邻居都包含未使用的prev。我没有花时间优化它们,但是也可以这样做。

然后,我们实现两个函数,这些函数将由节点继承,并允许其在其邻居上调用nextif 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); } 的构造函数希望引用所有节点的元组,并将从内部选择其邻居:

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

剩下要做的就是将我们需要的怪异初始化包装到maker函数中:

(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

请注意,由于从ReferenceError: options is not defined返回时发生了复制省略,因此管道中的指针仍然有效。但是,您不应该进一步复制它(作为练习保留正确的复制保护措施。)

仅此而已,伙计们。 See it live on Coliru

答案 1 :(得分:1)

使用Quentin回答的完整管道是解决之道。 但是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();

Demo

或者甚至使用lambda:

pipeline p(std::tuple(A{}, B{0}, B{1}, [](){ std::cout << "Lambda"; }));

Demo

答案 2 :(得分:0)

让我们假设我们可以实例化这些模板,例如的Start-> A-> End

在中间,我们将需要A的实例化,具体来说

A<Start<*>, End<*>>

除了我们没有要放入*的类型外,因为这是我们要实例化的类型。我们有一个没有基本案例的递归定义。

您要的内容无法在C ++类型中表达