---
title: 'Type Traits'
source: 'https://academia.sh/en/courses/cpp-templates/type-traits'
course: 'Templates and Performance'
language: en
updated: '2026-10-21T05:02:46+00:00'
license: 'CC BY-SA 4.0'
objectives:
    - 'Tells the standard type traits (is_integral_v, is_same_v, remove_cvref_t, conditional_t) apart as queries and transformations.'
    - 'Shows that selection with if constexpr does not compile the discarded branch in that instantiation.'
    - 'Builds a custom type trait on true_type and false_type.'
    - 'Explains that a decision made at compile time leaves zero queries at run time, and that constexpr alone does not guarantee this.'
---

# Type Traits

Standard type traits, selection at compile time with if constexpr, static_assert, writing your own type trait, and the selection leaving zero queries at run time.

The previous lesson counted how many bodies pack expansion makes the compiler write. When a template
has to look at a type and change its behavior, a second question comes up: when is that look taken,
and who pays for it? A **type trait** is a small template that answers a question about a type during
compilation. The claim of the lesson is this: when the selection is made at compile time, no query is
left at run time. The cost stays in the **compile** kind and carries a product count beside it.

This distinction answers a different question from M11/K01. K01 asked who makes a decision; this
course asks who pays the cost of a mechanism and what it is counted with. K01's `compiler` kind named
a decision. The **compile** kind here names a cost, and always carries a product count beside it.

## Queries and Transformations

`<type_traits>` provides two kinds of traits. **Queries** produce a `bool`: `std::is_integral_v<T>`
is true if the type is an integer type, and `std::is_same_v<A, B>` is true if two types are
identical. **Transformations** produce a new type: `std::remove_cvref_t<T>` strips the `const`,
`volatile` and reference qualifiers from a type; `std::conditional_t<K, A, B>` becomes `A` if the
condition is true and `B` otherwise. The suffixed `_v` and `_t` forms are shorthand for writing
`::value` and `::type`. All of them are evaluated at compile time.

```cpp
// traits.cpp — queries and transformations
#include <cstdio>
#include <string>
#include <type_traits>

int main() {
    std::printf("integral %d %d %d\n", std::is_integral_v<int>,
                std::is_integral_v<double>, std::is_integral_v<bool>);
    std::printf("same %d %d\n", std::is_same_v<int, int>,
                std::is_same_v<int, long>);
    std::printf("strip %d %d\n",
                std::is_same_v<std::remove_cvref_t<const int&>, int>,
                std::is_same_v<std::remove_cvref_t<std::string&&>, std::string>);
    using selected = std::conditional_t<(sizeof(int) >= 2), int, long>;
    std::printf("condition %d\n", std::is_same_v<selected, int>);
    return 0;
}
```

```text
integral 1 0 1
same 1 0
strip 1 1
condition 1
```

`bool` is an integer type, so the third query is true. When `const int&` is stripped, it becomes
`int`; the reference qualifier (`&&`) is stripped as well. `conditional_t` selects a type, not a
value: here `selected` is equal to `int`. This output is guaranteed by the standard; the result of
the first two queries does not change in any implementation.

## Selection with if constexpr

The way to use a query result to select a branch is `if constexpr`. The condition is evaluated at
compile time, and **the discarded branch is not compiled in that instantiation**. An ordinary `if`,
on the other hand, compiles both branches; if you write `x.size()` in one branch and give `int` as
`T`, compilation stops even if that branch never runs. `if constexpr` solves this problem: if `T` is
an integer, the compiler does not look at the `.size()` branch.

Below, `kind_of` gives every type a label. `kind_tagged` does the same job with a run-time
parameter, that is, with a decision supplied while running, and counts a query on every call.

```cpp
// kind.hpp — selection at compile time and at run time
#ifndef KIND_HPP
#define KIND_HPP

#include <string>
#include <type_traits>

inline int queries = 0;
inline int tagged_queries = 0;

template <typename T>
const char* kind_of(T) {
    if constexpr (std::is_integral_v<T>) {
        return "integer";
    } else if constexpr (std::is_floating_point_v<T>) {
        return "floating";
    } else {
        return "other";
    }
}

template <typename T>
const char* kind_tagged(T, int tag) {
    ++tagged_queries;
    if (tag == 0) {
        return "integer";
    }
    if (tag == 1) {
        return "floating";
    }
    return "other";
}

template <typename T>
long length(T x) {
    if constexpr (std::is_same_v<std::remove_cvref_t<T>, std::string>) {
        return static_cast<long>(x.size());
    } else {
        return static_cast<long>(x);
    }
}

#endif
```

```cpp
// kind-select.cpp — same result, different query count
#include <cstdio>
#include <string>

#include "kind.hpp"

int main() {
    int same = 0;
    same += std::string(kind_of(1)) == kind_tagged(1, 0);
    same += std::string(kind_of(2L)) == kind_tagged(2L, 0);
    same += std::string(kind_of('a')) == kind_tagged('a', 0);
    same += std::string(kind_of(1.5)) == kind_tagged(1.5, 1);
    same += std::string(kind_of(2.5f)) == kind_tagged(2.5f, 1);
    same += std::string(kind_of(std::string("x"))) == kind_tagged(std::string("x"), 2);
    std::printf("same=%d queries=%d tagged=%d\n", same, queries, tagged_queries);
    std::printf("length %ld %ld\n", length(std::string("abcd")), length(7));
    return 0;
}
```

```text
same=6 queries=0 tagged=6
length 4 7
```

**MT20.** In all six calls the two methods give the same label (`same=6`). `kind_of` makes no
query at run time (`queries=0`): which branch is selected has been decided in the compiler, and a
single `return` is left in each instantiation. `kind_tagged` reads a tag on every call
(`tagged=6`). This row is the `if constexpr selection` measurement in the core. `length` also shows
why the discarded branch is not compiled: for `int` there is no `x.size()` branch, and for `string`
there is no `static_cast<long>(x)` branch. With a plain `if`, both would be compile errors.

## static_assert and Constraints

`static_assert(condition, "message")` checks the condition at compile time. If the condition is
false, compilation stops and the message appears in the compiler output; if it is true, no code is
generated. The failure is described in prose here, because code that does not compile is not written
as a block: instantiating a template that contains `static_assert(std::is_integral_v<T>)` with
`double` stops compilation; compilers usually show, next to the message, where the instantiation was
requested from. This way, a wrong use is caught at compile time instead of as an error at run time.

## Writing Your Own Type Trait

The standard traits are built on `std::true_type` and `std::false_type`. Both have a `::value`
member. To write your own trait, the **primary template** derives from `false_type`, and the
**partial specialization** for the type you are interested in derives from `true_type`.

```cpp
// custom-trait.cpp — a custom type trait
#include <cstdio>
#include <string>
#include <type_traits>
#include <vector>

template <typename T>
struct is_vector : std::false_type {};

template <typename U>
struct is_vector<std::vector<U>> : std::true_type {};

template <typename T>
inline constexpr bool is_vector_v = is_vector<T>::value;

static_assert(is_vector_v<std::vector<int>>, "a vector is a vector");
static_assert(!is_vector_v<int>, "an int is not a vector");

int main() {
    std::printf("is_vector %d %d %d\n", is_vector_v<std::vector<double>>,
                is_vector_v<std::string>, is_vector_v<int>);
    return 0;
}
```

```text
is_vector 1 0 0
```

The partial specialization from the Template Specialization lesson is put to work here: if the
`std::vector<U>` pattern matches, `true_type` is chosen, and if none matches, the primary template
remains. As seen in that lesson, a specialization has to be visible before the first use. The two
`static_assert`s generate no code in this file; if the program compiled, both of them passed.

## Relations Between Types

Some queries look not at a single type but at the relation between two types.
`std::is_base_of_v<B, D>` is true if `D` derives from `B`. `std::is_convertible_v<A, B>` asks
whether a value of `A` can be implicitly converted to `B`. `std::is_trivially_copyable_v<T>` says
whether the type can be copied with a memory copy; since a `std::string` carries ownership inside it,
it gives `false` to this question: its copy is made not byte by byte but by its own copy constructor.
`std::decay_t<T>` performs the type conversion that happens when a value is passed to a function by
value: it strips the reference and the `const`, and turns an array type into a pointer.

```cpp
// relations.cpp — type relations and decay
#include <cstdio>
#include <string>
#include <type_traits>

struct Base {};
struct Derived : Base {};

int main() {
    std::printf("base %d %d\n", std::is_base_of_v<Base, Derived>,
                std::is_base_of_v<Derived, Base>);
    std::printf("convert %d %d\n", std::is_convertible_v<int, double>,
                std::is_convertible_v<std::string, int>);
    std::printf("copy %d %d\n", std::is_trivially_copyable_v<int>,
                std::is_trivially_copyable_v<std::string>);
    std::printf("decay %d %d\n",
                std::is_same_v<std::decay_t<const int&>, int>,
                std::is_same_v<std::decay_t<int[3]>, int*>);
    return 0;
}
```

```text
base 1 0
convert 1 0
copy 1 0
decay 1 1
```

**MT21.** `is_base_of` is directional: `Derived` derives from `Base`, and the reverse is not true.
`int` converts to `double`; `std::string` does not convert to `int`. `int` can be copied with a
memory copy, while `std::string` cannot. `decay_t<int[3]>` reduces the array type to `int*`. All of
these queries are answered at compile time; the lines in the output are the answers that compilation
produced, not a duration. None of them leaves a query at run time: for `int` and `std::string` the
values are already in the compiler's hands.

## Writing Your Own Transformation

Transformations are written with the same pattern. The primary template leaves the type as it is, and
partial specializations strip a pattern. The standard `std::remove_pointer_t` strips one layer of
pointer; here we build a recursive version that strips all the layers, together with a `_t` alias.
The `::type` member defined with `using` takes the place of the standard library's `::type`.

```cpp
// strip.cpp — a custom transformation trait
#include <cstdio>
#include <type_traits>

template <typename T>
struct strip_pointer {
    using type = T;
};

template <typename T>
struct strip_pointer<T*> {
    using type = typename strip_pointer<T>::type;
};

template <typename T>
using strip_pointer_t = typename strip_pointer<T>::type;

static_assert(std::is_same_v<strip_pointer_t<int>, int>);
static_assert(std::is_same_v<strip_pointer_t<int*>, int>);

int main() {
    std::printf("strip %d %d %d\n", std::is_same_v<strip_pointer_t<char**>, char>,
                std::is_same_v<strip_pointer_t<const int*>, const int>,
                std::is_same_v<strip_pointer_t<double>, double>);
    return 0;
}
```

```text
strip 1 1 1
```

**MT22.** The partial specialization strips one layer of pointer and descends to the remaining type
by calling itself: `char**` becomes `char` in two steps. This recursion lives inside the compiler;
it makes the compiler write one instantiation for each layer and leaves no trace at run time. One
point to watch is the `const int*` example: it is not the pointer itself but the type it points to
that is `const int`, so the result stays `const int`. The `const`ness of the pointer itself is in a
different position, and this pattern does not look at it.

## Combining Type Traits with Constraints

Type traits can be asked not only one at a time but also together. If we want a function to work only
for an unsigned integer type, we check several traits together with `static_assert`:
`std::is_integral_v<T> && std::is_unsigned_v<T>`. Conditions that can be combined with logical
operators can also be written with `std::conjunction_v`, `std::disjunction_v` and `std::negation_v`;
this second form short-circuits, that is, it does not instantiate the later traits once the condition
is settled.

```cpp
// combine.cpp — combining traits
#include <cstdio>
#include <type_traits>

template <typename T>
constexpr bool unsigned_integer_v = std::is_integral_v<T> && std::is_unsigned_v<T>;

template <typename T>
constexpr bool non_numeric_v =
    std::conjunction_v<std::negation<std::is_integral<T>>,
                       std::negation<std::is_floating_point<T>>>;

int main() {
    std::printf("unsigned %d %d %d\n", unsigned_integer_v<unsigned>,
                unsigned_integer_v<int>, unsigned_integer_v<double>);
    std::printf("non_numeric %d %d\n", non_numeric_v<int>,
                non_numeric_v<const char*>);
    return 0;
}
```

```text
unsigned 1 0 0
non_numeric 0 1
```

**MT23.** `unsigned` is both an integer and unsigned, `int` is signed, and `double` is not an
integer. `non_numeric_v` is true only for types that are neither integer nor floating point: false
for `int`, true for a pointer. What is worth recording here is that these combinations too are
resolved entirely in the compiler; no trace is left at run time, and the cost is again in the
**compile** kind.

## The Return Type Is Selected Too

The branches of `if constexpr` can return values of different types; the discarded branch does not
take part in return type deduction. For this reason an `auto` return type sees a single `return` in
every instantiation. Returning `long` and `double` in the same function with an ordinary `if` is a
compile error, because the two `return`s deduce different types. If we want to write the return type
explicitly, `std::conditional_t` computes a type name.

```cpp
// return-type.cpp — return type by branch
#include <cstdio>
#include <type_traits>

template <typename T>
auto twice(T x) {
    if constexpr (std::is_integral_v<T>) {
        return x * 2L;
    } else {
        return x * 2.0;
    }
}

template <typename T>
using wide_t = std::conditional_t<std::is_integral_v<T>, long long, double>;

int main() {
    std::printf("twice %d %d\n",
                std::is_same_v<decltype(twice(1)), long>,
                std::is_same_v<decltype(twice(1.5f)), double>);
    std::printf("wide %d %d\n", std::is_same_v<wide_t<short>, long long>,
                std::is_same_v<wide_t<float>, double>);
    std::printf("value %ld %.1f\n", twice(21), twice(1.25f));
    return 0;
}
```

```text
twice 1 1
wide 1 1
value 42 2.5
```

**MT24.** `twice(1)` selects the integer branch and returns `long`; `twice(1.5f)` selects the other
branch and returns `double`. The two return types live in two instantiations of a single function, and
they have separate bodies. `wide_t` moves the same decision into a type alias. The `short` and
`float` arguments in the second line fall into separate branches as an integer and a floating-point
type. Because the selection ends at compile time, it leaves no branch query at run time.

If you want to forbid unsupported types with a `static_assert` in an `else` branch, the condition has
to depend on `T`; a condition that does not depend on it may be evaluated when the template is
defined, even if that branch is never selected, and may stop compilation. For this reason, in such
branches use a type trait that ties the condition to `T`.

## The Bounding Measurement

A decision made at compile time leaves no step at run time, but writing `constexpr` alone does not
guarantee this. A `constexpr` function means it can be called both at compile time and at run time.
The context decides which one it will be: the initial value of a `constexpr` variable, or a context
that requires constant evaluation such as `static_assert`, moves the evaluation to compile time.
`std::is_constant_evaluated()` tells these two cases apart.

```cpp
// constant-evaluation.cpp — the same constexpr function, two contexts
#include <cstdio>
#include <type_traits>

inline int steps = 0;

constexpr int sum_of_squares(int n) {
    int t = 0;
    for (int i = 1; i <= n; ++i) {
        if (!std::is_constant_evaluated()) {
            ++steps;
        }
        t += i * i;
    }
    return t;
}

int main() {
    constexpr int a = sum_of_squares(10);
    std::printf("compile  result=%d steps=%d\n", a, steps);

    volatile int n = 10;
    int b = sum_of_squares(n);
    std::printf("run      result=%d steps=%d\n", b, steps);
    return 0;
}
```

```text
compile  result=385 steps=0
run      result=385 steps=10
```

**MT25.** The same function gives the same result; the call assigned to a `constexpr` variable is
evaluated at compile time and does not touch the counter (`steps=0`). A `volatile` argument, on the
other hand, prevents the compiler from knowing the value; the call is made at run time and ten
iterations are counted (`steps=10`). For this reason `constexpr` is not a promise but a permission:
what moves the cost to compile time is a context that requires constant evaluation. The number is
the evidence for the classification; the answer to the measurement question is a kind name, and here
that name is the **compile** kind.

## Summary

- The mechanism measured in this lesson moves the cost to the **compile** kind: the type query and
  the selection of the branch end at compile time, and `queries=0` is left at run time.
- Queries (`is_integral_v`, `is_same_v`) give a `bool`; transformations (`remove_cvref_t`,
  `conditional_t`) give a type.
- `if constexpr` does not compile the discarded branch in that instantiation; a plain `if` compiles
  both branches.
- A custom type trait is built by having the primary template derive from `false_type` and the
  partial specialization from `true_type`; `static_assert` stops a wrong use at compile time.
- `constexpr` does not guarantee the move: the same function gives `steps=0` in constant evaluation
  and `steps=10` at run time.

## Next Step

In this lesson the type query pulled the selection to compile time and left zero queries at run time.
The next lesson takes the same query one step back: how does the compiler eliminate a candidate
template that is not viable without raising an error, and what does overload resolution choose after
this elimination?
