Списки типов С++ 14, есть ли причина предпочесть «свободные функции» «методам» или наоборот?

Я вижу два возможных стиля реализации списков типов в C++ 11/14, и мне было любопытно, есть ли какая-то причина предпочесть один другому. Первый метод описан здесь и основан на библиотеке Boost MPL. В этом стиле вы определяете мета «свободные функции» (верхний уровень с использованием объявлений), которые принимают списки типов и работают с ними. Вот как вы могли бы реализовать мета-версию std::transform, которая работает с типами, а не со значениями в первом стиле:

    template <typename... Args>
    struct type_list;

    namespace impl
    {
        template <template <typename...> class F, class L>
        struct transform_impl;

        template <template <typename...> class F, template <typename...> class L, typename... T>
        struct transform_impl<F, L<T...>>
        {
            using type = L<typename F<T>::type...>;
        };
    }

    template <template <typename...> class F, class L>
    using transform = typename impl::transform_impl<F, L>::type;

Второй стиль заключается в определении мета-методов (с использованием объявлений внутри структуры списка типов). Вот как в этом стиле выглядит преобразование:

    template <typename... Args>
    struct type_list {
        // ... other 'methods'

        template<template<class> class Wrapper>
        using transform =
            type_list<Wrapper<Args>...>;

        // ... other 'methods'
    };

Преимущество, которое я вижу во втором стиле, заключается в том, что у вас все еще есть доступный пакет параметров Args..., поэтому вам не нужно делегировать вспомогательные функции impl. Два возможных недостатка заключаются в том, что 1) вам нужно поместить все свои мета-функции внутри type_list, а не помещать их в отдельные заголовки, поэтому вы теряете некоторую модульность и 2) «бесплатные» мета-функции также будут работать с кортежами и любым другим вариативным шаблоном. класс из коробки. Я не знаю, насколько распространено на практике стремление к # 2, я нашел только случаи, когда сам использовал type_list и tuple, и написать метакод для перевода между type_list и tuple не так сложно.

Есть ли веская причина отдавать предпочтение тому или другому? Может быть, № 2 на самом деле распространенный случай?


person Joseph Garvin    schedule 12.06.2015    source источник
comment
А, хм, не учел уродство вызывающего кода.   -  person Joseph Garvin    schedule 12.06.2015


Ответы (1)


Второй плохой по многим причинам.

Во-первых, звонить - это беспорядок. Шаблоны внутри шаблонов требуют использования ключевого слова template.

Во-вторых, он требует, чтобы ваш список типов включал все операции, которые вы хотите выполнять над списками типов в его теле. Это похоже на определение каждой операции над string как метода над строкой: если вы разрешаете свободные функции, можно создавать новые операции и даже реализовывать переопределения.

Наконец, подумайте о том, чтобы скрыть ::type:

Начните с этих примитивов:

template<class T>struct tag{using type=T;};
template<class Tag>using type_t=typename Tag::type;
template<class...Ts>struct types : tag<types<Ts...>>{};

преобразование, или fmap, выглядит следующим образом:

template<template<class...>class Z, class Types>
struct fmap;
template<template<class...>class Z, class...Ts>
struct fmap<Z, types<Ts...>>:types<Z<Ts...>>{};
template<template<class...>class Z, class Types>
using fmap_t = type_t<fmap<Z,Types>>;

и вы можете использовать type_t<fmap<Z,types<int,double>>> или fmap_t<Z,types<int,double>> для получения типов сопоставленного типа.

Еще один подход заключается в использовании constexpr функций, которые содержат различные вещи:

template<class T>struct tag{using type=T;};
template<class...>struct types{using type=types;};
template<class Tag>using type_t=typename Tag::type;

template<template<class...>class Z>
struct z {template<class...Ts>using apply=Z<Ts...>; constexpr z(){};};
template<class...Ts>
struct one_type {};
template<class T0>
struct one_type<T0> { using type=T0; };
template<class...Ts>
using one_type_t=typename one_type<Ts...>::type;

template<template<class>class Z>
struct z_one_base {
    template<class...Ts>
    using helper = Z<one_type_t<Ts...>>;
    using type = z<helper>;
};
template<template<class>class Z>
using z_one = type_t<z_one_base<Z>>;

теперь fmap просто:

// take a template metafunction and a list of types
// and apply the metafunction to each type, returning the list
template<template<class...>class Z, class...Ts>
constexpr auto fmap( z<Z>, types<Ts...> )
-> types<Z<Ts>...> { return {}; }

и другие функции следуют:

// a template metafunction and a list of types
// and apply the template metafunction to all of the types
template<template<class...>class Z, class...Ts>
constexpr auto apply( z<Z>, types<Ts...> )
-> tag<Z<Ts...>> { return {}; }

// take any number of tags
// and make a type list from them
template<class...Tags>
constexpr auto make_list( Tags... )
-> types<type_t<Tags>...> { return {}; }

// concat of nothing is an empty list
constexpr types<> concat() { return {}; }
// concat of a list alone is a list alone:
template<class...T1s>
constexpr auto concat(types<T1s...>)
->types<T1s...>{ return {}; }
// concat of 2 or more lists is the concat of the first two,
// concatted with the rest
template<class...T1s, class...T2s, class...Types>
constexpr auto concat(types<T1s...>,types<T2s...>,Types...)
->decltype( concat(types<T1s...,T2s...>{},Types{}...) )
{ return {}; }


// take a tagged list or a tagged type, and return a list
template<class T>
constexpr auto fbox( tag<T> )->types<T> { return {}; }
template<class...Ts>
constexpr auto fbox( tag<types<Ts...>> )->types<Ts...> { return {}; }

// create z_ versions of functions above:
#define CAT2(A,B) A##B
#define CAT(A,B) CAT2(A,B)
// lift functions to metafunctions with z_ prefix:
#define Z_F(F) \
  template<class...Ts> \
  using CAT(meta_, F) = decltype( F( Ts{}... ) ); \
  using CAT(CAT(z_, F),_t) = z<CAT(meta_, F)>; \
  static constexpr CAT(CAT(z_, F),_t) CAT(z_, F){}

Z_F(concat);
//Z_F(apply);
//Z_F(fmap);
Z_F(fbox);
static constexpr z_one<tag> z_tag{};


// joins a list of lists or types into a list of types
template<class...Ts>
constexpr auto join1(types<Ts...>)
->type_t<decltype( apply( z_concat, fmap( z_fbox, types<tag<Ts>...>{} ) ) )>
{ return {}; }
template<class Types>
constexpr auto join(Types types)
->type_t<decltype( apply( z_concat, fmap( z_fbox, fmap( z_tag, types ) ) ) )>
{ return {}; }

template<class Z, class...Ts>
constexpr auto fbind(Z z, Ts...ts)
->decltype( join( fmap( z, ts... ) ) )
{ return {}; }

и работать с псевдотипами (tags) вместо типов напрямую на верхнем уровне. Если вам нужно вернуться к типам с type_t, когда вы хотите.

Я думаю, что это подход, похожий на boost::hana, но я только начал рассматривать boost::hana. Преимущество здесь в том, что мы отделяем пакеты типов от операций, получаем доступ к полной перегрузке C++ (вместо сопоставления шаблонов с шаблонами, которое может быть более хрупким), и мы получаем возможность напрямую выводить содержимое пакетов типов без необходимости проделайте трюки с using и пустой первичной специализацией.

Все, что потребляется, является обернутым типом tag<?>, types<?> или z<?>, поэтому ничто не является «настоящим».

Тестовый код:

template<class T> using to_double = double;
template<class T> using to_doubles = types<double>;

int main() {
    types< int, int, int > three_ints;

    auto three_double = fmap( z_one<to_double>{}, three_ints );
    three_double  = types<double, double, double >{};
    auto three_double2 = join( fmap( z_one<to_doubles>{}, three_ints ) );
    three_double = three_double2;
    auto three_double3 = fbind( z_one<to_doubles>{}, three_ints );
    three_double3 = three_double2;
}

Живой пример.

person Yakk - Adam Nevraumont    schedule 12.06.2015
comment
Я использую второй, когда хочу быстро приготовить что-то для SO, и это почти все :) - person T.C.; 12.06.2015
comment
Я полагаю, что в вашем первом блоке кода в последней строке вы хотите types::tag, а не types:tag - person Joseph Garvin; 12.06.2015
comment
@JosephGarvin нет. types наследуется от tag самого себя. Смысл tag в том, чтобы иметь возможность быть не типом, а тегом, говорящим о типе. Это позволяет вам наследовать от tag<whatever>, тогда кто-то может type_t<you> извлечь whatever на другом конце. Кроме того, позже tag<whatever> можно будет передать constexpr функциям и обработать, а фактическое whatever — нет. - person Yakk - Adam Nevraumont; 12.06.2015
comment
Упс, прочитал слишком быстро, предположил, что это синтаксическая ошибка, не подумав о наследовании. - person Joseph Garvin; 12.06.2015
comment
Yakk: Я думаю, у вас здесь опечатка: using fmap_t = type_t<Z,Types>;. Вы наверное имели ввиду using fmap_t = type_t<fmap<Z,Types>>; То есть fmap в вашей версии отсутствует. - person Nawaz; 20.11.2016
comment
Чтобы вернуться от значения type<T> к T, я использовал template<auto &t> using type_of = typename std::remove_reference_t<decltype(t)>::type;. - person Johannes Schaub - litb; 28.10.2017
comment
Чтобы иметь возможность работать с prvalue, type может иметь статический встроенный элемент, например godbolt.org/g/ZhyWb5 . - person Johannes Schaub - litb; 28.10.2017