Traits templates 101

If you are not familiar with them, traits in C++ are a simple method of using templates to associate custom information with specific data types (and/or with specific data values) at compile time. If you think for a moment about the built-in integral types: int, short, long, and so on, you already know that these types each have a maximum value, a minimum value, the fact of whether they are signed, and a whole lot of other information. These bits of information are traits.

Traits templates are a big part of template meta-programming, but don’t let that concern you. The basic idea is no more complex than already stated.

Traits can be pretty much anything: constant values, other types, functions, and so on. They are sometimes implemented as recursive “functions” which execute at compile time. The ability to associate all kinds of information with types at compile time allows us to make decisions at compile time and control meta-programs. Such meta-programs can be arbitrarily complex and present something of a challenge, to say the least, to many developers. But traits can also be used for much more mundane tasks.

Note: I must admit to being slightly confused by some of the jargon around this. We have traits, traits templates, type traits, and probably other names which may or may not all mean the same thing. I’m not sure it matters.

For our purpose here, I’ll stick to constants, and develop a simple type trait.

A magic number type trait

Suppose that, for some reason, you would like to associate a single integer value called magic with certain data types. For example, int gets the value 123, float gets 456, and maybe some others. The values are meaningless, but that’s magic numbers for you. 🙂

Step 1: Primary template

First, we define a primary template to deal with the general case. We’ll make the integer value 0 unless otherwise specified:

template <typename T> struct MagicNumber 
{ static constexpr int magic = 0 };

In this example a template called MagicNumber is created. It is parametrised by some type which we have called T. This is just a stand in for the name of a concrete type. Remember that a template is not really code: it is rather a set of instructions to the compiler to tell it how to generate code later when we want it to. Think of T as a compile time variable. A template is in some ways just a fancy macro.

We tell the compiler to generate code by instantiating the template. We do this by providing the template with a value (that is, a type) for T, such as int, double, bool, MyClass, std::array<int,31> or whatever. In this case, the generated code will be a struct which defines an integer constant called magic with value 0. If T is int, the compiler will generate a struct called MagicNumber<int>. Don’t be fazed by the angle brackets: it’s just a name. If it helps, imagine the new struct being called MagicNumber_int instead.

// Generated data type
struct MagicNumber<int> 
{ static constexpr int magic = 0 };

// Or, equivalently, you might 
// have written this manually
struct MagicNumber_int 
{ static constexpr int magic = 0 };

Name mangling will most likely in reality give it some other name in your debugger: the point is that some code – a custom data type – is generated.

Step 2: Template specialisations

Second, we define one or more template specialisations for particular parameter types – that is, for particular values of T. For special cases, so to speak. This is how we associate different magic numbers with different types:

template <> struct MagicNumber<int> 
{ static constexpr int magic = 123; };

template <> struct MagicNumber<float> 
{ static constexpr int magic = 456; };

A specialisation is just different version of the template which is used instead of the primary for a specific value for T. The compiler will use the specialisation in place of the primary template when generating code if the given value of T matches. In this example, the code creates a specialisation for T = int. In this case MagicNumber is exactly the same struct as before, but the constant magic now equals 123. There is a second specialisation for T = float, with a constant value of 456.

Note that specialisation is not the same as instantiation. We still haven’t generated any code. We’ve just made the instructions to the compiler more detailed, more specific.

And that’s it.

Step 3: Using our type trait

So now let’s use this template. We might create an array whose size, for some reason, is the associated magic number:

int main()
{
    std::array<int, MagicNumber<int>::magic> int_array;
    for (int& value : int_array)
    {
        // Do something... 
    }
}

Or we might just print out the magic numbers for a bunch of types:

int main()
{
    // Prints 123
    std::cout << MagicNumber<int>::magic << '\n'; 
    // Prints 456
    std::cout << MagicNumber<float>::magic << '\n'; 
    // Prints 0
    std::cout << MagicNumber<bool>::magic << '\n'; 
    // Prints 0
    std::cout << MagicNumber<double>::magic << '\n'; 
}

Or a whole bunch of other things.

MagicNumber is a very simple example of a type trait. We have told the compiler to associate the number 123 with type int, 456 with type float, and 0 with all other types. That’s neat. If it didn’t exist already, you could implement sizeof using this technique. In fact, the standard library uses type traits a bit like this to return the maximum values (and many other things) of the various built-in types. https://en.cppreference.com/w/cpp/types/numeric_limits.

Creating compilation errors

We can do a little more with this example. We can change the primary template so that it doesn’t define a constant at all.

It is important to understand that template specialisations don’t need to define the same set of traits as the primary template (or each other). Specialisation is not like inheritance, but rather creates a wholly different version of the template – one which is used in special cases. 🙂

One way that we can use this fact is by forcing a compilation error whenever the primary template is instantiated:

// Primary doesn't contain any magic.
template <typename T> struct MagicNumber {  };
int main()
{
    // Prints 123
    std::cout << MagicNumber<int>::magic << '\n'; 
    // Prints 456
    std::cout << MagicNumber<float>::magic << '\n'; 
    // ERROR! Does not compile
    std::cout << MagicNumber<bool>::magic << '\n'; 
    // ERROR! Does not compile
    std::cout << MagicNumber<double>::magic << '\n'; 
}

The code now only compiles for the specialisations. For all other types, the generated code is an empty struct which does not even have a member called magic. That is to say, other types do not have a trait called magic. If we attempt to use MagicNumber for any types other than int or float, then the program will simply not compile. That’s power.

This simple technique can be used to convert potential run time errors into compile time errors. Fixing compile time errors is a lot simpler and quicker than finding and fixing run time errors. Imagine, for example, that you could force a compilation error if you accidentally configured a microprocessor peripheral with an invalid pin. That sounds useful to me. I explore this a little in another article: Traits for wakeup pins.

Non-type template parameters

You can also define templates whose parameters are particular values of types rather than types. These are non-type parameters. Here’s a silly example which gives you the twin primes of a few numbers. Since there are an infinite number of such prime pairs, this is not a great design…

template <int N> struct TwinPrimeOf { };

template <>
struct TwinPrimeOf<17> { static constexpr int twin = 19; };
template <>
struct TwinPrimeOf<19> { static constexpr int twin = 17; };

template <>
struct TwinPrimeOf<41> { static constexpr int twin = 43; };
template <>
struct TwinPrimeOf<43> { static constexpr int twin = 41; };

Such a template is not, I suppose, strictly speaking a type trait. I’m sure they’re called trait classes or some such, but for practical purposes the notion is exactly the same: you use information known at compile time to perform work at compile time.

Conclusion

Type traits, or traits templates, are a simple but powerful mechanism for associating information with types and/or values at compile time. This article has barely scratched the surface of what can be done with them, but we can see already that with very little effort, we can start to do useful work with type traits in our own software.

Published by unicyclebloke

Somewhat loud, red and obnoxious.

Leave a comment