首页 > 解决方案 > 使用模板而不是虚拟方法的管道模式

问题描述

我正在尝试创建没有虚拟方法的管道模式,以便类的C对象将调用对象类的方法B,将调用对象类的方法A,......(并通过不同的方法反过来)

如果这行得通,那么它将像管道模式一样运行,StartChain::next调用C::next调用B::next调用A::next调用EndChain::next,并且prevs 从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< ...显然不起作用。

标签: c++templatesadapter

解决方案


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

这个想法是每个管道节点 ( 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包含实际模板的 a。

然后是解决方案的核心:策略类,最初命名为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解包,以生成完整的节点类型。如果此邻居不存在,则contains ,它会在展开并返回时忽略其参数。PipelineIndextliftstd::void_tvoid

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

private:
    Prev *_prev;
    Next *_next;

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

然后我们实现两个将被节点继承的函数,并允许它调用prevnext的邻居。因为它没有增加复杂性,而且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. 但是,您不应该进一步复制它(正确的防止复制留作练习)。

就是这样,伙计们。在 Coliru 上现场观看


推荐阅读