C++14 type lists, any reason to prefer 'free functions' to 'methods' or vice versa?

1.4k views Asked by At

I see two possible styles for implementing type lists in C++11/14 and I was curious if there's any reason to prefer one over the other. The first technique is outlined here and models itself on Boost's MPL library. In this style you define meta 'free functions' (top level using declarations) that take in type lists and operate on them. Here is how you would implement a meta version of std::transform that works on types instead of values in the first style:

    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;

The second style is to define meta 'methods' (using declarations inside the type list struct). Here is how transform looks in that style:

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

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

        // ... other 'methods'
    };

The advantage I see in the second style is that you still have the Args... parameter pack available, so you don't have to delegate to impl helper functions. Two possible disadvantages are that 1) you have to put all your meta functions inside type_list rather than possibly putting them in separate headers, so you lose some modularity and 2) the 'free' meta functions will also work on tuples and any other variadic template class out of the box. I don't know how common the desire for #2 actually is in practice, I have only found occasions to use type_list and tuple myself, and writing meta code to translate between type_list and tuple is not that difficult.

Is there any good reason to strongly prefer one or the other? Maybe #2 is actually a common case?

1

There are 1 answers

7
Yakk - Adam Nevraumont On

The 2nd one is bad for many reasons.

First, calling it is a mess. Templates inside templates require using template keyword.

Second, it requires that your type list include every operation you want to do on type lists within its body. It is like defining every operation on a string as a method on the string: if you allow for free functions, new operations can be created, and you can even implement overrides.

Finally, consider hiding the ::type:

Start with these primitives:

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...>>{};

transform, or fmap, then looks like:

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

and you can either use type_t<fmap<Z,types<int,double>>>, or fmap_t<Z,types<int,double>> to get the types of the mapped-to type.

Yet another approach is to use constexpr functions that contain various things:

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

now fmap is simply:

// 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 {}; }

and other functions follow:

// 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 {}; }

and work with psuedo-types (tags) instead of with types directly at the top level. If you need to lift back to types with type_t when you want to.

I think this is a boost::hana like approach, but I have only started looking at boost::hana. The advantage here is that we decouple the type bundles from the operations, we gain access to full C++ overloading (instead of template pattern matching, which can be more fragile), and we get to directly deduce the contents of the type bundles without having to do the using and empty-primary-specialization tricks.

Everything that is consumed is a wrapped type of tag<?> or types<?> or z<?>, so nothing is "real".

Test code:

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;
}

Live example.