Skip to content
C++
Language
since C++17
Basic

if constexpr (C++17)

"C++17 compile-time branching in templates: discards the non-taken branch entirely without instantiating it, replacing most SFINAE and tag-dispatch patterns."

if constexprsince C++17

A compile-time conditional that parses but does not instantiate the non-taken branch, allowing template bodies to reference operations that would be ill-formed for other template arguments.

Overview

if constexpr solves a fundamental template authoring problem: how to write a single function body that takes structurally different paths depending on type properties β€” paths that may reference operations the other type doesn't support at all.

The critical distinction from a plain if with a constexpr condition: in a plain if, both branches are fully type-checked and instantiated regardless of which one executes. With if constexpr, the non-taken branch is parsed for syntax but its expressions are not semantically analyzed or instantiated. This means the discarded branch can freely reference members, operators, or functions that don't exist for the current template argument β€” as long as those expressions depend on a template parameter.

cpp
// Plain if β€” both branches compiled, both must be valid for every T
template<typename T>
void bad(T x) {
    if (std::is_pointer_v<T>)
        *x;   // compile error for non-pointer T even though condition is false
}

// if constexpr β€” discarded branch is not instantiated
template<typename T>                      // C++17
void good(T x) {
    if constexpr (std::is_pointer_v<T>)
        *x;       // compiled only for pointer types
    else
        x + 1;    // compiled only for non-pointer types
}

Syntax

cpp
if constexpr ( condition ) statement
if constexpr ( condition ) statement else statement
if constexpr ( init-statement ; condition ) statement   // C++17 init-if form

The condition must be a constant expression convertible to bool. It does not need to depend on a template parameter β€” if constexpr (sizeof(int) == 4) is valid outside templates β€” but when the condition is template-independent, neither branch is truly discarded: the compiler still validates both semantically, since there is no substitution to drive discarding.

Examples

Replacing SFINAE

Multiple enable_if overloads collapse into a single readable function with equivalent generated code:

cpp
// C++14: separate overloads required
template<typename T, std::enable_if_t<std::is_integral_v<T>, int> = 0>
void describe(T val) { /* integral path */ }

template<typename T, std::enable_if_t<std::is_floating_point_v<T>, int> = 0>
void describe(T val) { /* float path */ }

// C++17: one function, same codegen
template<typename T>
void describe(T val) {
    if constexpr (std::is_integral_v<T>)
        std::cout << "int: " << val << '\n';
    else if constexpr (std::is_floating_point_v<T>)
        std::cout << "float: " << val << '\n';
    else
        std::cout << "other\n";
}

if constexpr is not a full replacement for SFINAE at the interface level. It cannot remove a function from an overload set based on argument types β€” for that, use concepts (C++20) or enable_if.

Variadic pack recursion

Before C++17 fold expressions, variadic templates required explicit base-case specializations. if constexpr replaces them:

cpp
// C++14: two overloads required
template<typename T>
T sum(T a) { return a; }

template<typename T, typename... Args>
T sum(T a, Args... args) { return a + sum(args...); }

// C++17: single template with if constexpr
template<typename T, typename... Args>
T sum(T a, Args... args) {
    if constexpr (sizeof...(args) == 0)   // sizeof... since C++11
        return a;
    else
        return a + sum(args...);
}

The same pattern applies to type-list walking, tuple iteration, and heterogeneous argument processing.

Compile-time type dispatch

cpp
template<typename T>
inline constexpr bool always_false = false;   // C++17 inline variable β€” see Pitfalls

template<typename T>
std::string to_json(const T& val) {
    if constexpr (std::is_same_v<T, bool>) {           // C++17 _v helpers
        return val ? "true" : "false";
    } else if constexpr (std::is_integral_v<T>) {
        return std::to_string(val);
    } else if constexpr (std::is_floating_point_v<T>) {
        return std::format("{}", val);                 // C++20
    } else if constexpr (std::is_same_v<T, std::string>) {
        return '"' + val + '"';
    } else if constexpr (std::ranges::range<T>) {      // C++20
        std::string r = "[";
        for (const auto& item : val)
            r += to_json(item) + ',';
        if (!val.empty()) r.pop_back();
        return r + ']';
    } else {
        static_assert(always_false<T>,
            "to_json: unsupported type β€” add a specialization");
    }
}

std::variant visitor (C++17)

cpp
std::variant<int, double, std::string> v = "hello";   // C++17

std::visit([](const auto& val) {
    using T = std::decay_t<decltype(val)>;
    if constexpr (std::is_same_v<T, std::string>)
        std::cout << "string: " << val << '\n';
    else
        std::cout << "number: " << val << '\n';
}, v);

Compile-time policy flags in class templates

cpp
template<typename T, bool Logging = false>
class Cache {
    std::unordered_map<std::string, T> store_;
public:
    void set(std::string key, T value) {
        if constexpr (Logging)                          // zero overhead when false
            std::cout << "[Cache] set(" << key << ")\n";
        store_[std::move(key)] = std::move(value);
    }
};

Cache<int, true>  logged;   // logging compiled in
Cache<int, false> silent;   // logging branch absent from binary entirely

Constant-evaluation dispatch (C++20 / C++23)

if constexpr is a template mechanism. For detecting whether a constexpr function is being evaluated at compile time vs. runtime, use the related but distinct std::is_constant_evaluated (C++20) or if consteval (C++23):

cpp
// C++20: std::is_constant_evaluated() in a plain if
constexpr double fast_sqrt(double x) {
    if (std::is_constant_evaluated()) {     // C++20 β€” NOT if constexpr
        double r = x;
        for (int i = 0; i < 50; ++i) r = (r + x / r) / 2.0;
        return r;
    } else {
        return __builtin_sqrt(x);
    }
}

// C++23: if consteval β€” cleaner, purpose-built
constexpr double fast_sqrt23(double x) {
    if consteval {                          // C++23
        double r = x;
        for (int i = 0; i < 50; ++i) r = (r + x / r) / 2.0;
        return r;
    } else {
        return __builtin_sqrt(x);
    }
}

if consteval can only appear in constexpr/consteval functions and its condition is fixed: "am I being constant-evaluated?" if constexpr takes an arbitrary constant expression and works anywhere templates appear.

Common Pitfalls

static_assert(false) fires in discarded branches

The single most common mistake. A static_assert whose condition is template-independent is evaluated before branch discarding:

cpp
template<typename T>
void bad_fallback(T) {
    if constexpr (std::is_integral_v<T>) { /* ... */ }
    else {
        static_assert(false, "unsupported");  // fires for every instantiation, even integral ones
    }
}

Fix with a dependent false that only evaluates when the branch is actually instantiated:

cpp
template<typename T>
inline constexpr bool always_false = false;   // C++17

template<typename T>
void good_fallback(T) {
    if constexpr (std::is_integral_v<T>) { /* ... */ }
    else {
        static_assert(always_false<T>, "unsupported");   // fires only for non-integral T
    }
}

Discarded branches must be syntactically valid

The parser still processes discarded branches. Syntax errors are unconditional:

cpp
template<typename T>
void f(T x) {
    if constexpr (std::is_integral_v<T>)
        ++x;
    else
        x.;       // syntax error β€” fails to parse regardless of which branch is taken
}

Semantic errors (calling a non-existent member, using an undefined operator) are fine in discarded branches, provided the ill-formed expressions depend on template parameters. Template-independent semantic errors may still be diagnosed.

Does not affect overload resolution

if constexpr inside a function body cannot make that function appear or disappear from an overload set for specific argument types. For interface-level exclusion β€” preventing a template from being a candidate for certain types β€” you still need concepts (C++20) or enable_if_t. if constexpr is internal dispatch, not external filtering.

Condition outside a template is not truly discarding

cpp
// Outside any template: both branches are fully compiled
if constexpr (sizeof(int) == 4) {
    // taken on LP64 β€” but the else is still checked
} else {
    // not taken β€” but still must be semantically valid
}

This is useful for platform-specific code, but do not expect the "discarding" behavior to suppress errors in the not-taken branch.

Best Practices

  • Replace multi-overload enable_if patterns with if constexpr when all cases fit in one function body. The code is significantly more readable and the codegen is identical.
  • Use concepts (C++20) to constrain templates at the call boundary; use if constexpr for internal dispatch within an already-accepted argument.
  • In every exhaustive if constexpr chain, end with a static_assert(always_false<T>, "...") rather than silently accepting unsupported types or falling through to undefined behavior.
  • Prefer if consteval (C++23) over if (std::is_constant_evaluated()) (C++20) in constexpr functions β€” the older form has subtle footguns around which branch the compiler optimizes.
  • if constexpr reduces compile times vs. separate overloads: fewer template specializations are instantiated.

See Also

  • SFINAE β€” overload-set control that if constexpr cannot replace
  • Concepts β€” C++20 interface-level constraints
  • Type Traits β€” <type_traits> predicates used as conditions
  • Fold Expressions β€” C++17 variadic alternative to recursive if constexpr
  • consteval / if consteval β€” C++20/23 constant-evaluation enforcement