Type Traits

A detailed and practical tutorial for type traits in modern C++ covering how we can use them, and examples of the most common use cases
This lesson is part of the course:

Professional C++

Comprehensive course covering advanced concepts, and how to use them on large-scale projects.

DreamShaper_v7_fantasy_female_pirate_Sidecut_hair_black_clothi_0.jpg
Ryan McCombe
Ryan McCombe
Posted

Type traits are a feature of C++ that allows us to perform compile-time analysis of the types we use in our code. In this lesson, we’ll introduce type traits, and then cover examples of common use cases, including:

  • Template type safety and documentation: ensuring that template functions are only called with the expected types, thereby preventing runtime errors, and improving the developer experience
  • Compile-Time if statements: enabling or disabling lines of code based on the properties of a type, or other compile-time factors
  • Template metaprogramming: writing code to change how templates are generated and used at compile time
  • Conditional type selection: changing the data types we use based on boolean expressions
  • Custom types - how our own user-defined types can interact with any of these systems

Using Type Traits

The C++ standard library includes a large collection of type traits, and related functions, within the <type_traits> header file

#include <type_traits>

Type traits are within the std namespace. Almost all standard library-type traits are templated structs. They accept the type we want to investigate as a template parameter and make the result of that investigation available through a static value field.

For example, the std::is_arithmetic type trait tells us if a type is numeric:

std::is_arithmetic<int>::value // true
std::is_arithmetic<std::string>::value // false

These booleans can be used like any other boolean in our code:

#include <type_traits>
#include <iostream>
#include <vector>

int main() {
  if (std::is_arithmetic<float>::value) {
    std::cout << "float is arithmetic";
  }

  if (!std::is_arithmetic<std::string>::value) {
    std::cout << " but std::string isn't";
  }

  using vector_trait =
      std::is_arithmetic<std::vector<int>>;

  if (!vector_trait::value) {
    std::cout
        << "\nNeither is std::vector<int>";
  }
}
float is arithmetic but std::string isn't
Neither is std::vector<int>

From C++17 onwards, we have a more concise way to access the value field of standard library traits. We can instead append _v to the class name or, less commonly, we can use the () operator:

#include <type_traits>
#include <iostream>

int main() {
  if (std::is_arithmetic_v<double>) {
    std::cout << "double is arithmetic\n";
  }
  if (std::is_arithmetic<std::int32_t>()) {
    std::cout << "int32_t is also arithmetic";
  }
}
double is arithmetic
int32_t is also arithmetic

More usefully, we’d use type traits in a situation where we don’t necessarily know what type we’re dealing with. This means type traits are most commonly used when working with templates:

#include <type_traits>
#include <iostream>

template <typename T>
void Function(T Param) {
  if (std::is_arithmetic<T>::value) {
    std::cout << "Type is arithmetic\n";
  } else {
    std::cout << "Type is not arithmetic\n";
  }
}

int main() {
  Function(100);
  Function("Hello World");
}
Type is arithmetic
Type is not arithmetic

Compile Time Conditionals with if constexpr

It’s worth reiterating that type trait code is evaluated at compile time, not run time. When we want to perform a conditional check on something that is known at compile time, we can attach constexpr to the basic if statement, creating an if constexpr statement:

#include <type_traits>
#include <iostream>

template <typename T>
void Function(T Param) {
  if constexpr (is_integral_v<T>) {
    std::cout << "Integer!\n";
  }

  if constexpr (is_floating_point_v<T>) {
    std::cout << "Floating point number!\n";
  }
}

int main() {
  Function(42);
  Function(3.14);
}
Integer!
Floating point number!

By evaluating if statements at compile time, it allows non-matching code to be entirely removed. When the compiler encounters our calls to template functions, it is able to remove the conditional code from the generated function. We can imagine this process yielding results similar to the following:

void Function(int Param) {
  std::cout << "Integer!\n";
}

void Function(double Param) {
  std::cout << "Floating point number!\n";
}

This simpler form of output that if constexpr enables is quite important when doing more advanced work with templates. But perhaps more importantly, if our if statement can be resolved at compile time, making it if constexpr makes that behavior obvious to anyone reading our code.

As such, in situations where we can use if constexpr we generally should prefer it over if.

Below, we show more examples of if constexpr, and some more useful standard library type traits:

#include <type_traits>
#include <iostream>

class Actor {};
class Monster : Actor {};

template <typename T>
void Function(T Param) {
  using namespace std;
  if constexpr (is_pointer_v<T>) {
    cout << "\nType is a pointer";
  }

  if constexpr (is_class_v<T>) {
    cout << "\nType is a class";
  }

  if constexpr (is_same_v<Actor, T>) {
    cout << ": Actor";
  } else if constexpr (is_base_of_v<Actor, T>) {
    cout << ": Actor (or derived from Actor)";
  }
}

int main() {
  int x{5};
  Function(&x);

  Actor MyActor;
  Function(MyActor);

  Monster MyMonster;
  Function(MyMonster);
}
Type is a pointer
Type is a class: Actor
Type is a class: Actor (or derived from Actor)

For a list of all traits that are available by default, there are many standard library references we can use like cppreference.com

Type Safety with Type Traits

In the previous section, we created this simple template function.

template <typename TFirst, typename TSecond>
auto Average(TFirst x, TSecond y) {
  return (x + y) / 2;
}

This function allows us to pass two numeric values, and get their average. Because the types are templated, we can pass any type to this function.

However, that’s a bit too permissive. Our function is intended to work with numbers, but that is not really made explicit. What if another type is passed to this function? One of two things can happen.

In the best-case scenario, the type will not support one of the operators we’re using in the function, such as /. This will result in a compiler error, albeit not a very clear one:

binary '/':
'std::basic_string<char,std::char_traits<char>,std::allocator<char>>'
does not define this operator or a conversion to a type acceptable
to the predefined operator

In the worst case, the type we pass does implement all the required operators, but the use case for those operators is not the same as what our function is assuming. For example, our template function is assuming the + and / operators of the provided type are used for arithmetic addition and division.

But types are free to overload the + and / operators for any purpose - they don’t need to mean addition and division.

In such a case, the compiler will be happy to instantiate the template, because the type meets all the requirements. We won’t get any compiler error - we’ll instead have introduced a bug.

Type traits allow us to be more explicit about the requirements of our types. The most basic example of this is simply using a static_assert to ensure the type meets the expected criteria:

#include <type_traits>

template <typename TFirst, typename TSecond>
auto Average(TFirst x, TSecond y) {
  static_assert(
      std::is_arithmetic<TFirst>::value,
      "Average: First argument must be arithmetic");
  static_assert(
      std::is_arithmetic<TSecond>::value,
      "Average: Second argument must be arithmetic");
  return (x + y) / 2;
}

This has three benefits:

  • It implements the desired requirements. Now, the compiler will ensure that the provided type is a number - not just that it implements the + and / operators.
  • It documents our requirements. Anyone looking at our Average function will be able to see what the requirements are. Some IDEs will also be able to better understand the requirements of this function, and provide immediate feedback as we’re writing the code
  • If a type doesn’t meet the requirements, the error message will be more useful:
int main() {
  Average("Hello", "World");
}
static_assert failed: 'Average: First argument must be arithmetic'
static_assert failed: 'Average: Second argument must be arithmetic'

std::enable_if and SFINAE

The std::enable_if trait accepts two template parameters - a boolean and a type. If the boolean is true, the resulting struct has a type field, containing the type we passed.

In other words, we can imagine the following code:

#include <type_traits>

int main() {
  std::enable_if<true, int>::type MyInt{42};
}

At compile time, we can imagine our enable_if code will resolve simply to int, meaning our code is equivalent to this:

#include <type_traits>

int main() {
  int MyInt{42};
}

More commonly, we use the std::enable_if_t alias, which gives us direct access to the type:

std::enable_if_t<true, int> MyInt{42};

Let's see what happens when the boolean value is false:

#include <type_traits>

int main() {
  std::enable_if<false, int>::type MyInt{42};
}

In this case, we can imagine our code being equivalent to this:

#include <type_traits>

int main() {
  MyInt{42};
}

This code is invalid, so we get a compilation error. So, how is any of this useful?

It relates to a rule around function templates, called substitution failure is not an error, or SFINAE

In general, it means that when the compiler is considering whether a function template can create a function to serve a function call. When we substitute the types passed to our function call into a function template, it is okay if that process results in invalid code.

That template just gets ignored and the compiler moves on to the other candidates.

We can create template functions that may conditionally result in substitution failure. For example, consider the following function:

using std::enable_if_t, std::is_class_v;
template <typename T,
          enable_if_t<is_class_v<T>, int> = 0>
void Function(T Arg) {}

If T is a class, our template substitution will look something like this:

template <typename T, int = 0>
void Function(T Arg) {}

The above is entirely valid, so this function will be able to handle our call. However, if T is not a class, our template will look like this after substitution:

template <typename T, = 0>
void Function(T Arg) {}

This time, the resulting code was not valid. But, under the SFINAE rule, substitution failure does not mean our program necessarily fails to compile. It means this template cannot be used to create a function with the argument types provided, but there may be another template that can serve the request.

Below, we have a template that will result in substitution failure if the provided type is a class, and another that will result in substitution failure if the type is not a class.

#include <type_traits>
#include <iostream>;

using std::enable_if_t, std::is_class_v;
template <typename T,
          enable_if_t<is_class_v<T>, int> = 0>
void Function(T Arg) {
  std::cout << "That was a class\n";
}

template <typename T,
          enable_if_t<!is_class_v<T>, int> = 0>
void Function(T Arg) {
  std::cout << "That wasn't\n";
}

class Character {};

int main() {
  Function(Character{});
  Function(3.14);
}

The program compiles and runs successfully, because substitution failure is not an error, and all function calls were able to be handled by a template that didn’t suffer substitution failure:

That was a class
That wasn't

If the above code feels overly complicated, don’t worry - it is.

This is a brief introduction to a topic known as template metaprogramming, which is notoriously difficult and unfriendly. So much so that C++20 introduced a new language feature called concepts that makes many of the use cases for template metaprogramming more accessible.

We introduce concepts in the next lesson.

Conditional Type Selection with std::conditional

Within type_traits, the std::conditional template lets us dynamically select a type to use at compile time. It accepts three template parameters - a boolean value, a type to use if the boolean is true, and a type to use if the boolean is false. The resulting type is available from the type static member.

Here are some examples:

using namespace std;

// This will be int
conditional<true, int, float>::type

// We can use conditional_t to access ::type directly
// This will be std::string
conditional_t<true, string, char*>

// The bool can be any expression known at compile time
// This will be std::string
constexpr bool useStdString{true};
conditional_t<useStdString, string, char*>

// This will be std::vector<float>
conditional_t<false, vector<int>, vector<float>>

// This will be std::vector<float> too
vector<conditional_t<false, int, float>>

Typically, std::conditional is used to create a type alias, which we can then easily use throughout our code:

#include <iostream>
#include <type_traits> // for std::conditional
#include <typeinfo> // for typeid

constexpr bool Use64BitIntegers{true};
using integer =
    std::conditional<Use64BitIntegers,
                     std::int64_t,
                     std::int32_t>::type;

int main() {
  integer MyInt{42};
  std::cout << typeid(integer).name() << " ("
            << sizeof(integer) << " bytes)";
}
__int64 (8 bytes)

Altering Types

The type_traits header has a range of utilities that receive a type, and return a new type based on that input. For example, the std::remove_const struct is created by providing a type as a template parameter.

A resulting type is available within the type static member, which is equivalent to the input type, with the const specifier removed.

// Results in int
std::remove_const<const int>::type

// We can append _t instead of accessing ::type
// This results in int
std::remove_const_t<const int>

// The input type does not need to be const
// This results in int
std::remove_const_t<int>
  

This is typically used when we want to compare types, but we don’t necessarily care if the types have specifiers such as const:

#include <iostream>
#include <type_traits>

// Compares types without removing const
template <typename TFirst, typename TSecond>
void FuncA(TFirst& A, TSecond& B) {
  using namespace std;
  if constexpr (is_same_v<TFirst, TSecond>) {
    cout << "FuncA: Types are the same\n";
  } else {
    cout << "FuncA: Types are not the same\n";
  }
}

// Removes const, then compares types
template <typename TFirst, typename TSecond>
void FuncB(TFirst& A, TSecond& B) {
  using namespace std;
  if constexpr (is_same_v<
                    remove_const_t<TFirst>,
                    remove_const_t<TSecond>>) {
    cout << "\nFuncB: Types are the same";
  } else {
    cout << "\nFuncB: Types are not the same";
  }
}

int main() {
  int x{1};
  const int y{2};

  FuncA(x, x);
  FuncA(x, y);

  FuncB(x, x);
  FuncB(x, y);
}
FuncA: Types are the same
FuncA: Types are not the same

FuncB: Types are the same
FuncB: Types are the same

The standard library includes several more helpers to modify types in similar ways. A standard library reference such as cppreference.com will include a complete list.

Creating our own Type Traits

As usual, we’re not restricted to just the type traits that come with the standard library. We can get pull traits in from third-party libraries or create our own. Let's imagine we have a template function that receives objects that may or may not have a render function. We’d like to write an is_renderable trait to help us out:

template <typename T>
void Function(T Param) {
  if constexpr (is_renderable<T>::value) {
    Param.Render();
  } else {
    std::cout << "\nNot Renderable";
  }
}

Standard library-type traits typically make a boolean available as the value field. We can easily make our struct behave in the same way by inheriting from the helper classes std::false_type and std::true_type, available by including <type_traits>.

Typically, we’ll want value to be false by default, so we inherit from false_type:

template <typename>
struct is_renderable : std::false_type {};

Then, for any class where we want the trait to be true, we can provide a specialization of our type trait that inherits from true_type instead:

template <>
struct is_renderable<MyClass> : std::true_type {};

With these 4 lines of code, we now have our own custom-type trait working:

#include <type_traits>
#include <iostream>

template <typename>
struct is_renderable : std::false_type {};

class Fish {
 public:
  void Render() {
    std::cout << "Fish: ><((((`>";
  }
};

template <>
struct is_renderable<Fish> : std::true_type {};

class Sound {};

template <typename T>
void Function(T Param) {
  if constexpr (is_renderable<T>::value) {
    Param.Render();
  } else {
    std::cout << "\nNot Renderable";
  }
}

int main() {
  Fish MyFish;
  Function(MyFish);

  Sound MySound;
  Function(MySound);
}
Fish: ><((((`>
Not Renderable

There is much more we can do with type traits and templates in general, but we don’t want to get too advanced in this course. Additionally, a new addition to the C++20 overlaps with type traits somewhat, and it’s useful to understand our options before going too deep.

This new addition is called concepts, and we cover it in the next lesson.

Making Custom Types Interact with Standard Library Type Traits

A common question is whether we can make our own custom types interact with standard library type traits in the same way as the built-in fundamental types, such as int.

For example, we may have created our own numeric type, and would like objects such as std::is_arithmetic_t to return true when provided with it as a template parameter.

This may be possible but is generally not recommended. The C++ specification does not define what should happen when we try this - ie, it is undefined behavior. It may work in some compilers, it may not work in others. So, it’s not something we should try to do - we should instead find an alternative way to implement our required behavior.

Was this lesson useful?

Ryan McCombe
Ryan McCombe
Posted
This lesson is part of the course:

Professional C++

Comprehensive course covering advanced concepts, and how to use them on large-scale projects.

Templates
7a.jpg
This lesson is part of the course:

Professional C++

Comprehensive course covering advanced concepts, and how to use them on large-scale projects.

Free, unlimited access!

This course includes:

  • 106 Lessons
  • 550+ Code Samples
  • 96% Positive Reviews
  • Regularly Updated
  • Help and FAQ
Next Lesson

C++20 Concepts

A full guide to the concepts feature added in C++20. Learn how we can improve and document our templates with practical examples
c.jpg
Contact|Privacy Policy|Terms of Use
Copyright © 2023 - All Rights Reserved