Search code examples
c++templatesmetaprogrammingc++14template-templates

Bind metafunction: accept both types and template template parameters (accept anything)


I'm trying to write a Bind metaprogramming template helper metafunction that binds a template parameter to something.

I have a working implementation for simple template metafunctions:

template<typename T0, typename T1>
struct MakePair
{
    using type = std::pair<T0, T1>;
};

template<template<typename...> class TF, typename... Ts>
struct Bind
{
    template<typename... TArgs>
    using type = TF<Ts..., TArgs...>;
};

using PairWithInt = typename Bind<MakePair, int>::type;
static_assert(std::is_same<PairWithInt<float>, MakePair<int, float>>{}, "");

But what if MakePair's template arguments were template templates? Or simple numerical values?

template<template<typename> class T0, template<typename> class T1>
struct MakePair0
{
    using type = /*...*/;
};

template<template<typename...> class TF, template<typename> class... Ts>
struct Bind0 { /*...*/ }

// ...

template<int T0, int T1>
struct MakePair1
{
    using type = /*...*/;
};

template<template<int...> class TF, int... Ts>
struct Bind1 { /*...*/ }

A lot of unnecessary repetition. It gets unmanageable if template arguments are mixed between types, template templates, and integral constants.

Is something like the following piece of code possible?

template<template<ANYTHING...> class TF, ANYTHING... Ts>
struct BindAnything
{
    template<ANYTHING... TArgs>
    using type = TF<Ts..., TArgs...>;
};

ANYTHING would accept types, template templates, template template templates, integral values, etc...


Solution

  • When I'm doing serious metaprogramming, I turn everything into types.

    template<class T>struct tag{using type=T;};
    template<class Tag>using type_t=typename Tag::type;
    
    template<template<class...>class> struct Z {};
    template<class Z, class...Ts>
    struct apply {};
    template<template<class...>class z, class...ts>
    struct apply< Z<z>, ts... >:
      tag< z<ts...> >
    {};
    template<class Z, class...Ts>
    using apply_t = type_t< apply<Z, Ts...> >;
    

    now we pass template<?> foo around as Z<foo>, and it is now a type.

    Similar things can be done for constants, using std::integral_constant<T, t> (and easier to use aliases of same), or template<class T, T* p> struct pointer_constant {};, by turning them into types.

    Once everything is a type, your metaprogramming becomes more uniform. Templates just become a kind of type on which apply_t does things to.

    There is no way in C++ to have a template argument that can be a type, a value or a template. So this is about the best you can get.

    templates not written for the above pattern need to be wrapped up, and their arguments "lifted" to being types. As an example:

    template<class T, class t>
    using number_constant = std::integral_constant< T, t{} >;
    using number_constant_z = Z<number_constant>;
    

    has had its arguments "lifted" from values to types, and then it has been wrapped with a Z to turn itself into a type.

    Bind now reads:

    template<class z, class... Ts>
    struct Bind {
      template<class... More>
      using type_base = apply_t< z, Ts..., More... >;
      using type = Z<type_base>;
    };
    template<class Z, class...Ts>
    using Bind_t = type_t<Bind<Z,Ts...>>; // strip ::type
    using Bind_z = Z<Bind_t>; // quote into a Z<?>
    

    and Bind_z is a type wrapping a template that returns a wrapped template, and takes a type that wraps a template as its first argument.

    To use it:

    template<class...>struct types{using type=types;};
    using types_z=Z<types>;
    
    template<class...Ts>
    using prefix =apply_t< Bind_z, types_z, Ts... >;
    using prefix_z = Z<prefix>;
    

    prefix_z takes a set of types, and generates a factory of types<?...> that will contain the prefix Ts... first.

    apply_t< apply_t< prefix_z, int, double, char >, std::string >
    

    is

    types< int, double, char, std::string >
    

    live example.

    There is another fun approach: do metaprogramming in functions:

    template<template<class...>class z, class...Ts>
    constexpr auto apply_f( Z<z>, tag<Ts>... )
    -> tag<z<Ts...>> { return {}; }
    

    here, types are represented by values of type tag<t>, templates a Z<z> and values as std::integral_constant<?>.

    These two:

    template<class T>
    constexpr tag<T> Tag = {};
    template<template<class...>class z>
    constexpr Z<z> Zag = {};
    

    give you ways to get values that represent types and templates respectively.

    #define TYPEOF(...) type_t<decltype(__VA_ARGS__)>
    

    is a macro that moves from an instance of a tag to type type in the tag, and Tag<?> moves from a type to an instance of a tag.

    TYPEOF( apply_f( apply_f( Zag<prefix>, Tag<int>, Tag<double>, Tag<char> ), Tag<std::string> ) )
    

    is

    apply_t< apply_t< prefix_z, int, double, char >, std::string >
    

    strange, but can be interesting.