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++17A 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.
// 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
if constexpr ( condition ) statement
if constexpr ( condition ) statement else statement
if constexpr ( init-statement ; condition ) statement // C++17 init-if formThe 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:
// 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:
// 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
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)
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
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 entirelyConstant-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):
// 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:
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:
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:
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
// 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_ifpatterns withif constexprwhen 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 constexprfor internal dispatch within an already-accepted argument. - In every exhaustive
if constexprchain, end with astatic_assert(always_false<T>, "...")rather than silently accepting unsupported types or falling through to undefined behavior. - Prefer
if consteval(C++23) overif (std::is_constant_evaluated())(C++20) inconstexprfunctions β the older form has subtle footguns around which branch the compiler optimizes. if constexprreduces compile times vs. separate overloads: fewer template specializations are instantiated.
See Also
- SFINAE β overload-set control that
if constexprcannot 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