Исходная информация
Я работаю над шаблоном проектирования, похожим на поток данных. Представленные ниже классы предназначены для представления механизма распределения выходных данных. level1
базовый класс CRTP getOutput<N>
в level1
это функция, которую можно использовать для получения выходных данных из экземпляра производного класса. В зависимости от параметра шаблона N
он вызывает один из определенных пользователем методов getOutputImpl
, Эти методы предназначены для предоставления в производном классе (в стиле CRTP). Каждый из методов getOutputImpl
определяет порт вывода, связанный с определенным пользователем производным классом. Тип ввода метода getOutputImpl
определяется дизайном. Типы вывода методов getOutputImpl
может изменяться. Однако по типу выходной тип должен иметь структуру std::unique_ptr<TOutputType>
где TOutputType может быть любого класса. Более подробную информацию можно найти здесь: предыдущий вопрос.
Вопрос
Чтобы разрешить автоматическое распознавание количества определенных пользователем портов (то есть методов getOutputImpl
) метод getOutputPortsNumber(void)
предоставляется в базе level1
учебный класс. Этот метод основан на идее, что возвращаемый тип всех пользовательских методов getOutputImpl
является std::unique_ptr<TOutputType>
, Таким образом, можно определить дополнительный getOutputImpl
метод в базовом классе, который не имеет этого возвращаемого типа (например, он имеет void
тип возврата: void getOutputImpl(...)
).
Описанная выше методология работает, если void getOutputImpl(...)
определяется в определяемом пользователем производном классе (DataflowOutputClass
в этом примере) вместе с другими определенными пользователем std::unique_ptr<TOutputType> getOutputImpl(...)
методы. Тем не менее, когда дополнительный void getOutputImpl(...)
метод перенесен на базу level1
класс, я получаю ошибку компиляции: no matching function for call to 'DataflowOutputClass<int>::getOutputImpl(PortIdxType<2ul>, const PolyIndex&) const
,
Код
typedef size_t Index;
typedef unsigned long Natural;
typedef std::vector<Index> PolyIndex;
typedef const PolyIndex& crPolyIndex;
template<Index N> struct PortIdxType{};
template<typename TLeafType>
class level1
{
public:
TLeafType* asLeaf(void)
{return static_cast<TLeafType*>(this);}
TLeafType const* asLeaf(void) const
{return static_cast<TLeafType const*>(this);}
template <Index N>
auto getOutput(crPolyIndex c_Idx) const
{return asLeaf() -> getOutputImpl(PortIdxType<N>{}, c_Idx);}
static constexpr Natural getOutputPortsNumber(void)
{return getOutputPortsNumberImpl<0>();}
template<Index N>
static constexpr std::enable_if_t<
std::is_void<
decltype(
std::declval<TLeafType*>() ->
getOutput<N>(PolyIndex({}))
)
>::value,
Index
> getOutputPortsNumberImpl(void)
{return N;}
template<Index N>
static constexpr std::enable_if_t<
!std::is_void<
decltype(
std::declval<TLeafType*>() ->
getOutput<N>(PolyIndex({}))
)
>::value,
Index
> getOutputPortsNumberImpl(void)
{return getOutputPortsNumberImpl<N + 1>();}
template<Index N>
void getOutputImpl(
PortIdxType<N>, crPolyIndex c_Idx
) const
{throw std::runtime_error("Wrong template argument.");}};
template<typename T>
class DataflowOutputClass:
public level1<DataflowOutputClass<T>>
{
public:
// if void getOutputImpl(...) const is moved here from level1,
// then the code compiles and works correctly.
//overload for when N = 0
std::unique_ptr<double> getOutputImpl(
PortIdxType<0>, crPolyIndex c_Idx
) const
{
std::unique_ptr<double> mydouble(new double(10));
return mydouble;
}
//overload for when N = 1
std::unique_ptr<int> getOutputImpl(
PortIdxType<1>, crPolyIndex c_Idx
) const
{
std::unique_ptr<int> myint(new int(3));
return myint;
}
};int main()
{
DataflowOutputClass<int> a;
std::cout << a.getOutputPortsNumber() << std::endl;
}
В исходном коде я определил три проблемы:
std::declval<TLeafType*>() -> getOutput
пытается найти имя в неполном классе.
std::declval<TLeafType*>() -> getOutput<N>
не называет шаблон функции getOutput
,
getOutputImpl
Объявления в производном классе скрывают все функции-члены с тем же именем базового класса.
Выражение std::declval<TLeafType*>() -> getOutput
используется в типе возврата DataflowOutputClass::getOutputPortsNumberImpl
,
Создание шаблона класса приводит к созданию объявлений всех функций-членов. Когда вы получаете с CRTP через level1<DataflowOutputClass<T>>
в DataflowOutputClass
класс, компилятор должен создать экземпляр level1<..>
перед созданием производного класса. Следовательно, во время создания level1<DataflowOutputClass<T>>
, DataflowOutputClass<T>
класс все еще не завершен.
Обходной путь должен отложить определение типа возврата DataflowOutputClass::getOutputPortsNumberImpl
сделав его зависимым от параметра шаблона шаблона функции:
template<Index N, typename T = TLeafType>
static constexpr std::enable_if_t<
std::is_void<
decltype(
std::declval<T*>() ->
getOutput<N>(PolyIndex({}))
)
>::value,
Index
> getOutputPortsNumberImpl(void)
{return N;}
Теперь тип возвращаемого значения зависит от параметра-шаблона. шаблона функции. Этот тип возврата может быть разрешен только тогда, когда функция создается экземпляр. Функция создается неявно через использование getOutputPortsNumber
в main
где производный класс уже завершен.
Обратите внимание, что нет необходимости искать имя getOutput
в области производного класса, вы также можете по умолчанию T = level1
, Мы бы не искали имя в производном классе, если бы использовали:
template<Index N, typename T = TLeafType>
static constexpr std::enable_if_t<
std::is_void<
decltype(
getOutput<N>(PolyIndex({}))
)
>::value,
Index
> getOutputPortsNumberImpl(void)
{return N;}
Однако, чтобы определить тип возвращаемого значения этого getOutputPortsNumberImpl
, создание определения getOutput
необходимо, потому что getOutput
использует вычет типа возврата. Его определение будет страдать от проблемы, аналогичной исходному коду: он пытается найти имя в неполном виде.
Исправление проблемы с вычетом возвращаемого типа путем установления зависимости возвращаемого типа шаблона вызывающей функции от параметра шаблона функции кажется мне плохим решением, но всю технику можно заменить чем-то более простым, см. Ниже.
В # 1 мы уже заменили это на std::declval<T*>() -> getOutput<N>(PolyIndex({}))
, но вопрос тот же. Рассматривать:
bool operator> (bool, PolyIndex);
// class template level1
struct derived
{
int getOutput;
};
С этой настройкой, выражение как declval<T*>() -> getOutput<N>(PolyIndex({}))
может быть проанализирован как:
(
(declval<T*>()->getOutput) < N
)
>
(
PolyIndex({})
)
То есть, (x < N) > PolyIndex{}
,
Чтобы позволить компилятору понять, что getOutput
шаблон, используйте template
ключевое слово:
std::declval<T*>() -> template getOutput<N>(PolyIndex{})
(Дополнительный ()
инициализировать PolyIndex
ненужны.)
В общем, любой член в производном классе скрывает членов с одинаковыми именами базовых классов. Чтобы перегрузить функцию-член в производном классе с помощью функций-членов в базовом классе, вы можете использовать используя декларирование «внедрить» имя базового члена в производный класс:
template<typename T>
class DataflowOutputClass:
public level1<DataflowOutputClass<T>>
{
public:
using level1<DataflowOutputClass<T>>::getOutputImpl;
//overload for when N = 0
std::unique_ptr<double> getOutputImpl(
PortIdxType<0>, crPolyIndex c_Idx
) const;
// ...
};
В настоящее время требуется объявление об использовании, так как метапрограммирование OP должно создавать допустимый тип возврата для выражения getOutput<N>(PolyIndex({}))
, Текущий подход отличает void
из не-void
возвращаемые типы. Вместо этого мы можем просто определить, является ли выражение getOutput<N>(PolyIndex{})
хорошо сформирован. Для этого я буду использовать Уолтера Е. Брауна void_t
техника:
template<typename T>
struct voider { using type = T; };
template<typename T>
using void_if_well_formed = typename voider<T>::type;
Мы будем использовать это следующим образом:
void_if_well_formed< decltype(expression) >
даст тип void
если выражение правильно сформировано. В противном случае, если выражение неправильно сформировано из-за сбоя замещения в непосредственном контексте, весь void_if_well_formed<..>
приведет к ошибке замещения в ближайшем контексте. Эти виды ошибок могут использоваться методом, называемым SFINAE: Ошибка замещения не является ошибкой. Это может быть более подходящим, чтобы быть названным Сбой замещения в непосредственном контексте не является ошибкой.
SFINAE могут быть использованы, например, объявив два функциональных шаблона. Позволять expression<T>()
означает любое выражение, которое зависит от T
,
template<typename T, void_if_well_formed<decltype(expression<T>())>* = nullptr>
std::true_type test(std::nullptr_t);
template<typename T>
std::false_type test(void*);
Если теперь мы называем тест через test<some_type>(nullptr)
первая перегрузка предпочтительна, потому что типы аргументов точно соответствуют типу параметра функции. Однако вторая перегрузка также жизнеспособна. Если первая перегрузка неправильно сформирована из-за SFINAE, она удаляется из набора перегрузок, и вместо нее выбирается вторая перегрузка:
template<typename T>
using test_result = decltype( test<T>(nullptr) );
Используя эти методы, мы можем реализовать level1
следующее:
template<typename TLeafType>
class level1
{
public:
template <Index N, typename T = TLeafType>
using output_t =
decltype(std::declval<T*>() ->
getOutputImpl(PortIdxType<N>{}, std::declval<crPolyIndex>()));
static constexpr Natural getOutputPortsNumber(void)
{return getOutputPortsNumberImpl<0>(nullptr);}
template<Index N>
static constexpr Index getOutputPortsNumberImpl(void*)
{return N;}
template<Index N, typename T = TLeafType,
void_if_well_formed<output_t<N, T>>* = nullptr>
static constexpr Index getOutputPortsNumberImpl(std::nullptr_t)
{return getOutputPortsNumberImpl<N + 1>(nullptr);}
};
С немного больше работы, мы можем даже написать это следующим образом:
template<Index N>
struct HasOutputFor
{
static auto P() -> PortIdxType<N>;
static auto cr() -> crPolyIndex;
template<typename T>
static auto requires_(T&& t) -> decltype(t.getOutputImpl(P(), cr()));
};
template<Index N, typename T = TLeafType, REQUIRE( HasOutputFor<N>(T) )>
static constexpr Index getOutputPortsNumberImpl(std::nullptr_t);