Type Traits 101

C++
Author

Quasar

Published

November 25, 2024

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 identity(T x){
    return 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:

ValueIdentity<42>::value

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

Compiler Explorer

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
        arg.handle(x);
    }

    double x;                  //[3] x is a non-dependent name
};

int main(){
    handler<double> doubleHandler;                //[5] template instantiation
    parser<handler<double>> doubleParser(3.14);   //[6] template instantiation
    doubleParser.parse(doubleHandler);
    return 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(){
    parser<double> p;
    p.parse();
}

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()
{
    parser<double> p;
    p.parse();
    return 0;
}

Compiler Explorer

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()
{
    parser<double> p;
    p.parse();
    return 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{
        os << id << std::endl;
        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){
    os << g.id << "," << g.name << "\n";
    return os;
}

int main(){
    return 0;
}

Compiler Explorer

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:

widget w{1, "one"};
w.write(std::cout);

gagdet g{2, "two"}
std::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:

serialize(std::cout, w);
serialize(std::cout, g);

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){
        os << obj;
    }
};

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){
        obj.write(os);
    }
};

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){
    serializer<uses_write<T>::value>::serialize(os, obj);
}

Compiler Explorer