Hello! Welcome back to our exploration of C++'s powerful features.
In our last lesson, we learned how to use template specialization to provide entirely separate, optimized implementations for specific types or categories of types. This is a powerful way to override the default generic behavior, but it can sometimes lead to maintaining multiple, disconnected template definitions.
Today, we'll explore a more modern and often cleaner alternative. The goal of this lesson is to learn how to use if constexpr to perform compile-time branching within a single template. This C++17 feature allows the compiler to discard irrelevant code branches before they are even fully compiled, giving us the benefits of specialization without the boilerplate. This is a key tool for writing zero-overhead abstractions, a concept central to systems programming.
The Problem: Why a Regular if Fails in Templates
Imagine you want to write a single generic function, printValue, that behaves differently for integers and strings. For an int, it should print the value doubled. For a std::string, it should print the value in uppercase.
Using your knowledge of templates and type traits from previous lessons, you might first try something like this:
#include <iostream>
#include <string>
#include <algorithm>
#include <type_traits>
template<typename T>
void printValue(T value) {
if (std::is_same_v<T, int>) {
std::cout << "Integer: " << value * 2 << std::endl;
} else if (std::is_same_v<T, std::string>) {
std::string upper = value;
// This line is problematic for non-string types
std::transform(upper.begin(), upper.end(), upper.begin(), ::toupper);
std::cout << "String: " << upper << std::endl;
}
}
// int main() { printValue(42); } // This will cause a compile error!
This code seems logical, but it won't compile. When the compiler instantiates printValue<int>, it must ensure that all code paths within the function are syntactically valid for T = int. Even though the else if branch will never be executed at runtime, the compiler still tries to compile the std::transform line with an int, which is a type error.
This is a fundamental aspect of template instantiation that can be frustrating. Before C++17, the common solutions were complex techniques like SFINAE or tag dispatching.
The Solution: if constexpr
C++17 introduced if constexpr to solve this exact problem. It looks like a regular if, but it's a signal to the compiler that the condition is a compile-time constant. The compiler evaluates the condition and then discards the branch that isn't taken.
What does "discarded" mean? It means that code is never instantiated and doesn't even go through full semantic analysis. It's as if you had surrounded it with #if 0.
Let's look at the elegant solution using if constexpr.
if constexpr: Compile-Time Branching in C++17
This article provides a fantastic, detailed explanation of if constexpr. It starts by clearly illustrating the problem with regular if in templates and then demonstrates the clean solution with if constexpr.
Read through 'Approach 2: Templates with Regular if (Fails!)' to solidify your understanding of the problem. Then, read 'Approach 4: The Elegant Solution — if constexpr'. Pay close attention to the explanation of why the if constexpr version compiles: the discarded branch is 'never checked for validity'.
As you saw, the code becomes clean and intuitive:
template<typename T>
void printValue(T value) {
if constexpr (std::is_same_v<T, int>) {
std::cout << "Integer: " << value * 2 << std::endl;
} else if constexpr (std::is_same_v<T, std::string>) {
std::string upper = value;
std::transform(upper.begin(), upper.end(), upper.begin(), ::toupper);
std::cout << "String: " << upper << std::endl;
} else {
std::cout << "Unsupported type" << std::endl;
}
}
When you call printValue(42), the compiler generates a version of the function where only the int branch exists. All other branches are completely removed from the compiled code.
Under the Hood: The Impact on Generated Code
Your interest lies in how things work internally, so let's examine the output. With if constexpr, the branching logic is resolved entirely at compile time. The final machine code contains no if/else checks at all.

Let's see this in action.
C++ Weekly - Ep 18 C++17's constexpr if
This short video by Jason Turner demonstrates the core concept and shows that only the chosen branch is compiled into the final executable.
Watch the segments from 0:21-2:17 and 5:57-6:37. In the first part, observe the basic syntax. In the second part, focus on his explanation of what the compiler effectively generates: a function containing only the code from the selected branch. This is the 'zero-overhead' principle in action.
The key takeaway is that if constexpr isn't just a runtime if with a constant condition. It fundamentally changes what code is generated. This is why it's a tool for compile-time polymorphism—you are creating different versions of a function for different types before the program even runs.
The Most Important Detail: Templates and Dependent Names
You might now be wondering: if if constexpr just needs a constant expression, can I use it anywhere? Let's look at a common point of confusion.
Consider this code from a Stack Overflow question:
// In a template, this works:
template <typename T>
void print(T value) {
if constexpr (std::is_pointer_v<T>)
std::cout << *value << std::endl; // OK, *value not compiled for non-pointers
else
std::cout << value << std::endl;
}
// In a non-template function, this fails:
void does_not_work() {
auto value = 100;
// The type of 'value' is known here, it's 'int'.
// The condition `std::is_pointer_v<decltype(value)>` is `false`.
// So why does this fail to compile?
if constexpr (std::is_pointer_v<decltype(value)>)
std::cout << *value << std::endl; // COMPILE ERROR: cannot dereference an 'int'
else
std::cout << value << std::endl;
}
This behavior seems inconsistent, but it reveals the deep connection between if constexpr and the C++ compilation model, specifically two-phase name lookup for templates. This is exactly the kind of "under the hood" detail you appreciate.
"If constexpr" in C++17 does not work in a non-templated function
This Stack Overflow thread provides one of the best explanations available for this behavior. It's a deep dive into compiler mechanics.
Read the answer by Guillaume Racicot. This is dense but gets to the heart of the matter. Focus on these key ideas: Two-Phase Lookup: The compiler checks templates in two passes. Phase 1 (before instantiation): It checks 'non-dependent' names (code that doesn't depend on a template parameter T). An error here fails compilation immediately. Phase 2 (during instantiation): It checks 'dependent' names (code that does depend on T, like *value where value's type is T). The Punchline: if constexpr controls instantiation. A discarded branch is not instantiated, so Phase 2 lookup never happens for it. But in the non-template example, there's only one phase; *value (on an int) is checked and fails immediately.
To summarize that excellent explanation:
- In the non-template
does_not_workfunction,*valueis an error because the compiler parses the whole function at once. It sees you trying to dereference anintand immediately flags it as an error, even though it's in aconstexprbranch that would be false. - In the
print<T>template,*valueis a dependent expression—its validity depends onT. The compiler defers checking it until Phase 2 (instantiation). Becauseif constexprprevents instantiation of the discarded branch, this check never happens for non-pointer types, and thus no error occurs.
This is why if constexpr is most powerful when used inside templates with conditions that depend on the template parameters.
Test your understanding!
You want to write a generic function that processes a container.
- If the container is a
std::vector, you want to callvec.capacity(). - If the container is a
std::list, this call would be a compile error becausestd::listhas nocapacity()member.
How does if constexpr allow you to write a single function that handles both cases correctly? Relate your answer to the concept of dependent expressions and instantiation.
Show answer
By using if constexpr inside a template function template<typename T> void process(T& container), the expression container.capacity() becomes a dependent expression.
When you call process(my_list), the compiler evaluates if constexpr (std::is_same_v<T, std::vector<...>>), which is false. This causes the entire branch containing container.capacity() to be discarded.
Because the branch is never instantiated, the compiler never performs the second phase of name lookup on the dependent expression container.capacity(). It never tries to find a .capacity() member on a std::list, thus avoiding a compile error. The code behaves as if that branch never existed in the source code for that specific instantiation.
Conclusion
You've now added a critical C++17 tool to your arsenal. if constexpr allows for clean, readable, compile-time branching that generates highly efficient code.
Key Takeaways:
if constexprevaluates a constant expression at compile time and discards the branch that is not taken.- A discarded branch is not fully compiled, which prevents errors from code that would be invalid for a given type instantiation. This makes it a clean alternative to SFINAE and tag dispatch.
- The generated machine code has zero runtime overhead; the branching logic is fully resolved by the compiler.
- Its true power is unlocked inside templates, where it interacts with C++'s two-phase name lookup. It prevents the instantiation of discarded branches, thereby skipping the analysis of dependent expressions that would otherwise cause compilation to fail.
What's Next?
We have now explored two major techniques for compile-time polymorphism: template specialization (creating separate definitions) and if constexpr (branching within one definition).
In the next lesson, we will step back and compare the fundamental differences, use cases, and trade-offs between compile-time polymorphism (templates) and the other major type of polymorphism in C++: runtime polymorphism (virtual functions). Understanding when to use each is a hallmark of a proficient C++ developer.