C++ / a working model

53 / 163   ·   C++17   ·   9 min

Type traits and if constexpr compile-time branching

Keep this sentence

Type traits express type properties as compile-time values or type transformations; if constexpr discards inapplicable branches when a template is instantiated. It solves the problem that different types need different well-formed expressions; it does not turn a runtime condition into a compile-time fact.

In this lesson
  1. Property queries and type transformations
  2. Discarding a branch is not preprocessor deletion
  3. Keep branches around types you actually support
  4. Example
  5. Exercise

Property queries and type transformations

C++11 <type_traits> provides queries such as is_integral and is_pointer, and transformations such as remove_reference and remove_cv. The older form reads a value through ::value and a type through ::type. The common _t aliases arrived in C++14, and the matching _v shorthands became widespread in C++17, so examples must distinguish versions.

Before you query, decide whether the type should be normalized. int, const int, and int& are not the same type; is_integral<int&> is false. If a generic function cares about properties of the referred-to value, strip the reference and then top-level const. If const itself affects writability, do not blindly remove it just to make a trait return true.

Discarding a branch is not preprocessor deletion

C++17 if constexpr requires a constant-expression condition. During template instantiation, after the condition is determined, the unselected branch is not instantiated. One branch can therefore call integer operations while another calls size, which only strings have, without requiring both types to satisfy both interfaces.

This is not #if: the compiler still parses the whole program. Errors in non-dependent names cannot hide in a dead branch, and ill-typed expressions in non-template code cannot be waived with if constexpr(false). Understand instantiation selection, not “the compiler never looks at these lines.”

Keep branches around types you actually support

The example uses one function to describe an integer or a string, removing references and top-level const before querying the type. The integer branch calls to_string, the string branch computes length, and other types get a clear diagnostic through a static_assert that depends on a template parameter. Every branch returns string, so callers see a consistent result type.

A trait answers only the specific question it defines. For example, is_move_constructible says construction from some rvalue is possible; it does not necessarily mean a dedicated move constructor exists, and it does not prove that moving is cheap. Performance, semantics, and exception guarantees should be checked separately; do not treat a trait named something like “supports move” as a license for every optimization.

Pitfalls

  • An ordinary if does not skip instantiating the other branch the way if constexpr does inside a template, even when the condition is known at compile time.
  • A discarded branch still needs legal syntax; undeclared non-dependent names, wrong header dependencies, and similar errors cannot hide behind if constexpr.

Run an example

Minimum C++17 · complete program · Download .cpp

#include <cassert>
#include <iostream>
#include <string>
#include <type_traits>

template<class> constexpr bool supported = false;
template<class T>
std::string describe(const T& x) {
    using U = std::remove_cv_t<std::remove_reference_t<T>>;
    if constexpr (std::is_integral_v<U>) {
        return "integer:" + std::to_string(x);
    } else if constexpr (std::is_same_v<U, std::string>) {
        return "text:" + std::to_string(x.size());
    } else {
        static_assert(supported<U>, "requires integer or string");
    }
}

int main() {
    const int n = 7;
    std::string s = "abc";
    assert(describe(n) == "integer:7");
    assert(describe(s) == "text:3");
    std::cout << describe(n) << ' ' << describe(s) << '\n';
}

Compile locally

g++ -std=c++17 -Wall -Wextra -Wpedantic -pthread modern-traits.cpp -o example && ./example

Expected result

integer:7 text:3

CHECK YOUR UNDERSTANDING

Close the answer. Explain it.

Why does describe("abc") not enter the string branch? How do you explicitly take the existing string path?

Show a reference answer

A string literal is a character array; deduction through const T& does not automatically become std::string. After stripping top-level qualifiers it is still not string, so it falls into the unsupported branch. Write describe(std::string("abc")) to construct the required type explicitly. If the interface should accept several text forms with zero copies, design a separate string_view entry rather than misclassifying array types.

Check the sources

Drafts and official chapters change. The version mark is only the example’s minimum.

Back to the catalog