본문으로 건너뛰기
Previous
Next
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

자주 묻는 질문 (FAQ)

Q. 이 내용을 실무에서 언제 쓰나요?

A. enable_if / enable_if_t for SFINAE-friendly overloads: return types, default template parameters, and migrating toward C… 실무에서는 위 본문의 예제와 선택 가이드를 참고해 적용하면 됩니다.

Q. 선행으로 읽으면 좋은 글은?

A. 각 글 하단의 이전 글 또는 관련 글 링크를 따라가면 순서대로 배울 수 있습니다. C++ 시리즈 목차에서 전체 흐름을 확인할 수 있습니다.

Q. 더 깊이 공부하려면?

A. cppreference와 해당 라이브러리 공식 문서를 참고하세요. 글 말미의 참고 자료 링크도 활용하면 좋습니다.


같이 보면 좋은 글 (내부 링크)

이 주제와 연결되는 다른 글입니다.

  • [C++ Concepts and Constraints | Type Requirements in C++20](/en/blog/cpp-concepts-constraints/
  • [C++20 Modules](/en/blog/cpp-series-24-1-modules-basics/
  • [C++ Expression Templates](/en/blog/cpp-expression-template/

이 글에서 다루는 키워드 (관련 검색어)

C++, enable_if, SFINAE, Templates, Metaprogramming 등으로 검색하시면 이 글이 도움이 됩니다.