if constexpr: Compile-Time Branching in C++17
C++17 introduced if constexpr to support compile time conditional branch selection. If the condition is true at compile time, only the true…
if constexpr: Compile-Time Branching in C++17

C++17 introduced if constexpr to support compile time conditional branch selection.
If the condition is true at compile time, only the true branch is used to generate code. If the condition is false at compile time, only the false branch is used to generate code.
- The non-selected branch is discarded before semantic analysis (name lookup, type checking, overload resolution, and template processing).
- Because it is never used to generate code, it produces no symbols and no machine instructions, and therefore contributes nothing to the final binary.
A Simple philosophy: Evaluate at compile time, keep only what is needed, discard the dead code.
if constexpr (a_condition)
/* do something */;
else if constexpr (b_condition)
/* do another thing */;
else
/* do default */;
Note: The conditions must be evaluated from a constant expression.
// OK
constexpr int i=10;
if constexpr(i==10)
std::cout<<"hi";
The below will not compile since i is not a constant expression.
// NG
int i = 10;
if constexpr (i == 10) // Error: i is not a constant expression
std::cout << "hi";
Lets understand how it works with a simple example:
#include <iostream>
int main() {
constexpr int i = 10;
if constexpr (i == 10)
std::cout << "hi";
else
std::cout << "bye";
}
Lets have a look at the assembly:
.LC0:
.string "hi"
main:
push rbp
mov rbp, rsp
sub rsp, 16
mov DWORD PTR [rbp-4], 10
mov esi, OFFSET FLAT:.LC0
mov edi, OFFSET FLAT:std::cout
call std::basic_ostream<char, std::char_traits<char>>& std::operator<<<std::char_traits<char>>(std::basic_ostream<char, std::char_traits<char>>&, char const*)
mov eax, 0
leave
ret
Since the compiler knows the answer at compile time it only emits:
.LC0:
.string "hi"
mov esi, OFFSET FLAT:.LC0
mov edi, OFFSET FLAT:std::cout
call std::operator<<
So the above code compiles as if we have (no if else branching) like this:
constexpr int i=10;
std::cout<<"hi";
The same code if we write with normal if-else it would include both the conditional branches in the assembly an in-turn in the binary. Look the data section contains both hi and bi . Also the cmp jne instruction(for the if-else).

Note: When
if constexpris used there is zero runtime overhead for the branch decision, as the “choice” is made entirely by the compiler.
Lets Deep Dive into Further Details
Ok at this point you have a basic understanding of what if constexpr does and how it does. Now its time to take an example and answer the following questions:
- How its helpful in Template Meta-Programming by providing a cleaner alternative to SFINAE (Substitution Failure Is Not An Error) or tag dispatching ?
- How it differ from normal
if-else?
So here is the problem statement of our example:
We want to create a program that prints a value differently based on whether its type is int or std::string. Specifically:
- For
int: print the value doubled - For
std::string: print the value in uppercase
These are the various approaches i can think of !!!!
Approach 1: Function Overloading:
#include <iostream>
#include <string>
#include <algorithm>
void printValue(int value) {
std::cout << "Integer: " << value * 2 << std::endl;
}
void printValue(const std::string& value) {
std::string upper = value;
std::transform(upper.begin(), upper.end(), upper.begin(), ::toupper);
std::cout << "String: " << upper << std::endl;
}
int main() {
printValue(42); // Integer: 84
printValue("hello"); // String: HELLO
}
Simple right !! But it has the following drawbacks:
- This works but doesn’t scale well when you need to support many types or want to use template Meta-Programming
- The function selection happens at runtime based on the argument type, which adds runtime overhead even though we are passing compile time constants as argument and the compiler is capable of evaluating this at compile time.
- You need to write separate functions for each type you want to support
Ok fair enough lets use templates to reduce the problem of writing many overloaded functions by replacing with a generic implementation and let compiler generate the functions for us. After all thats what Template's do right.
Approach 2: Templates with Regular if (Fails!)
#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;
std::transform(upper.begin(), upper.end(), upper.begin(), ::toupper);
std::cout << "String: " << upper << std::endl;
}
}
Oops This fails to compile! Here’s why:
When you call printValue(42), the compiler instantiates the entire function body with T = int. This means both branches of the if statement get compiled:
- The first branch tries to compile
value * 2withint— this works fine - The second branch tries to compile string operations on an
int— compilation error!
Even though the runtime condition std::is_same_v<T, std::string> is false for int, the compiler still checks that the code inside that branch is syntactically valid for the given type.
So what is a compile-time solution. How about using the famous Tag Dispatch with Template function approach, lets try that.
Approach 3: Templates with Tag Dispatch
#include <iostream>
#include <string>
#include <algorithm>
#include <type_traits>
// Tag types
struct IntTag {};
struct StringTag {};
// Helper to select the appropriate tag
template<typename T>
using SelectTag = std::conditional_t<
std::is_same_v<T, int>,
IntTag,
std::conditional_t<std::is_same_v<T, std::string>, StringTag, void>
>;
// Implementation for int
template<typename T>
void printValueImpl(T value, IntTag) {
std::cout << "Integer: " << value * 2 << std::endl;
}
// Implementation for string
template<typename T>
void printValueImpl(T value, StringTag) {
std::string upper = value;
std::transform(upper.begin(), upper.end(), upper.begin(), ::toupper);
std::cout << "String: " << upper << std::endl;
}
// Main template function that dispatches to the correct implementation
template<typename T>
void printValue(T value) {
printValueImpl(value, SelectTag<T>{});
}
int main() {
printValue(42); // Integer: 84
printValue(std::string("hello")); // String: HELLO
}
This code works and we have got the compile-time solution — the compiler selects the correct function at compile time based on the tag type, so there’s no runtime overhead. But look at the code, its verbose, ugly, and tedious to maintain. You need to create tag types, multiple implementation functions, and a dispatcher function for every type-dependent operation.
Since you already know about if constexpr lets use that and re-write the solution.
Approach 4: The Elegant Solution — if constexpr
#include <iostream>
#include <string>
#include <algorithm>
#include <type_traits>
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;
}
}
int main() {
printValue(42); // Integer: 84
printValue(std::string("hello")); // String: HELLO
printValue(3.14); // Unsupported type
}
Neat, Simple and Elegant. The condition is evaluated at compile time
- Only the branch that matches is actually compiled
- The other branch is discarded and never checked for validity
- When
printValue(42)is called, only theintbranch exists in the compiled code, forprintValue(std::string("hello"))thestringbranch and forprintValue(3.14)the code under else branch.
If we look at the assembly and carve out the asembly generated for printValue(42) we can clearly see there is no branching instruction.No jmp, je, jne, or any conditional branching instructions! The function is just straight-line code that prints the doubled integer. The if constexpr branches have been completely eliminated - only the code for the int case exists in this instantiation.
void printValue<int>(int):
push rbp
mov rbp, rsp
sub rsp, 16
mov DWORD PTR [rbp-4], edi
mov esi, OFFSET FLAT:.LC2 ; "Integer: "
mov edi, OFFSET FLAT:std::cout
call std::operator<<<...> ; cout << "Integer: "
mov rdx, rax
mov eax, DWORD PTR [rbp-4]
add eax, eax ; value * 2
mov esi, eax
mov rdi, rdx
call std::ostream::operator<<(int) ; print the doubled value
; ... print endl ...
leave
ret
Notable Benefits of if constexpr
- Template Meta-Programming:
if constexpris a game-changer for template Meta-Programming. Before C++17, Meta- Programming required complex techniques like SFINAE (Substitution Failure Is Not An Error), template specialization, or tag dispatch. Now, you can write straightforward, readable code. Armed with C++20 concepts,if constexprbecomes even more powerful:
template<typename T>
auto getValue(T container) {
if constexpr (std::is_array_v<T>) {
// Handle C-style arrays
return container[0];
}
else if constexpr (requires { container.front(); }) {
// Handle containers with front() method
return container.front();
}
else if constexpr (requires { *container.begin(); }) {
// Handle containers with iterators
return *container.begin();
}
else {
return container; // Return as-is for other types
}
}
- Platform-Specific Code:
if constexpris perfect for writing cross-platform libraries where different code paths are needed based on the target platform. This allows you to write platform-specific optimizations (like SIMD intrinsics for x86 vs ARM) or use different APIs based on the operating system, all in a single codebase without the troublesome preprocessor macros.
template<typename T>
void serialize(T* ptr) {
if constexpr (sizeof(void*) == 8) {
// 64-bit platform
uint64_t address = reinterpret_cast<uint64_t>(ptr);
// Use 64-bit serialization format
}
else if constexpr (sizeof(void*) == 4) {
// 32-bit platform
uint32_t address = reinterpret_cast<uint32_t>(ptr);
// Use 32-bit serialization format
}
}
- Performance Optimization: You can create generic algorithms that optimize based on type properties. These compile-time decisions ensure zero runtime overhead while keeping code maintainable and type-safe.
template<typename T>
void processData(std::vector<T>& data) {
if constexpr (std::is_trivially_copyable_v<T>) {
// Use fast memcpy-based operations for POD types
std::memcpy(/* ... */);
}
else {
// Use proper copy constructors for complex types
for (auto& item : data) { /* ... */ }
}
}
This is the essence of if constexpr - a powerful tool that embodies modern C++'s commitment to zero-overhead abstractions and clean, maintainable code.
Found this article helpful? Please Clap 👏 and follow for more C++ and system programming content.
If you’re serious about passing technical interviews, try PracHub. It helped me structure my preparation with advanced mock sessions. [**Check it out here**] and start practicing. (Disclosure: This is an affiliate link).
메타데이터
- post_id
- ebe46c3da9cf
- slug
- cpp-if-constexpr-ebe46c3da9cf
- url
- https://towardsdev.com/cpp-if-constexpr-ebe46c3da9cf
- canonical_url
- https://towardsdev.com/cpp-if-constexpr-ebe46c3da9cf
- author_url
- https://medium.com/@sagarmadala
- status
- ok
- fetched_at
- 2026-06-23 06:34:20