Templates

Type Traits

A type trait is a simple template that, during compilation, provides an answer to a question concerning a type. The answer is a type or a Boolean value that other template programs can read and thus allow decisions that would normally need runtime tests to be made during compilation instead.

Probably the easiest question to answer is if a type is an integral type. The trait which can answer this question is defined in the standard header, <type_traits>, and is called std::is_integral. The nested member, value, is a compile time Boolean constant.

#include <iostream>
#include <type_traits>

int main()
{
    std::cout << std::is_integral<int>::value << '\n';
    std::cout << std::is_integral<double>::value << '\n';
    return 0;
}

The expression std::is_integral<int>::value causes the trait template to be instantiated with the type int. As int is an integral type, the nested constant value turns into true, printing out 1. The next line causes the same trait to instantiate with double; the constant is false and prints out 0. No function call occurs at runtime; the Boolean is fixed before the program even begins.

One shorter form, available from C++17 onward, is the variable template std::is_integral_v. It expands directly to the bool value, eliminating the need to write ::value:

#include <iostream>
#include <type_traits>

int main()
{
    std::cout << std::is_integral_v<int> << '\n';
    std::cout << std::is_integral_v<double> << '\n';
    return 0;
}

Both lines give the same result as before. The compiler is still instantiating the underlying trait; the _v form simply provides a shortcut that returns the Boolean straight away.

This is also true for floating point types. To see if a type is a floating point, the trait std::is_floating_point_v can be used.

#include <iostream>
#include <type_traits>

int main()
{
    std::cout << std::is_floating_point_v<float> << '\n';
    std::cout << std::is_floating_point_v<int> << '\n';
    return 0;
}

Instantiation with float returns true, instantiation with int returns false. Each trait only looks at the type it has been given; it does not check the runtime value.

Traits are effective within other templates when you want to generate different code for different kinds of type. A function template that prints a value chooses a different message based on a trait's outcome.

#include <iostream>
#include <type_traits>

template <typename T>
void describe(T)
{
    // Query this type property at compile time
    if constexpr (std::is_integral_v<T>)
        std::cout << "integral\n";
    else if constexpr (std::is_floating_point_v<T>)
        std::cout << "floating-point\n";
    else
        std::cout << "other\n";
}

int main()
{
    describe(42);
    describe(3.14);
    describe('A');
    return 0;
}

The argument to describe is unused; only its type is important. The if constexpr statements are evaluated during template instantiation, and for describe<int> the first check passes so the compiler discards the other branches and generates only describe<int> by keeping the line printing integral. For describe<double> the second branch is kept, and for describe<char> both type trait tests fail so the branch at the end of the template gets kept. So we are left with three separate functions, each with a single insert statement.

The other basic question is whether two types are exactly equal or not. The trait std::is_same_v gives an answer for it.

#include <iostream>
#include <type_traits>

int main()
{
    std::cout << std::is_same_v<int, int> << '\n';
    std::cout << std::is_same_v<int, double> << '\n';
    return 0;
}

The first instantiation compares int with int and results in true, whereas the second instantiation compares int with double and results in false. Again, the value of comparison is a compile time constant and nothing executes at runtime to compare the two.

Since traits are just templates, they can be combined. Here's a little helper that determines whether a type is integral or floating point based on the two existing ones.

#include <iostream>
#include <type_traits>

// Make the following definition depend on template arguments
template <typename T>
constexpr bool is_number_v = std::is_integral_v<T> || std::is_floating_point_v<T>;

int main()
{
    std::cout << is_number_v<int> << '\n';
    std::cout << is_number_v<double> << '\n';
    std::cout << is_number_v<char> << '\n';
    return 0;
}

The variable template is_number_v is defined as the logical or of the two given standard traits. Instantiating the template with either int or double yields true, while instantiating it with char yields false as char is integral, but the example treats the combined question as the interesting question. Now the helper may be used anywhere a Boolean trait is required.

Type traits, then, push decisions that take effect solely on the type out of the running program and into the compile time. They keep the primary templates simple, while providing the compile time answers that allow those templates to adapt to each real type they are instantiated with.

Exercise: Type TraitsStarter code, hints and solutionOpenClose
Guided practiceBeginner · 10–15 min

Practice: Type Traits

Work through the mission, reveal hints only when needed, and compare your result with the model solution.

Your challenge

Call describe with true and a string literal. Identify each deduced type and which compile-time branch it selects.

Mission checklist

  1. Run the starter as a baseline, then predict the change requested in Your challenge.
  2. Make that change in your editor; use the lesson-specific hints if you need help.
  3. Compile and run your answer, then compare with the worked solution and explain the result.

Starter program

This is the working baseline, not the completed answer. Compile as C++20, then make the change described above.

#include <iostream>
#include <type_traits>

template <typename T>
void describe(T)
{
    // Query this type property at compile time
    if constexpr (std::is_integral_v<T>)
        std::cout << "integral\n";
    else if constexpr (std::is_floating_point_v<T>)
        std::cout << "floating-point\n";
    else
        std::cout << "other\n";
}

int main()
{
    describe(42);
    describe(3.14);
    describe('A');
    return 0;
}

Hints

Hint 1

describe chooses an if constexpr branch from T, not from the runtime magnitude of the value. char counts as an integral type even when it prints like a character.

Hint 2

Try describe(true) and then a string literal. Identify the argument type for each call before deciding between integral, floating-point, and other.

Worked solution

The new bool argument follows the integral branch. The string literal decays to a character pointer for this by-value parameter, which is neither integral nor floating point, so it follows other. if constexpr selects the applicable branch at compile time.

#include <iostream>
#include <type_traits>

template <typename T>
void describe(T)
{
    // Query this type property at compile time
    if constexpr (std::is_integral_v<T>)
        std::cout << "integral\n";
    else if constexpr (std::is_floating_point_v<T>)
        std::cout << "floating-point\n";
    else
        std::cout << "other\n";
}

int main()
{
    describe(42);
    describe(3.14);
    describe('A');
    describe(true);
    describe("text");
    return 0;
}

Example output

integral
floating-point
integral
integral
other

0 comments

No comments yet. Be the first.

Add a comment

About CppLearn

CppLearn is an interactive, free reference for modern C++. In each lesson, you'll find out how the language works, rather than just lines of code that you can copy and paste. We cover concepts starting from beginner lessons on fundamentals and proceed to templates, move semantics, smart pointers, and areas that usually trap or scare C++ learners.