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