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");

Все из них преуспевают.