본문으로 건너뛰기 C++ `if constexpr` | Compile-Time Branching in Templates

C++ `if constexpr` | Compile-Time Branching in Templates

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

Compilerif constexprNotes
GCC7+Full support
Clang3.9+Full support
MSVC2017 15.3+Full support

Keywords

C++, if constexpr, C++17, compile-time, templates, metaprogramming, SFINAE alternative


Frequently Asked Questions (FAQ)

Q. When would I use this in practice?

A. Use if constexpr to discard untaken branches during instantiation—unlike runtime if, avoiding ill-formed code in unu…

Q. What should I read before this?

A. Follow the previous article or related articles links at the bottom of each post to learn in sequence. See the C++ series index for the full picture.

Q. Where can I study this more deeply?

A. Check cppreference and the relevant library’s official documentation. The reference links at the end of the article are also worth using.


Other articles related to this topic.


Keywords Covered in This Article (Related Search Terms)

This article covers C++, if constexpr, C++17, Compile-time, Templates.