Why if constexpr Doesn’t Fully Remove Code: Understanding Discarded Branches and SFINAE
The article explains that a false if constexpr condition does not magically erase the other branch; the compiler still performs semantic checks on non‑dependent code, and the real difference lies in how SFINAE discards whole overload candidates while if constexpr only discards statements after overload resolution.
Many developers assume that if constexpr (false) simply deletes the alternate branch, but the compiler still validates code that is not dependent on template parameters, causing errors even when the branch is never taken.
For example, in a non‑template function the snippet if constexpr (false) { int* p = 0; p = 1.5; } triggers error: cannot convert 'double' to 'int*' in assignment with GCC 14.2 – the discarded code is still type‑checked because there is no template instantiation context that could exempt it.
The key distinction is that SFINAE removes entire overload candidates at the declaration level, whereas if constexpr discards only a sub‑statement after overload resolution. The standard ([stmt.if]/2) states that the discarded substatement is not instantiated during the instantiation of an enclosing templated entity , and the condition must be not value‑dependent after instantiation.
Two common patterns illustrate this:
// SFINAE (enable_if tag dispatch)
template<class It>
std::enable_if_t<std::is_base_of_v<std::random_access_iterator_tag, typename std::iterator_traits<It>::iterator_category>>
adv(It& it, std::ptrdiff_t n) { it += n; }
// if constexpr inside the function body
template<class It>
void adv2(It& it, std::ptrdiff_t n) {
if constexpr (std::is_base_of_v<std::random_access_iterator_tag, typename std::iterator_traits<It>::iterator_category>)
it += n;
else
while (n--) ++it;
}With the SFINAE version, the overload that fails substitution is silently removed from the candidate set, so its body is never instantiated and no error is reported. In the if constexpr version there is only one overload; overload resolution succeeds, the function body is instantiated, and the discarded branch is still subject to semantic checks unless its statements are dependent.
Consequently, a discarded branch in a template is only exempt from instantiation when the condition is not value‑dependent and the surrounding entity is being instantiated. Non‑template functions lack this exemption, so the compiler must fully check the code, even though it will never be executed.
SFINAE’s “radius” stops at the immediate context of the substitution. Errors that appear in definitions outside that context become hard errors. For instance, using std::void_t<typename T::value_type> without the CWG 1558 rule causes the whole substitution to be ignored, turning a detection into a always‑true condition.
Practical guidance: use if constexpr only when you have already employed SFINAE (or requires) to select an implementation inside a function body. For interface‑level constraints, continue to rely on SFINAE or the newer requires clause, because if constexpr cannot prevent hard errors in signatures.
Only answer one question: where is the branch discarded?
Recent compiler changes illustrate the subtlety: a template containing static_assert(false, "unsupported T") was rejected by GCC 12 but accepted by GCC 13 after the C++23 amendment that treats such a discarded static assert as ill‑formed, no diagnostic required (IFNDR).
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
IT Services Circle
Delivering cutting-edge internet insights and practical learning resources. We're a passionate and principled IT media platform.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
