if constexpr (C++17)

Compile-time conditional evaluation in templates

The Problem: SFINAE Complexity

Before C++17, conditional template logic required SFINAE techniques that were verbose and error-prone.

// C++11/14: SFINAE for conditional behavior
template<typename T>
typename std::enable_if<std::is_integral<T>::value, std::string>::type
type_name() {
    return "integer";
}

template<typename T>
typename std::enable_if<std::is_floating_point<T>::value, std::string>::type
type_name() {
    return "floating point";
}

// Verbose, requires overloads, hard to read

C++17: if constexpr

if constexpr evaluates conditions at compile-time, discarding the non-taken branch.

// C++17: Clean compile-time branching
template<typename T>
auto type_name() {
    if constexpr (std::is_integral_v<T>) {
        return "integer";
    } else if constexpr (std::is_floating_point_v<T>) {
        return "floating point";
    } else {
        return "other";
    }
}

// Single function, clear logic, no SFINAE needed!

How It Works

The discarded branch is not instantiated, so it can contain code that wouldn't compile for that type.

template<typename T>
void process(T value) {
    if constexpr (std::is_pointer_v<T>) {
        // This branch is only compiled when T is a pointer
        std::cout << "Pointer to: " << *value << std::endl;
    } else {
        // This branch is only compiled when T is not a pointer
        // *value would be a compile error here if T isn't a pointer
        // but it's discarded for pointer types!
        std::cout << "Value: " << value << std::endl;
    }
}

process(42);        // Prints "Value: 42"
process(&answer);   // Prints "Pointer to: 42"

Key Characteristics

Compile-Time Only

Condition must be a constant expression. No runtime overhead.

Discarded Branch

Non-taken branch is not instantiated. Can have invalid code for that type.

Template Context

Must be in a template context. Doesn't work in regular functions.

Return Type

All branches must still produce compatible return types.

Practical Examples

1. Type-Safe Container Printing

template<typename Container>
void print(const Container& c) {
    if constexpr (requires { c.key_comp(); }) {
        // It's an associative container (map/set)
        std::cout << "Associative container:" << std::endl;
        for (const auto& [key, value] : c) {
            std::cout << "  " << key << " -> " << value << std::endl;
        }
    } else {
        // It's a sequence container
        std::cout << "Sequence container:" << std::endl;
        for (const auto& elem : c) {
            std::cout << "  " << elem << std::endl;
        }
    }
}

2. Generic Serializer

template<typename T>
std::string serialize(const T& value) {
    if constexpr (std::is_same_v<T, std::string>) {
        return """ + value + """;
    } else if constexpr (std::is_arithmetic_v<T>) {
        return std::to_string(value);
    } else if constexpr (requires { value.serialize(); }) {
        return value.serialize();
    } else {
        static_assert(false, "Cannot serialize this type");
    }
}

3. Deep Copy Helper

template<typename T>
auto deep_copy(const T& value) {
    if constexpr (std::is_pointer_v<T>) {
        // Deep copy for pointers
        return value ? std::make_unique<std::remove_pointer_t<T>>(*value) 
                     : nullptr;
    } else if constexpr (requires { value.clone(); }) {
        // Use clone() method if available
        return value.clone();
    } else {
        // Default: copy construction
        return T{value};
    }
}

Common Pitfalls

1. Must Be in Template Context

// ERROR: if constexpr only works in templates
void regular_function(bool flag) {
    if constexpr (flag) {  // ERROR: not a template
        // ...
    }
}

// CORRECT: Must be templated
template<bool flag>
void templated_function() {
    if constexpr (flag) {  // OK
        // ...
    }
}

2. All Branches Must Compile (Return Type)

template<typename T>
auto bad_function(T value) {
    if constexpr (std::is_integral_v<T>) {
        return value;           // Returns int
    } else {
        return std::to_string(value);  // Returns string
    }
    // ERROR: Incompatible return types!
}

// SOLUTION: Use common type or explicit return
template<typename T>
std::common_type_t<T, std::string> good_function(T value) {
    if constexpr (std::is_integral_v<T>) {
        return value;
    } else {
        return std::to_string(value);
    }
}

C++20 Concepts + if constexpr

C++20 concepts provide an even cleaner approach for many use cases:

// C++20: Concepts often replace if constexpr
template<typename T>
void process(T value) 
    requires std::integral<T> || std::floating_point<T>
{
    // Only accepts arithmetic types
}

// But if constexpr is still useful for:
// - Implementation details within a function
// - Optimization paths
// - When you need the same interface but different implementations

Best Practices

  • Replace SFINAE with if constexpr when possible (clearer code)
  • Use requires expressions (C++20) in conditions when appropriate
  • Keep branches focused and simple
  • Don't use for runtime conditions (regular if is clearer)
  • Avoid deeply nested if constexpr chains (consider specialization instead)