Currying для шаблонов в метапрограммировании С++
Это скорее концептуальный вопрос. Я пытаюсь найти самый простой способ преобразования шаблона с двумя аргументами (аргументы типа) в шаблон с одним аргументом. I.e., связывая один из типов.
Это будет мета-программный эквивалент bind
в boost/std. Мой пример включает возможный прецедент, который передает std::is_same
в качестве аргумента шаблона шаблону, который принимает аргумент шаблона шаблона с одним аргументом (std::is_same
является шаблоном с двумя аргументами), то есть до TypeList::FindIf
. TypeList
здесь не полностью реализована, и не FindIf
, но вы получаете идею. Он принимает "унарный предикат" и возвращает тип, для которого этот предикат является истинным, или void
, если не такой тип.
У меня есть 2 рабочих варианта, но первый не является однострочным, а второй использует довольно подробное приспособление BindFirst
, которое не будет работать для аргументов шаблона, отличного от типа. Есть ли простой способ написать такой однострочный шрифт? Я считаю, что процедура, которую я ищу, называется currying
.
#include <iostream>
template<template<typename, typename> class Function, typename FirstArg>
struct BindFirst
{
template<typename SecondArg>
using Result = Function<FirstArg, SecondArg>;
};
//template<typename Type> using IsInt = BindFirst<_EqualTypes, int>::Result<Type>;
template<typename Type> using IsInt = std::is_same<int, Type>;
struct TypeList
{
template<template<typename> class Predicate>
struct FindIf
{
// this needs to be implemented, return void for now
typedef void Result;
};
};
int main()
{
static_assert(IsInt<int>::value, "");
static_assert(!IsInt<float>::value, "");
// variant #1: using the predefined parameterized type alias as predicate
typedef TypeList::FindIf<IsInt>::Result Result1;
// variant #2: one-liner, using BindFirst and std::is_same directly
typedef TypeList::FindIf< BindFirst<std::is_same, int>::Result>::Result Result2;
// variant #3: one-liner, using currying?
//typedef TypeList::FindIf<std::is_same<int, _>>::Result Result2;
return 0;
}
Нажмите здесь для кода в онлайн-компиляторе GodBolt.
Ответы
Ответ 1
Я думаю, что типичный способ сделать это - держать все в мире типов. Не принимайте шаблоны шаблонов - они грязные. Пусть напишите метафункцию с именем ApplyAnInt
, которая возьмет "класс метафонов" и применит к ней int
:
template <typename Func>
struct ApplyAnInt {
using type = typename Func::template apply<int>;
};
Если простой класс metafunction может просто проверять, является ли данный тип int
:
struct IsInt {
template <typename T>
using apply = std::is_same<T, int>;
};
static_assert(ApplyAnInt<IsInt>::type::value, "");
Теперь цель состоит в поддержке:
static_assert(ApplyAnInt<std::is_same<_, int>>::type::value, "");
Мы можем это сделать. Мы будем называть типы, содержащие <лямбда-выражения _
, и писать метафункцию под названием lambda
, которая либо переадресует класс метафунта, который не является лямбда-выражением, либо создаст новый метафунг, если он:
template <typename T, typename = void>
struct lambda {
using type = T;
};
template <typename T>
struct lambda<T, std::enable_if_t<is_lambda_expr<T>::value>>
{
struct type {
template <typename U>
using apply = typename apply_lambda<T, U>::type;
};
};
template <typename T>
using lambda_t = typename lambda<T>::type;
Итак, мы обновляем наш оригинальный metafunction:
template <typename Func>
struct ApplyAnInt
{
using type = typename lambda_t<Func>::template apply<int>;
};
Теперь это оставляет две вещи: нам нужны is_lambda_expr
и apply_lambda
. На самом деле это не так уж плохо. Для первого мы увидим, является ли это экземпляром шаблона класса, в котором один из типов _
:
template <typename T>
struct is_lambda_expr : std::false_type { };
template <template <typename...> class C, typename... Ts>
struct is_lambda_expr<C<Ts...>> : contains_type<_, Ts...> { };
И для apply_lambda
мы просто подставим _
в заданный тип:
template <typename T, typename U>
struct apply_lambda;
template <template <typename...> class C, typename... Ts, typename U>
struct apply_lambda<C<Ts...>, U> {
using type = typename C<std::conditional_t<std::is_same<Ts, _>::value, U, Ts>...>::type;
};
И это все, что вам нужно на самом деле. Я оставлю это расширение, чтобы поддержать arg_<N>
как упражнение для читателя.
Ответ 2
Да, у меня была эта проблема. Потребовалось несколько итераций, чтобы найти достойный способ сделать это. В принципе, для этого нам нужно указать разумное представление о том, чего мы хотим и чего необходимо. Я заимствовал некоторые аспекты от std::bind()
в том, что я хочу указать шаблон, который я хочу связать, и параметры, которые я хочу привязать к нему. Затем внутри этого типа должен быть шаблон, который позволит вам передать набор типов.
Итак, наш интерфейс будет выглядеть так:
template <template <typename...> class OP, typename...Ts>
struct tbind;
Теперь наша реализация будет иметь эти параметры плюс контейнер типов, которые будут применяться в конце:
template <template <typename...> class OP, typename PARAMS, typename...Ts>
struct tbind_impl;
Наш базовый пример даст нам тип шаблона, который я назову ttype
, который вернет шаблон содержащихся типов:
template <template <typename...> class OP, typename...Ss>
struct tbind_impl<OP, std::tuple<Ss...>>
{
template<typename...Us>
using ttype = OP<Ss...>;
};
Затем мы имеем дело с перемещением следующего типа в контейнер и ttype
ссылаемся на ttype
в немного более простой базовый случай:
template <template <typename...> class OP, typename T, typename...Ts, typename...Ss>
struct tbind_impl<OP, std::tuple<Ss...>, T, Ts...>
{
template<typename...Us>
using ttype = typename tbind_impl<
OP
, std::tuple<Ss..., T>
, Ts...
>::template ttype<Us...>;
};
И, наконец, нам понадобится переназначение шаблонов, которые будут переданы в ttype
:
template <template <typename...> class OP, size_t I, typename...Ts, typename...Ss>
struct tbind_impl<OP, std::tuple<Ss...>, std::integral_constant<size_t, I>, Ts...>
{
template<typename...Us>
using ttype = typename tbind_impl<
OP
, typename std::tuple<
Ss...
, typename std::tuple_element<
I
, typename std::tuple<Us...>
>::type
>
, Ts...
>::template ttype<Us...>;
Теперь, поскольку программисты ленивы и не хотят вводить std::integral_constant<size_t, N>
для каждого параметра для переназначения, мы указываем некоторые псевдонимы:
using t0 = std::integral_constant<size_t, 0>;
using t1 = std::integral_constant<size_t, 1>;
using t2 = std::integral_constant<size_t, 2>;
...
О, почти забыл о реализации нашего интерфейса:
template <template <typename...> class OP, typename...Ts>
struct tbind : detail::tbind_impl<OP, std::tuple<>, Ts...>
{};
Обратите внимание, что tbind_impl
был помещен в пространство имен detail
.
И вуаля, tbind
!
К сожалению, есть дефект до С++ 17. Если вы передадите tbind<parms>::ttype
в шаблон, который ожидает шаблон с определенным числом параметров, вы получите сообщение об ошибке, поскольку количество параметров не соответствует (конкретный номер не соответствует ни одному числу). Это усложняет ситуацию, требуя дополнительного уровня косвенности.: (
template <template <typename...> class OP, size_t N>
struct any_to_specific;
template <template <typename...> class OP>
struct any_to_specific<OP, 1>
{
template <typename T0>
using ttype = OP<T0>;
};
template <template <typename...> class OP>
struct any_to_specific<OP, 2>
{
template <typename T0, typename T1>
using ttype = OP<T0, T1>;
};
...
Использование этого для wrap tbind
заставит компилятор распознать шаблон с указанным количеством параметров.
Пример использования:
static_assert(!tbind<std::is_same, float, t0>::ttype<int>::value, "failed");
static_assert( tbind<std::is_same, int , t0>::ttype<int>::value, "failed");
static_assert(!any_to_specific<
tbind<std::is_same, float, t0>::ttype
, 1
>::ttype<int>::value, "failed");
static_assert( any_to_specific<
tbind<std::is_same, int , t0>::ttype
, 1
>::ttype<int>::value, "failed");
Все из них преуспевают.