Compile-Time Branching — Advanced
Advanced if constexpr patterns with concepts, consteval guards, SFINAE migration, and if consteval — plus the pitfalls that trip up experienced engineers.
if constexprsince C++17A compile-time branch selector that prevents instantiation of the discarded branch in a template — enabling type-specific logic without SFINAE or tag dispatch.
Overview
if constexpr evaluates its condition as a constant expression and, inside a template, removes the non-taken branch from instantiation entirely. The discarded branch never becomes machine code for those template arguments, so it can reference member functions or types that don't exist — as long as those references are dependent on a template parameter.
Without if constexpr, the equivalent required either SFINAE (std::enable_if specializations), tag dispatch, or explicit template specializations — patterns that scatter a single logical operation across multiple overloads and produce cryptic error messages.
The dependent name requirement is the most important subtlety. if constexpr suppresses instantiation only of dependent expressions. Non-dependent ill-formed code in the discarded branch is still a hard error:
template<typename T>
void example(T val) {
if constexpr (std::is_integral_v<T>) { // C++17
val.non_existent(); // discarded for non-integral T — OK: name depends on T
}
}
void non_template() {
if constexpr (false) {
int x = "not an int"; // ERROR: not in a template — both branches fully checked
}
}Outside a template, if constexpr is not #if. Both branches must be semantically valid; only the runtime execution of the non-taken branch is suppressed.
Syntax
// Basic form — C++17
if constexpr (constant-expression) statement
if constexpr (constant-expression) statement else statement
// With init-statement — C++17
if constexpr (auto x = compute(); predicate(x)) { use(x); }
// if consteval — C++23 (replaces is_constant_evaluated() idiom)
if consteval { /* compile-time path */ } else { /* runtime path */ }
if !consteval { /* runtime-only */ }The condition must be a contextually converted constant expression of type bool. It may reference constexpr variables, type traits, concept checks, sizeof, requires expressions, or any combination thereof.
Examples
Replacing SFINAE
The canonical motivation. SFINAE requires mirrored enable conditions across multiple declarations:
// C++14: SFINAE — hard to read, easy to get the negated condition wrong
template<typename Num, typename Den,
std::enable_if_t<std::is_integral_v<Num> &&
std::is_integral_v<Den>>* = nullptr>
auto divide(Num n, Den d) {
if (d == Den{0}) throw std::domain_error{"division by zero"};
return n / d;
}
template<typename Num, typename Den,
std::enable_if_t<!std::is_integral_v<Num> ||
!std::is_integral_v<Den>>* = nullptr>
auto divide(Num n, Den d) { return n / d; }
// C++17: single function, linear reading order
template<typename Num, typename Den>
auto divide(Num n, Den d) {
if constexpr (std::is_integral_v<Num> && std::is_integral_v<Den>) {
if (d == Den{0}) throw std::domain_error{"division by zero"};
}
return n / d;
}Variadic Recursion Without a Base-Case Overload
Before C++17, compile-time variadic recursion required a separate overload to terminate:
// C++17: self-contained, no second overload needed
template<typename T, typename... Rest>
T sum(T first, Rest... rest) {
if constexpr (sizeof...(rest) == 0)
return first;
else
return first + sum(rest...);
}
auto total = sum(1, 2, 3, 4); // 10Member Detection with Concepts (C++20)
// C++20
template<typename T>
concept HasSerialize = requires(const T& t) {
{ t.serialize() } -> std::convertible_to<std::string>;
};
template<typename T>
void persist(const T& val) {
if constexpr (HasSerialize<T>)
store(val.serialize());
else if constexpr (std::is_trivially_copyable_v<T>)
store_raw(&val, sizeof(val));
else
static_assert(sizeof(T) == 0, // dependent false — see Pitfalls
"T must be serializable or trivially copyable");
}Fold Expressions Combined with if constexpr
Type-specific formatting across a variadic pack using a lambda per element:
// C++17 fold + if constexpr; std::print requires C++23
template<typename... Ts>
void print_typed(const Ts&... vals) {
([](const auto& v) {
using V = std::decay_t<decltype(v)>;
if constexpr (std::is_floating_point_v<V>)
std::print("{:.4f} ", v); // C++23
else if constexpr (std::is_same_v<V, bool>)
std::print("{} ", v ? "true" : "false");
else
std::print("{} ", v); // C++23
}(vals), ...);
std::println(""); // C++23
}
print_typed(42, 3.14159, true, "hello");
// 42 3.1416 true helloconsteval and if consteval (C++20/23)
consteval (C++20) forces compile-time-only execution. if consteval (C++23) cleanly splits compile-time vs runtime paths inside a constexpr function — replacing the fragile is_constant_evaluated() idiom:
// C++20: is_constant_evaluated() in a plain if — works correctly
constexpr double accurate_sqrt(double x) {
if (std::is_constant_evaluated()) { // plain if, NOT if constexpr — see Pitfalls
double y = x;
for (int i = 0; i < 20; ++i) y = 0.5 * (y + x / y);
return y;
} else {
return std::sqrt(x); // fast runtime path
}
}
// C++23: if consteval — same semantics, no trap
constexpr double accurate_sqrt(double x) {
if consteval {
double y = x;
for (int i = 0; i < 20; ++i) y = 0.5 * (y + x / y);
return y;
} else {
return std::sqrt(x);
}
}Best Practices
Prefer concept overloads for primary dispatch. When branches apply to fundamentally different types, separate constrained overloads (C++20) compose better and produce cleaner errors than a single function with if constexpr chains:
// C++20: independently callable, documentable, mockable
void process(std::ranges::range auto const& r) { /* range path */ }
void process(std::integral auto v) { /* integer path */ }
// Reserve if constexpr for within-template logic that shares significant
// setup or teardown code across type variants.Terminate chains with a dependent static_assert. The static_assert(sizeof(T) == 0, ...) pattern gives actionable error messages when no branch matches:
template<typename T>
void handle(const T& v) {
if constexpr (std::is_integral_v<T>) { handle_int(v); }
else if constexpr (std::is_class_v<T>) { handle_obj(v); }
else {
// sizeof(T) == 0 is always false but depends on T → fires only at instantiation
static_assert(sizeof(T) == 0, "handle(): requires integral or class type");
}
}Use if consteval in C++23, not if constexpr (is_constant_evaluated()). The older form requires extra care to avoid the always-true trap (see Pitfalls).
Common Pitfalls
if constexpr outside templates does not suppress semantic checking. Both branches must be well-formed regardless of the condition value. This surprises engineers who expect #if-like behaviour:
void f() {
if constexpr (false) {
int x = "oops"; // still a compile error — not in a template
}
}if constexpr (std::is_constant_evaluated()) is always true. The condition of if constexpr is itself a constant expression context, so is_constant_evaluated() unconditionally returns true. The else branch becomes permanently dead code:
constexpr int bad(int x) {
if constexpr (std::is_constant_evaluated()) { // always true here!
return x * 2; // only branch ever taken
} else {
return x * 3; // unreachable
}
}
// Fix: use plain `if (std::is_constant_evaluated())` or C++23 `if consteval`Non-dependent static_assert fires at template definition time. A static_assert(false, ...) with no dependency on a template parameter triggers when the template is parsed, not when it's instantiated — even inside a discarded branch:
template<typename T>
void g() {
if constexpr (std::is_integral_v<T>) { }
else {
static_assert(false, "bad"); // ERROR at definition, not at use
static_assert(sizeof(T) == 0, // OK: dependent on T
"requires integral type");
}
}Mismatched return types without auto. if constexpr does not exempt a function from having consistent return type. Use auto so deduction happens per instantiation:
// Fails to compile if branches return different types
template<typename T>
auto get_value(T val) {
if constexpr (std::is_pointer_v<T>)
return *val; // deduced as pointed-to type
else
return val; // deduced as T — auto resolves per-instantiation
}See Also
- constexpr Functions — compile-time computation context
- Concepts — cleaner alternative for type dispatch
- consteval and constinit — compile-time-only functions
- Template Type Traits — the predicates powering if constexpr conditions
- Tag Dispatch and SFINAE — the patterns if constexpr replaces