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.

if constexpr vs tag dispatch

Tag dispatch predates if constexpr and still has its place, but if constexpr is almost always the cleaner choice for new code. Tag dispatch routes to an overload by passing a dummy type argument derived from a trait; the correct overload is selected at call time by overload resolution. if constexpr performs the same routing inside a single function body, with no extra overloads or dummy types.

cpp
// C++11 tag dispatch: one overload per case
namespace detail {
    template<typename T>
    void serialize_impl(const T& val, std::true_type  /*is_integral*/) {
        std::cout << "int: " << val << '\n';
    }
    template<typename T>
    void serialize_impl(const T& val, std::false_type /*is_integral*/) {
        std::cout << "other\n";
    }
}

template<typename T>
void serialize_tag(const T& val) {
    detail::serialize_impl(val, std::is_integral<T>{});  // C++11
}

// C++17 if constexpr: same logic, no helper overloads
template<typename T>
void serialize_ic(const T& val) {                         // C++17
    if constexpr (std::is_integral_v<T>)
        std::cout << "int: " << val << '\n';
    else
        std::cout << "other\n";
}

Tag dispatch is still preferable when you want each case to be its own separately-named function for documentation or profiling purposes, or when a library must remain C++11/14 compatible.

if constexpr vs SFINAE

SFINAE operates at the overload-set boundary; if constexpr operates inside the function body. These are complementary, not interchangeable. Use SFINAE (or concepts in C++20) to exclude a function from consideration entirely; use if constexpr to select among implementations once the function has already been chosen.

cpp
// SFINAE: prevents the template being a candidate at all for non-numeric T
template<typename T, std::enable_if_t<std::is_arithmetic_v<T>, int> = 0>
T clamp_sfinae(T val, T lo, T hi) {       // C++14
    return val < lo ? lo : val > hi ? hi : val;
}

// if constexpr: function is always a candidate, dispatches internally
template<typename T>
T clamp_ic(T val, T lo, T hi) {           // C++17
    if constexpr (std::is_arithmetic_v<T>)
        return val < lo ? lo : val > hi ? hi : val;
    else {
        static_assert(always_false<T>, "clamp requires arithmetic type");
    }
}

With if constexpr, passing a non-arithmetic type still causes a hard error β€” the static_assert fires. With SFINAE, the template simply drops out of overload resolution and another overload (or an error about no viable candidate) takes over. Concepts (C++20) provide the cleanest interface-level gate and pair naturally with if constexpr for internal dispatch:

cpp
template<std::arithmetic T>              // C++20 concept constraint
T clamp_c20(T val, T lo, T hi) {
    if constexpr (std::is_floating_point_v<T>)
        return std::clamp(val, lo, hi);   // handles NaN
    else
        return val < lo ? lo : val > hi ? hi : val;
}

Nested if constexpr chains

Nesting if constexpr blocks is valid and sometimes unavoidable, but more than two levels usually signals a design that would benefit from a type-map or policy template. Each level is independently evaluated; only the taken branch at each level is instantiated.

cpp
template<typename T>
std::string classify(T val) {             // C++17
    if constexpr (std::is_arithmetic_v<T>) {
        if constexpr (std::is_floating_point_v<T>) {
            if constexpr (std::is_same_v<T, long double>)
                return "long double";
            else if constexpr (sizeof(T) == 8)
                return "double";
            else
                return "float";
        } else {
            return std::is_signed_v<T> ? "signed int" : "unsigned int";
        }
    } else if constexpr (std::is_pointer_v<T>) {
        return "pointer";
    } else {
        return "other";
    }
}

When chains grow beyond three or four arms, a constexpr type-dispatch table or if constexpr combined with a fold-expression visitor can be cleaner. Deep nesting also makes it harder to see which else belongs to which if.

if constexpr in lambda templates

Generic lambdas (C++14) and explicit lambda templates (C++20) support if constexpr in their bodies, making inline visitors concise. The lambda's call operator is itself a function template, so the discarding semantics apply just as in any other template.

cpp
// C++17 generic lambda with if constexpr
auto print = [](const auto& val) {
    using T = std::decay_t<decltype(val)>;
    if constexpr (std::is_same_v<T, bool>)
        std::cout << (val ? "true" : "false") << '\n';
    else if constexpr (std::is_arithmetic_v<T>)
        std::cout << val << '\n';
    else if constexpr (std::is_convertible_v<T, std::string_view>)
        std::cout << '"' << val << '"' << '\n';
    else
        std::cout << "(unprintable)\n";
};

print(42);        // "42"
print(true);      // "true"
print("hello");   // "\"hello\""

C++20 explicit template-head syntax in lambdas is sometimes clearer when you need to name the type parameter directly:

cpp
// C++20 explicit lambda template
auto to_double = []<typename T>(T val) -> double {   // C++20
    if constexpr (std::is_same_v<T, std::string>)
        return std::stod(val);
    else
        return static_cast<double>(val);
};

Compile-time branch selection in generic algorithms

if constexpr is the idiomatic way to specialize algorithmic behavior based on iterator category or type capabilities without writing multiple overloads. This keeps the entire algorithm in one readable function while still generating the optimal code path per instantiation.

cpp
#include <iterator>
#include <type_traits>

// Single implementation that uses O(1) advance for random-access iterators
// and O(n) advance for forward/bidirectional iterators             // C++17
template<typename It, typename Distance>
void advance_fast(It& it, Distance n) {
    using Cat = typename std::iterator_traits<It>::iterator_category;
    if constexpr (std::is_base_of_v<std::random_access_iterator_tag, Cat>)
        it += n;          // O(1): pointer arithmetic
    else
        while (n-- > 0) ++it;   // O(n): step forward
}

Another common use is detecting optional interface support via requires-expressions (C++20), avoiding a void_t detection idiom:

cpp
// C++20: detect optional .reserve() and call it if available
template<typename Container>
void fill(Container& c, int count) {
    if constexpr (requires { c.reserve(count); })   // C++20 requires-expression
        c.reserve(count);
    for (int i = 0; i < count; ++i)
        c.push_back(i);
}

Common misconception: both branches must be valid

A widespread misreading is "the discarded branch can be completely ill-formed" β€” this is only half-true. The non-taken branch must still be syntactically parseable and its expressions must be well-formed if they do not depend on a template parameter. Only template-dependent expressions in the discarded branch escape semantic checking.

cpp
template<typename T>
void demo(T x) {                          // C++17
    if constexpr (std::is_pointer_v<T>)
        *x;               // OK: depends on T, discarded for non-pointer
    else
        x + 1;            // OK: depends on T, discarded for pointer
}

// This still breaks β€” the ill-formed expression is template-independent:
template<typename T>
void broken(T x) {
    if constexpr (std::is_pointer_v<T>)
        *x;
    else
        1 / 0;            // division by zero: template-independent; still diagnosed
}

A practical corollary: you cannot place a goto into a discarded branch to jump over a return in the taken branch, and you cannot reference a local variable that doesn't exist in the outer scope just because "it's in the discarded branch." The discarded branch is not invisible to the compiler β€” it is just not instantiated at the semantic level for template-dependent expressions.

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