본문으로 건너뛰기 C++ std::enable_if | Conditional Templates and SFINAE

C++ std::enable_if | Conditional Templates and SFINAE

C++ std::enable_if | Conditional Templates and SFINAE

이 글의 핵심

enable_if and enable_if_t enable SFINAE-friendly template overloads. Learn the three placement styles, common type predicates, real examples, and when to migrate to C++20 concepts instead.

The Problem: Unconstrained Templates Accept Everything

A template function that should only work for certain types will give confusing errors if called with the wrong type:

#include <type_traits>
#include <iostream>

// Should work for integers, but nothing prevents calling with a string
template<typename T>
T doubleValue(T x) {
    return x * 2;  // error deep in instantiation if T doesn't support *
}

doubleValue(5);         // OK
doubleValue(3.14);      // OK — doubles work too, maybe unintended
// doubleValue("hello"); // error in instantiation — confusing message

enable_if lets you conditionally enable or disable a template based on type properties, producing a clear “no matching overload” at the call site.


What is SFINAE?

SFINAE — Substitution Failure Is Not An Error. When the compiler substitutes template arguments, if substitution fails (because enable_if’s ::type doesn’t exist), that candidate is silently removed from overload resolution rather than causing a compile error.

enable_if<true, T>::type   → T   (substitution succeeds — overload stays)
enable_if<false, T>::type  → (no ::type)  (substitution fails — overload removed)

The Three Placement Styles

1. In the Return Type

#include <type_traits>

// Only enabled when T is integral (int, long, char, ...)
template<typename T>
std::enable_if_t<std::is_integral_v<T>, T>
doubleValue(T x) {
    return x * 2;
}

doubleValue(5);    // OK: int → 10
doubleValue(5L);   // OK: long → 10
// doubleValue(3.14);  // no matching overload — clean error at call site

enable_if_t<condition, T> is C++14 shorthand for typename enable_if<condition, T>::type.

2. As a Default Template Parameter

Useful for class templates or when you want the return type to remain clean:

template<typename T,
         typename = std::enable_if_t<std::is_arithmetic_v<T>>>
class Statistics {
    std::vector<T> data_;
public:
    void add(T value) { data_.push_back(value); }
    T mean() const {
        T sum{};
        for (T v : data_) sum += v;
        return sum / static_cast<T>(data_.size());
    }
};

Statistics<int>    s1;  // OK — int is arithmetic
Statistics<double> s2;  // OK — double is arithmetic
// Statistics<std::string> s3;  // error — string is not arithmetic

3. As a Dummy Function Parameter

template<typename T>
void print(T value,
           std::enable_if_t<std::is_pointer_v<T>>* = nullptr) {
    std::cout << "pointer → " << *value << '\n';
}

template<typename T>
void print(T value,
           std::enable_if_t<!std::is_pointer_v<T>>* = nullptr) {
    std::cout << "value → " << value << '\n';
}

int n = 42;
print(&n);   // pointer → 42
print(100);  // value → 100

This style is the most fragile — the dummy parameter can cause issues with perfect forwarding and is harder to read. Prefer the return type or default template parameter style.


Selecting Between Overloads

The most common use of enable_if is selecting between two overloads based on type properties:

#include <type_traits>
#include <iostream>

// Overload 1: integral types
template<typename T>
std::enable_if_t<std::is_integral_v<T>>
serialize(T value) {
    std::cout << "int[" << sizeof(T)*8 << "]: " << value << '\n';
}

// Overload 2: floating-point types
template<typename T>
std::enable_if_t<std::is_floating_point_v<T>>
serialize(T value) {
    std::cout << "float[" << sizeof(T)*8 << "]: " << std::fixed << value << '\n';
}

// Overload 3: string-like types
template<typename T>
std::enable_if_t<std::is_convertible_v<T, std::string_view>>
serialize(T value) {
    std::cout << "string: " << value << '\n';
}

int main() {
    serialize(42);          // int[32]: 42
    serialize(3.14);        // float[64]: 3.140000
    serialize("hello");     // string: hello
}

The conditions must be mutually exclusive — if two overloads could match, you get an ambiguity error.


Common Type Predicates

#include <type_traits>

// Type categories
std::is_integral_v<T>          // int, long, char, bool, ...
std::is_floating_point_v<T>    // float, double, long double
std::is_arithmetic_v<T>        // integral or floating_point
std::is_pointer_v<T>           // raw pointers
std::is_reference_v<T>         // lvalue or rvalue references
std::is_class_v<T>             // class or struct types
std::is_enum_v<T>              // enum or enum class

// Properties
std::is_const_v<T>             // const-qualified
std::is_same_v<T, U>           // T and U are exactly the same type
std::is_base_of_v<Base, T>     // T is derived from Base (or is Base)
std::is_convertible_v<From, To> // From is implicitly convertible to To
std::is_copy_constructible_v<T> // T can be copy-constructed
std::is_default_constructible_v<T> // T can be default-constructed

// Removing qualifiers (useful for comparisons)
std::remove_cv_t<T>            // remove const/volatile
std::remove_reference_t<T>     // remove & or &&
std::decay_t<T>                // remove refs + apply array/function decay

Multiple Conditions

Combine conditions with && and ||:

// Only for signed integers (excludes bool and unsigned types)
template<typename T>
std::enable_if_t<
    std::is_integral_v<T> &&
    std::is_signed_v<T> &&
    !std::is_same_v<T, bool>,
    T>
safeNegate(T value) {
    return -value;
}

safeNegate(-5);   // OK: int
safeNegate(42L);  // OK: long
// safeNegate(5U);    // error: unsigned not signed
// safeNegate(true);  // error: bool excluded

When conditions get complex, a helper concept or type trait alias improves readability:

template<typename T>
constexpr bool is_signed_integer_v =
    std::is_integral_v<T> && std::is_signed_v<T> && !std::is_same_v<T, bool>;

template<typename T>
std::enable_if_t<is_signed_integer_v<T>, T>
safeNegate(T value) { return -value; }

Practical: Type-Safe Container

#include <vector>
#include <type_traits>
#include <stdexcept>

template<typename T,
         typename = std::enable_if_t<std::is_arithmetic_v<T>>>
class NumericVector {
    std::vector<T> data_;
public:
    void push(T value) { data_.push_back(value); }

    T sum() const {
        T result{};
        for (T v : data_) result += v;
        return result;
    }

    T max() const {
        if (data_.empty()) throw std::runtime_error("empty");
        return *std::max_element(data_.begin(), data_.end());
    }
};

int main() {
    NumericVector<int> vi;
    vi.push(3); vi.push(1); vi.push(4);
    std::cout << "sum: " << vi.sum() << ", max: " << vi.max() << '\n';
    // sum: 8, max: 4

    // NumericVector<std::string> vs;  // compile error — string is not arithmetic
}

Migrating to C++20 Concepts

Every enable_if pattern has a cleaner concept equivalent:

// === Pre-C++20: enable_if ===
template<typename T>
std::enable_if_t<std::is_integral_v<T>, T>
square(T x) { return x * x; }

// === C++20: concept (abbreviated function template) ===
auto square(std::integral auto x) { return x * x; }

// === C++20: requires clause ===
template<typename T> requires std::integral<T>
T square(T x) { return x * x; }

Error message comparison for square(3.14f):

  • enable_if: “no matching function for call to square(float)” + multiple lines of template substitution notes
  • Concept: “float does not satisfy the constraint integral” — one readable line

Key Takeaways

  • enable_if<condition, T>::type exists only when condition is true — when false, substitution fails silently (SFINAE)
  • Three placement styles: return type (most common for functions), default template parameter (good for classes), dummy parameter (fragile, avoid when possible)
  • Mutually exclusive conditions are required when selecting between overloads — overlapping conditions cause ambiguity errors
  • enable_if_t<condition, T> is the C++14 shorthand — always prefer it over the verbose typename enable_if<...>::type
  • static_assert vs enable_if: static_assert gives a clear custom message but is a hard error; enable_if allows other overloads to match
  • C++20 concepts replace enable_if for new code — shorter, readable in the signature, and produce dramatically better error messages
  • Keep conditions in a named constexpr bool or concept when they get complex — it aids readability and reuse

Frequently Asked Questions (FAQ)

Q. When would I use this in practice?

A. enable_if / enable_if_t for SFINAE-friendly overloads: return types, default template parameters, and migrating toward C…

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++, enable_if, SFINAE, Templates, Metaprogramming.