Template metaprogramming

At first glance template metaprogramming may look very difficult, but in fact it is rather challenging than complicated. Sometimes TMP is abused to write unmaintainable code. On the other hand simple utilities can make your project much more flexible, generic and elegant. So what should we have in our toolbox?

apply

Let's start with something really simple: template metafunction apply that takes another metafunction and applies parameter pack to it.

namespace meta {
  template <template <typename...> class MetaFunction, typename... Args>
  struct apply {
    using type = typename MetaFunction<Args...>::type;
  };

  template <template <typename...> class MetaFunction, typename... Args>
  using apply_t = typename apply<MetaFunction, Args...>::type;
}

What is a template metafunction? It is simply a template that translates one or more parameters into types or values. What can be an argument of such metafunction? There are three possibilities:

Metafunction returns via named member types or values. According to common practice return member type should be named typename T::type or T::value if it is non-type. Since c++14 standard library has helper alias templates (see std::remove_reference_t) with _t suffix that allows to skip typename and ::type. In similar way variable templates can be used to skip ::value. There is no limitation for number of returns, but it is a good practice to have exactly one, still there are useful counterexamples like std::iterator_traits.

void_t

This year at cppcon Walter E. Brown had a great talk titled Modern Template Metaprogramming: A Compendium Part I & Part II. He introduced void_t - simple but powerfull metaprogramming tool (official proposal to the standard is N3911).

namespace meta {
  namespace detail {
    template <typename...>
    struct voider {
      using type = void;
    };
  }

  template <typename... Ts>
  using void_t = apply_t<detail::voider, Ts...>;
}

Usage of void_t does not directly rely on member alias using type = void. The trick is in template argument list. It can contain only well-formed types, otherwise entire void_t is ill-formed. This property can be detected with SFINAE

Type introspection

Type introspection is a possibility to query properties of a given type. In c++11 and c++14 it can be done at compile-time with type_traits or at runtime with RTTI. It is expected that in c++17 there will be more advanced facility called Concepts Lite.

has_member

void_t allows to write type trait for user-defined properties. Starting point is a struct has_member which exploits SFINAE.

namespace meta {
  template <template <typename> class, typename, typename = meta::void_t<>>
  struct has_member : std::false_type {};

  template <template <typename> class Member, typename Tp>
  struct has_member<Member, Tp, meta::void_t<Member<Tp>>> : std::true_type {};
}

has_member takes three parameters. First is template template metafunction that works like a getter. Second is a type to introspect. Third parameter (unnamed parameter with default value) is a technical trick that allows selecting implementation. There are two cases of has_member:

  • basic case, potentially handling any type, returns value == false,
  • refined case, handling only well-formed void_t<Member<Tp>>, returns value == true.

Refined case will be prefered as it is more specialized, so basic case will be instantiated only when void_t<Member<Tp>> is ill-formed.

ValueType

Following example presents how to write a trait that checks whether given type Tp has a member typename Tp::value_type.

template <typename Tp>
using ValueType = typename Tp::value_type;

template <typename Tp>
using has_value_type = meta::has_member<ValueType, Tp>;

Metafunction getter ValueType tries to extract value_type from Tp. If it will succeed void_t<Member<Tp>> is well-formed. Refined case of has_member is valid and its implementation is instantiated.

// test
static_assert(has_value_type<std::vector<int>>::value, "");
static_assert(!has_value_type<std::pair<int, int>>::value, "");
static_assert(!has_value_type<int>::value, "");

Public data members

Usage of has_member is not only limited to querying for member types. Next example shows how to write a trait that checks if a given type has accessible data member.

namespace detail {
  template <typename Tp>
  using InspectDataProperty = decltype(std::declval<Tp>().data);
}

template <typename Tp>
using has_data_property = meta::has_member<detail::InspectDataProperty, Tp>;

All the work is done by InspectDataProperty alias for decltype. Expression inside decltype parenthesis is in unevaluated context. This means that it is never executed. Compiler only simulates expression to obtain its type. To create an instance in unevaluated context simply constructor Tp{} can be called, but this works only when Tp is default constructible. More generic solution is to use std::declval instead.

Finally InspectDataProperty is equivalent to the type of the Tp member called data. When InspectDataProperty is able to obtain data then has_member template argument is well-formed and trait return value is true. Otherwise, when there is no data in a given type, has_member returns false.

// test
static_assert(has_data_property<TypeWithPublicData>::value, "");
static_assert(!has_data_property<std::vector<int>>::value, "");
static_assert(!has_data_property<TypeWithPrivateData>::value, "");
static_assert(!has_data_property<int>::value, "");

Member functions

Not only types or data members but also member functions can be detected by has_member. To check if any generic container supports memory reservation has_reserve trait can be used.

namespace detail {
  template <typename Tp>
  using InspectReserve = decltype(
      std::declval<Tp>().reserve(std::declval<typename Tp::size_type>()));
}

template <typename Tp>
using has_reserve = meta::has_member<detail::InspectReserve, Tp>;

InspectReserve helper simulates in decltype unevaluated context call of reserve member function. Similar to std::vector reserve should take size_type number of elements. Like in previous example, when helper is well-formed then has_reserve trait returns true.

// test
static_assert(has_reserve<std::vector<int>>::value, "");
static_assert(!has_reserve<std::array<int, 10>>::value, "");

Operators

Finally has_member can be used to the check presence of operators, since operators can be implemented as a member functions. This time unevaluated context in helper InspectCopyAssignable simulates assigning Tp const& to Tp&&.

namespace detail {
  template <typename Tp>
  using InspectCopyAssignable =
      decltype(std::declval<Tp>() = std::declval<Tp const&>());
}

template <typename Tp>
using is_copy_assignable = meta::has_member<detail::InspectCopyAssignable, Tp>;

It works for both copyable (returns true) and non-copyable types (return false);

// test
static_assert(is_copy_assignable<std::vector<int>>::value, "");
static_assert(is_copy_assignable<std::pair<int, int>>::value, "");
static_assert(!is_copy_assignable<std::unique_ptr<int>>::value, "");

But it fails for int.

// static_assert(is_copy_assignable<int>::value, ""); //fails

I leave it to the readers as a puzzle how to fix InspectCopyAssignable so that it work also for fundamental types. Here is a small hint... take a look at reference collapsing and this build error message:

error: using xvalue (rvalue reference) as lvalue
using int_t = decltype(std::declval<int>() = std::declval<int const&>());
                                           ^

Try this code.

References