C++ if constexpr | Compile-Time Branching in Templates
이 글의 핵심
Use if constexpr to discard untaken branches during instantiation—unlike runtime if, avoiding ill-formed code in unused branches for templates.
What is if constexpr?
Compile-time branching for templates (C++17). Discarded branches are not instantiated.
template<typename T>
auto getValue(T value) {
if constexpr (std::is_pointer_v<T>) {
return *value; // Only compiled when T is pointer
} else {
return value;
}
}
int x = 10;
auto a = getValue(x); // Uses else branch
auto b = getValue(&x); // Uses if branch
Basic usage
#include <type_traits>
#include <iostream>
template<typename T>
void print(T value) {
if constexpr (std::is_integral_v<T>) {
std::cout << "Integer: " << value << "\n";
} else if constexpr (std::is_floating_point_v<T>) {
std::cout << "Float: " << value << "\n";
} else {
std::cout << "Other: " << value << "\n";
}
}
// Usage
print(42); // "Integer: 42"
print(3.14); // "Float: 3.14"
print("hello"); // "Other: hello"
if vs if constexpr
Runtime if: All branches must compile
template<typename T>
void processRuntime(T value) {
if (std::is_pointer_v<T>) {
std::cout << *value; // ❌ Error when T=int: cannot dereference int
} else {
std::cout << value;
}
}
if constexpr: Only selected branch compiled
template<typename T>
void processCompileTime(T value) {
if constexpr (std::is_pointer_v<T>) {
std::cout << *value; // ✅ OK: only compiled when T is pointer
} else {
std::cout << value;
}
}
Real-world examples
1. Generic serialization
#include <type_traits>
#include <string>
#include <sstream>
template<typename T>
std::string toString(const T& value) {
if constexpr (std::is_arithmetic_v<T>) {
return std::to_string(value);
} else if constexpr (std::is_same_v<T, std::string>) {
return value;
} else if constexpr (std::is_same_v<T, const char*>) {
return std::string(value);
} else {
std::ostringstream oss;
oss << value;
return oss.str();
}
}
// Usage
auto s1 = toString(42); // "42"
auto s2 = toString(3.14); // "3.140000"
auto s3 = toString("hello"); // "hello"
2. Container optimization
template<typename Container, typename T>
void addElement(Container& c, const T& value) {
// Reserve space if container supports it
if constexpr (requires { c.reserve(1); }) {
c.reserve(c.size() + 1);
}
// Use push_back if available, otherwise insert
if constexpr (requires { c.push_back(value); }) {
c.push_back(value);
} else {
c.insert(c.end(), value);
}
}
// Works with vector (has reserve + push_back)
std::vector<int> vec;
addElement(vec, 10);
// Works with set (has insert only)
std::set<int> s;
addElement(s, 10);
3. Variadic print with recursion
template<typename T>
void print(const T& value) {
std::cout << value << "\n";
}
template<typename T, typename....Rest>
void print(const T& first, const Rest&....rest) {
std::cout << first;
if constexpr (sizeof...(rest) > 0) {
std::cout << ", ";
print(rest...); // Recursive call
} else {
std::cout << "\n";
}
}
// Usage
print(1, 2, 3, "hello", 3.14);
// Output: 1, 2, 3, hello, 3.14
Replacing SFINAE
Before C++17: SFINAE with enable_if
// Overload 1: for integral types
template<typename T>
std::enable_if_t<std::is_integral_v<T>, T>
square(T x) {
return x * x;
}
// Overload 2: for floating-point types
template<typename T>
std::enable_if_t<std::is_floating_point_v<T>, T>
square(T x) {
return x * x;
}
After C++17: Single function with if constexpr
template<typename T>
T square(T x) {
if constexpr (std::is_integral_v<T>) {
return x * x;
} else if constexpr (std::is_floating_point_v<T>) {
return x * x;
} else {
static_assert(std::is_arithmetic_v<T>, "T must be arithmetic");
}
}
Common mistakes
Mistake 1: Using runtime condition
template<typename T>
void process(T value, bool flag) {
if constexpr (flag) { // ❌ Error: flag is not constant expression
// ...
}
}
// ✅ Fix: use runtime if
if (flag) {
// ...
}
Mistake 2: Expecting different return types
template<typename T>
auto getValue(T x) {
if constexpr (std::is_pointer_v<T>) {
return *x; // Returns T's pointee type
} else {
return &x; // Returns T*
}
}
// ❌ Error: inconsistent return types
Fix: Use common return type or std::variant:
template<typename T>
auto getValue(T x) -> std::conditional_t<std::is_pointer_v<T>,
std::remove_pointer_t<T>,
T> {
if constexpr (std::is_pointer_v<T>) {
return *x;
} else {
return x;
}
}
Mistake 3: Assuming complete elimination
template<typename T>
void debug(T value) {
if constexpr (false) {
value.nonExistentMethod(); // ⚠️ May still cause error in some contexts
}
}
Note: Discarded branches still undergo phase 1 parsing. They must be syntactically valid.
Performance implications
Zero runtime cost: if constexpr is resolved at compile time. Generated code contains only the selected branch.
Assembly comparison (GCC 13, -O3):
template<typename T>
int process(T x) {
if constexpr (std::is_integral_v<T>) {
return x * 2;
} else {
return static_cast<int>(x);
}
}
int a = process(10); // Assembly: imul eax, 2
int b = process(3.14); // Assembly: cvttsd2si eax, xmm0
No branching instructions—completely different code paths.
Compiler support
| Compiler | if constexpr | Notes |
|---|---|---|
| GCC | 7+ | Full support |
| Clang | 3.9+ | Full support |
| MSVC | 2017 15.3+ | Full support |
Related posts
Keywords
C++, if constexpr, C++17, compile-time, templates, metaprogramming, SFINAE alternative
자주 묻는 질문 (FAQ)
Q. 이 내용을 실무에서 언제 쓰나요?
A. Use if constexpr to discard untaken branches during instantiation—unlike runtime if, avoiding ill-formed code in unu… 실무에서는 위 본문의 예제와 선택 가이드를 참고해 적용하면 됩니다.
Q. 선행으로 읽으면 좋은 글은?
A. 각 글 하단의 이전 글 또는 관련 글 링크를 따라가면 순서대로 배울 수 있습니다. C++ 시리즈 목차에서 전체 흐름을 확인할 수 있습니다.
Q. 더 깊이 공부하려면?
A. cppreference와 해당 라이브러리 공식 문서를 참고하세요. 글 말미의 참고 자료 링크도 활용하면 좋습니다.
같이 보면 좋은 글 (내부 링크)
이 주제와 연결되는 다른 글입니다.
- C++ constexpr 함수 | ‘컴파일 타임 함수’ 가이드
- C++ constexpr Lambda | ‘컴파일 타임 람다’ 가이드
- C++ CTAD | ‘클래스 템플릿 인자 추론’ 가이드
이 글에서 다루는 키워드 (관련 검색어)
C++, if constexpr, C++17, Compile-time, Templates 등으로 검색하시면 이 글이 도움이 됩니다.