Образец трубы, использующий шаблоны вместо виртуальных методов - PullRequest
0 голосов
/ 30 сентября 2018

Я пытаюсь создать шаблон канала без виртуальных методов, чтобы объект класса C вызывал метод класса объекта B, вызывал метод класса объекта A, ...(и наоборот через другой метод)

Если бы это сработало, то оно функционировало бы как паттерн трубы, с StartChain::next вызовом C::next вызовом B::next вызовом A::next вызовом EndChain::next, ис prev s, идущими от 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< ... явно не работает.

Ответы [ 3 ]

0 голосов
/ 01 октября 2018

Это было ... путешествие.Мне даже пришлось сделать перерыв и вернуться, чтобы понять, что я только что написал.

Идея состоит в том, что каждый узел конвейера (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 и * 1039.* на своих соседей.Поскольку это не добавляло сложности, и мне все равно был нужен шаблон для 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.Вы не должны, однако, копировать его дальше (правильное предотвращение копирования оставлено как упражнение).

Вот и все, ребята. Посмотри в прямом эфире на Колиру

0 голосов
/ 02 октября 2018

Использование полного конвейера, как ответил Квентин, - это путь.но тогда 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();

Демо

Или даже используйте лямбду:

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

Демо

0 голосов
/ 01 октября 2018

Давайте предположим, что мы можем создать экземпляры этих шаблонов так, как вы хотите, например: Start -> A -> End.

В середине нам потребуется экземпляр A, в частности

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

За исключением того, что у нас нет типа для вставки *, поскольку именно этот тип мы пытаемся создать.У нас есть рекурсивное определение без базового регистра.

То, что вы просите, не выражается в типах C ++

...