Meta-programs are programs that treat other programs as data. They could be other programs or itself. A meta-function is not a function, but a class
or struct
. Metafunctions are not part of the language and have no formal language support. They exist purely as an idiomatic use of the existing language features. Now, since their use is not enforced by the language, their used has to be dictated by a convention. Over the years, the C++ community has created common set of standard conventions. Actually this work goes back all the way to Boost type_traits
.
Metafunctions are not functions. Technically, they are a class with zero or more template parameters and zero+ return types and values. The convention is that a metafunction should return just one thing, like a regular function. The convention was developed over time, so there are plenty of existing examples that do not follow this convention. More modern metafunctions do follow this convention.
How do we return
from a metafunction
If we have to return a value, basically, we are going to expose a public
field named value
.
template<typename T>
struct TheAnswer{
static constexpr int value = 42;
};
And if we are going to return a type, we are going to expose a public
field named type
.
template<typename T>
struct Echo{
using type = T;
};
Now, here’s kind of the difference between regular functions and metafunctions. A regular function in C++ always works on some form of data and it’s always going to return to you some piece of data as well. Amongst metafunctions, we have value metafunctions that work on values like we are used to and then we have metafunctions that work entirely on types and they yield back some type to you. And so in the both of the examples above, we return something by exposing the public members of a class.
Value metafunctions
A value metafunction is kind of like a simple regular function. Let’s look at a simple regular function - the integer identity function.
int int_identity(int x)
{
return x;
}
assert(42 == int_identity(42));
This function just applies the identity transformation on any integer passed to it, and spits out the same number. A simple metafunction for identity - we can call it the intIdentity
metafunction would look like this:
template<int X>
struct intIdentity{
static constexpr int value = X;
};
static_assert(42 == IntIdentity<42>::value);
We see that it’s not that much different. You return a value by having a static
data-member called value
and it has the metafunction’s return value. IntIdentity<42>::value
is where we are calling the metafunction. Now, this convention needs to be adhered to, because if you give your metafunction some other name such as my_value
, for example, if you write:
template<int X>
struct intIdentity{
static constexpr int my_value = X;
};
static_assert(42 == IntIdentity<42>::value);
it’s not going to work well with other things.
Generic Identity Function
Let’s look at the generic identity function.
template<typename T>
(T x){
T identityreturn x;
}
//Returned type will be 42
assert(42 == identity(42));
// Returned type will be unsigned long long
assert(42ul == identity(42ul))
This is just a function that will be an identity for any type. You give me a value of any type and I will give you that value back. Now we can create a generic identity metafunction as well:
template<typename T, T x>
struct ValueIdentity{
static constexpr T value = x;
};
// The type of value will be int
static_assert(42 == identity<int,42>::value);
// The type of value will unsigned long long
static_assert(42ull == identity<unsigned long long,42ull>::value);
ValueIdentity
is a generic metafunction, so we have to first feed it the type int
and then the value 42
. It’s a little cumbersome, but you get used to it after a while.
In C++17, things get a little bit easier with generic metafunctions, because we have this cool keyword called auto
. I won’t go into all the details of auto
. For now, it basically means that the template will accept and deduce the type of any non-type template parameter.
template<auto X>
struct ValueIdentity{
static constexpr auto value = X;
};
// The type of value will be int
static_assert(42 == identity<int,42>::value);
// The type of value will unsigned long long
static_assert(42ull == identity<unsigned long long,42ull>::value);
Let’s look at another function sum()
. We can do this in a regular function, and we can do this in a metafunction as well.
int sum(int x, int y){
return x + y;
}
template<int X, int Y>
struct intSum{
static constexpr int value = X + Y;
};
static_assert(42 == IntSum<30,12>::value);
So, we can also create a generic version of this:
template<typename X, typename Y>
auto sum(T x, Ty){
return x + y;
}
template<auto X, auto Y>
struct Sum{
static constexpr auto value = X + Y;
};
Type metafunctions
Type metafunctions are the workhorse of doing type transformations. You can manipulate types through type metafunctions. Type *metafunctions are going to return just a type.
Here’s our TypeIdentity
function:
template<typename T>
struct TypeIdentity{
using type = T;
}
Just like we have ValueIdentity
, where given any value, it’s going you the value back; we have TypeIdentity
, where you give it any type, and it’s going to give you the type back.
C++20 actually introduces std::type_identity
, which is pretty much what we see above.
Calling Type Metafunctions
When we call a value metafunction, we can easily call the function:
<42>::value ValueIdentity
ValueIdentity
is the metafunction, it’s passed the parameter 42
in the angle brackets (just like parentheses for a regular value function) and ::value
is how I get it’s value back.
When I call a type metafunction, it’s the same way. The function call consists of the metafunction name std::type_identity
, the parameters to the metafunction in angle brackets (<42>
) and ::type
is how I get it’s value back.
#include <iostream>
#include <type_traits>
int main()
{
using T = std::type_identity<int>::type;
return 0;
}
Understanding name binding and dependent types
Name binding is the process of establishing, determining explicitly the type of each name (declaration) in a template. There are two kinds of names used within a template: dependent and non-dependent names. Names that depend on a template parameter are called dependent names.
- For dependent names, name binding is performed at the point of template instantiation.
- For non-dependent names, name binding is performed at the point of template definition.
Consider the following C++ code:
// Ref: Template metaprogramming with C++
// Mariusz Bancilla, pages 123
#include <iostream>
template<typename T>
struct handler{
void handle(T value) //[1] handle is a dependent name
{
std::cout << "handler<T>: " << value << '\n';
}
};
template<typename T>
struct parser{
void parse(T arg){ //[2] parse is a dependent name
.handle(x);
arg}
double x; //[3] x is a non-dependent name
};
int main(){
<double> doubleHandler; //[5] template instantiation
handler<handler<double>> doubleParser(3.14); //[6] template instantiation
parser.parse(doubleHandler);
doubleParserreturn 0;
}
When the compiler sees dependent names, e.g. at points [1]
and [2]
, it cannot determine the type-signature of these functions. So, parse()
and handle()
are not bound at this point.
The declaration double x
at point [3]
declares a non-dependent type. So, the type of the variable x
is known and bound.
Continuing with the code, at point [4]
, there is a template specialization for the handler
class template for the type int
.
Template instantiation happens at points [5]
and [6]
. At point [5]
, handler<double>::handle
is bound to handle
and at point [6]
, parser<handler<double>>::parse
is bound to parse
.
Two-phase name lookup
Name binding happens differently for dependent names (those that depend on a template parameter) and non-dependent names. When the compiler passes throuh the definition of a template, it needs to figure out whether a name is dependent or non-dependent. Further, name binding depends upon this categorization. Thus, the instantiation of a template happens in 2-phases.
- The first phase occurs at the point of the definition when the template syntax is checked and the names are categorized as dependent or non-dependent.
- The second phase occurs at the point of template instantiation when the template arguments are substituted for the template parameters. Name binding for dependent names happens at this point.
This process in two steps is called the two-phase name lookup.
Consider the following C++ code:
// Ref: Template metaprogramming with C++
// Mariusz Bancilla, pages 125
template<typename T>
struct base_parser
{
void init() // [1] non-dependent name
{
std::cout << "init\n";
}
};
template<typename T>
struct parser::public base_parser<T>
{
void parse(){ // [2] non-dependent name
// The compiler at [3] will try to bind init(), as it's a non-dependent name.
// However, base_parser has not yet been instantiated. This will result in a
// compile-error
//init(); // [3] non-dependent name
std::cout << "parse\n";
}
};
int main(){
<double> p;
parser.parse();
p}
The call to init()
inside the parse()
member function has been commented out. Uncommenting it will cause a compile error.
The intention here is to call the base-class init()
function. However, the compiler will issue an error, because it’s not able to find init()
. The reason is that init()
is a non-dependent name. Therefore, it must be bound at the time of the definition of the parser
template. Although, base_parser<T>::init()
exists, this template has still not been instantiated. The compiler cannot assume its what we want to call, because the primary template base_parser
can always be specialized and init()
can be defined as something else.
This problem can be fixed, by making init
a dependent name. This can be done by either prefixing it with this->
or with base_parser<T>::
.
Dependent type names
There are cases where a dependent name is a type. Consider the following C++ code:
#include <iostream>
template<typename T>
struct base_parser{
using value_type = T;
};
template<typename T>
struct parser: base_parser<T>{
void parse(){
// value_type v{}; // [1] : Error
// base_parser<T>::value_type v{}; //[2] :Error
std::cout << "parse\n";
}
};
int main()
{
<double> p;
parser.parse();
preturn 0;
}
In this code snippet, the metafunction base_parser
is an identity metafunction and returns back the type you give it. base_parser<T>::value_type
is actually a dependent type, which depends on the template parameter T
. At point [1]
and [2]
, the compiler does not know what T
will be. If it attempts to bind the name v
, it will fail. We need to tell the compiler explicitly base_parser<T>::value_type
is a dependent type. You do that using the typename
keyword.
#include <iostream>
template<typename T>
struct base_parser{
using value_type = T;
};
template<typename T>
struct parser: base_parser<T>{
void parse(){
// value_type v{}; // [1] : Error
// base_parser<T>::value_type v{}; //[2] :Error
typename base_parser<T>::value_type v{}; //[3] :Ok
std::cout << "parse\n";
}
};
int main()
{
<double> p;
parser.parse();
preturn 0;
}
So, any time, when calling a type metafunction, if the compiler does not know what ::type
is, you must prefix it using the typename
keyword, if we want to treat it as a type.
Convenience calling functions
Value metafunctions often use helper functions (variable templates) ending with _v
. For example, we often define the helper function:
template <auto X>
inline constexpr auto ValueIdentity_v = ValueIdentity<X>::value;
static_assert(42 == ValueIdentity<42>::value);
static_assert(42 == ValueIdentity_v<42>)
We are just calling the ValueIdentity<>
metafunction, grabbing its value and storing it into this variable ValueIdentity_v
. This is a convenient way of calling value metafunctions. It does require you to instantiate an extra variable template.
It’s really helpful when we start using it with types. Type metafunctions use alias templates ending with _t
. It helps us get rid of the entire typename
dance.
template <typename T>
using TypeIdentity_t = typename TypeIdentity<T>::type;
static_assert(std::is_same_v<int, TypeIdentity_t<int>);
Instead of calling the TypeIdentity
metafunction with the parameter int
and writing ::type
to get its value, I can just call the metafunction with _t
and with its parameters in angle brackets(<>
).
These calling conventions are easier to use. But each one must be explicitly hand-written. So, every time you write a metafunction, if you want to provide convenience capabilities, you also have to write the convenience variable template or alias template.
Useful metafunctions to think of
How is std::remove_pointer
metafunction implemented? You can intuitively come up with what it must look like:
template<typename T>
struct RemovePointer{
using type = T;
};
// template specialization
template<typename T>
struct RemovePointer<T*>{
using type = T;
};
If the std::remove_pointer
metafunction receives int*
as a parameter, the specialized version of the template kicks in, as int*
is matched against T*
. Here is the full implementation of std::remove_pointer<T>
:
template <class T> struct remove_pointer { using type = T };
template <class T> struct remove_pointer<T*> { using type = T };
template <class T> struct remove_pointer<T* const> { using type = T };
template <class T> struct remove_pointer<T* volatile> { using type = T };
template <class T> struct remove_pointer<T* const volatile> { using type = T };
As you can see, the same technique is applied to more subtle edge cases.
How about std::remove_reference
?
template<typename T>
struct RemoveReference{
using type = T;
};
// template specialization
template<typename T>
struct RemoveReference<T&>{
using type = T;
};
// template specialization
template<typename T>
struct RemoveReference<T&&>{
using type = T;
};
But, removing qualifiers from types is only the tip of the iceberg. How are std::enable_if
and std::conditional
implemented? Take a guess!
The type metafunction std::enable_if
returns the type T
, if the predicate B
is true
.
template <bool B, typename T>
struct EnableIf{};
template <typename T>
struct EnableIf<true,T>{
using type = T;
};
The type metafunction std::conditional
returns T
, if the predicate B
is true, otherwise returns F
. It’s a compile-time if-else
operating with types.
template <bool B, typename T, typename F>
struct Conditional{
using type = T;
};
template <typename T, typename F>
struct Conditional<false, T, F>{
using type = F;
};
The type metafunction std::remove_const
removes any top-level const
qualifier. It’s a transformation trait. Let’s look at the usage of this metafunction to ensure we handle all cases correctly.
We could try to implement it from scratch as follows:
//Primary template : do nothing if there's no const
template<typename T>
struct RemoveConst : std::type_identity<T> {};
//Partial specialization
template<typename T> struct RemoveConst<T const>{
using type = T;
}
and likewise for removing volatile
.
template <typename T>
struct RemoveVolatile {
using Type = T;
};
// remove volatile
template <typename T>
struct RemoveVolatile<volatile T> {
using Type = T;
};
RemoveConst
and RemoveVolatile
can be composed into RemoveCV
.
template <typename T>
struct RemoveCVT : RemoveConst<RemoveVolatile<T>> {};
I hope you enjoyed the warm-up. Now, let’s try and implement std::decay
from scratch.
Implementing std::decay
Since C++11, std::decay
was introduced into <type_traits>
. It is used to decay a type, or to convert a type into it’s corresponding by-value type. It will remove any top-level cv
-qualifiers (const
, volatile
) and reference qualifiers for the specified type. For example, int&
is turned into int
. An array type becomes a pointer to its element types. A function type becomes a pointer to the function.
Non-Array and Non-function case
We handle the non-array and non-function cases first.
// RemoveConst and RemoveVolatile can be composed into RemoveCV
template <typename T>
struct DecayT : RemoveCVT<RemoveReference<T>> {};
Array-to-pointer decay
Now, we take array types into account. Below are partial specialisations to convert an array type into a pointer to its element type:
// unbounded array
template <typename T>
struct DecayT<T[]> {
using Type = T*;
};
// bounded array
template <typename T, std::size_t N>
struct DecayT<T[N]> {
using Type = T*;
};
Function-to-pointer decay
We want to recognise a function regardless of its return type and parameter types, and then get its function pointer. Because there are different number of parameters, we need to employ variadic templates:
template <typename Ret, typename...Args>
struct DecayT<Ret(Args...)> {
using Type = Ret(*)(Args...);
};
std::integral_constant
metafunction
The std:integral_constant
wraps a static constant of the specified type. It is a value meta-function. In fact, it is an identity meta-function, so that, std::integral_constant<char,'a'>::value
returns a
. It is also a type meta-function. Here’s a possible implementation:
template<class T, T v>
struct integral_constant
{
static constexpr T value = v;
using value_type = T;
using type = integral_constant; // using injected-class-name
constexpr operator value_type() const noexcept { return value; }
constexpr value_type operator()() const noexcept { return value; } // since c++14
};
Now, std::true_type
is simply the compile-time constant defined as std::integral_constant<bool,true>
. std::false_type
is the compile-time constant defined as std::integral_constant<bool,false>
.
std::is_same
std::is_same<T1,T2>
is a comparison metafunction for types. We have a primary template:
// Primary template
template<typename T1, typename T2>
struct is_same : std::false_type {};
It takes two types as arguments. If the two types are the same, we have the explicit specialisation:
// Template metaprogramming
// Mariusz Bancila
template<typename T>
struct is_same<T,T>{
using value = std::true_type;
}
// Convenience - variable template
template<typename T1, typename T2>
constexpr inline bool is_same_v = is_same<T1,T2>::value;
Now, we can define is_floating_point
as an alias template:
template<class T>
using is_floating_point = std::bool_constant<
// Note: standard floating-point types
std::is_same<float, typename std::remove_cv<T>::type>::value
|| std::is_same<double, typename std::remove_cv<T>::type>::value
|| std::is_same<long double, typename std::remove_cv<T>::type>::value
// Note: extended floating-point types (C++23, if supported)
|| std::is_same<std::float16_t, typename std::remove_cv<T>::type>::value
|| std::is_same<std::float32_t, typename std::remove_cv<T>::type>::value
|| std::is_same<std::float64_t, typename std::remove_cv<T>::type>::value
|| std::is_same<std::float128_t, typename std::remove_cv<T>::type>::value
|| std::is_same<std::bfloat16_t, typename std::remove_cv<T>::type>::value
>;
Examples of using type traits
Consider the below widget
and gadget
classes:
#include <array>
#include <iterator>
#include <iostream>
#include <string>
struct widget{
int id;
std::string name;
std::ostream& write(std::ostream& os) const{
<< id << std::endl;
os return os;
}
};
struct gadget{
int id;
std::string name;
friend std::ostream& operator<<(std::ostream& os, gadget const & g);
};
std::ostream& operator<<(std::ostream& os, gadget const & g){
<< g.id << "," << g.name << "\n";
os return os;
}
int main(){
return 0;
}
The widget
class contains a member function write
. However, for the gadget
class, the stream operator <<
is overloaded for the same purpose. We can write the following code using these classes:
{1, "one"};
widget w.write(std::cout);
w
{2, "two"}
gagdet gstd::cout << g
However, we want to write a function template that enables us to treat them the same way. In other words, instead of using either write
or the <<
operator, we should be able to write the following:
(std::cout, w);
serialize(std::cout, g); serialize
How would such a function template look? How can we know whether a type provides a write
method or has the <<
operator overloaded? In other words, we need to query if a type supports write
. We can write our own type trait.
Let’s write a type metafunction uses_write<T>
which returns true
, if T
is widget
and false
otherwise.
template<typename T>
struct uses_write {
static inline constexpr bool value = false;
};
template<>
struct uses_write<widget>{
static inline constexpr bool value = true;
};
Next, let’s assume for simplicity that types that don’t provide a write()
member function always overload the output stream operator <<
.
I can write a primary template to handle the default case.
template<bool B>
struct serializer{
template<typename T>
static void serialize(std::ostream& os, T const & obj){
<< obj;
os }
};
I can specialize this template to handle the case where T
supports write
.
struct serializer<true>{
template<typename T>
static void serialize(std::ostream& os, T const & obj){
.write(os);
obj}
};
We can now write a free-standing function serialize
function, that calls the type-metafunction use_write<T>
to determine which function to dispatch at compile-time.
template <typename T>
void serialize(std::ostream& os, T const & obj){
<uses_write<T>::value>::serialize(os, obj);
serializer}