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:
- non-type template parameter (like
v
in std::integral_constant) - type template parameter (like
T
in std::add_pointer) - template template parameter (like
MetaFunction
in example above)
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>>
, returnsvalue == 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.