Type Traits & SFINAE — enable_if·if constexpr·태그 디스패치
개요 — Type Traits와 SFINAE
Section titled “개요 — Type Traits와 SFINAE”Type Traits는 컴파일 타임에 타입의 속성을 질의하는 메타함수의 집합입니다 (<type_traits> 헤더). 예를 들어 std::is_integral<T>::value는 T가 정수 타입인지를 컴파일 타임 bool로 반환합니다.
**SFINAE(Substitution Failure Is Not An Error)**는 템플릿 파라미터 대입 실패가 컴파일 오류가 아닌 후보 제거(overload removal)로 처리되는 규칙입니다. 이를 이용해 타입 조건에 따른 함수 오버로드 선택을 구현합니다.
1. Type Traits 카탈로그
Section titled “1. Type Traits 카탈로그”1.1 타입 분류 (Primary type categories)
Section titled “1.1 타입 분류 (Primary type categories)”#include <type_traits>
// 정수·실수 분류static_assert(std::is_integral_v<int>); // truestatic_assert(std::is_integral_v<bool>); // truestatic_assert(std::is_floating_point_v<double>); // truestatic_assert(std::is_arithmetic_v<float>); // true (integral || floating_point)
// 포인터·참조 분류static_assert(std::is_pointer_v<int*>); // truestatic_assert(std::is_reference_v<int&>); // truestatic_assert(std::is_lvalue_reference_v<int&>); // truestatic_assert(std::is_rvalue_reference_v<int&&>); // true
// 클래스·열거형static_assert(std::is_class_v<std::string>); // truestatic_assert(std::is_enum_v<std::byte>); // truestatic_assert(std::is_function_v<int(int)>); // true
// voidstatic_assert(std::is_void_v<void>); // true1.2 타입 속성 (Type properties)
Section titled “1.2 타입 속성 (Type properties)”struct Foo { Foo() {} ~Foo() {} };struct Bar { Bar() = default; ~Bar() = default; };
// 생성/소멸 특성static_assert(std::is_default_constructible_v<Bar>);static_assert(std::is_copy_constructible_v<std::vector<int>>);static_assert(std::is_move_constructible_v<std::unique_ptr<int>>);static_assert(!std::is_copy_constructible_v<std::unique_ptr<int>>);
// trivial 특성 (memcpy 안전 여부)static_assert(std::is_trivially_copyable_v<int>);static_assert(!std::is_trivially_copyable_v<std::string>);
// const/volatilestatic_assert(std::is_const_v<const int>);static_assert(std::is_volatile_v<volatile int>);1.3 타입 관계 (Type relationships)
Section titled “1.3 타입 관계 (Type relationships)”struct Base {};struct Derived : Base {};
static_assert(std::is_same_v<int, int>);static_assert(!std::is_same_v<int, long>);static_assert(std::is_base_of_v<Base, Derived>);static_assert(std::is_convertible_v<int, double>); // 암시적 변환 가능1.4 타입 변환 (Type transformations)
Section titled “1.4 타입 변환 (Type transformations)”// 수정자 제거using NoRef = std::remove_reference_t<int&>; // intusing NoCV = std::remove_cv_t<const volatile int>; // intusing NoPtr = std::remove_pointer_t<int*>; // intusing NoCRef = std::remove_cvref_t<const int&>; // int (C++20)
// 수정자 추가using AddConst = std::add_const_t<int>; // const intusing AddPtr = std::add_pointer_t<int>; // int*using AddLRef = std::add_lvalue_reference_t<int>; // int&
// 유용한 변환using Decay = std::decay_t<int[5]>; // int* (배열→포인터)using Decay2 = std::decay_t<int(int)>; // int(*)(int) (함수→포인터)using Common = std::common_type_t<int, double>; // double2. std::enable_if — SFINAE 기반 오버로드
Section titled “2. std::enable_if — SFINAE 기반 오버로드”// 정수 타입에만 활성화template<typename T>std::enable_if_t<std::is_integral_v<T>, T>Double(T value){ return value * 2;}
// 부동소수점 타입에만 활성화template<typename T>std::enable_if_t<std::is_floating_point_v<T>, T>Double(T value){ return value * 2.0;}
auto a = Double(5); // int 버전auto b = Double(3.14); // double 버전// Double("hello"); // 오류 — 어떤 버전도 매칭 안 됨enable_if 위치별 문법
Section titled “enable_if 위치별 문법”// 위치 1: 반환 타입template<typename T>std::enable_if_t<std::is_integral_v<T>, void>Process1(T v) { std::cout << "정수\n"; }
// 위치 2: 템플릿 파라미터 기본값 (더 유연)template<typename T, std::enable_if_t<std::is_integral_v<T>, int> = 0>void Process2(T v) { std::cout << "정수\n"; }
// 위치 3: 함수 파라미터 (권장하지 않음)template<typename T>void Process3(T v, std::enable_if_t<std::is_integral_v<T>>* = nullptr){ std::cout << "정수\n"; }3. 태그 디스패치 (Tag Dispatch)
Section titled “3. 태그 디스패치 (Tag Dispatch)”SFINAE의 대안으로, 타입 태그를 오버로드 선택자로 사용하는 패턴입니다. 오류 메시지가 SFINAE보다 명확하고 코드 구조가 직관적입니다.
// 태그 타입 정의struct IntegralTag {};struct FloatingPointTag {};struct OtherTag {};
// 태그 선택 메타함수template<typename T>using SelectTag = std::conditional_t< std::is_integral_v<T>, IntegralTag, std::conditional_t<std::is_floating_point_v<T>, FloatingPointTag, OtherTag>>;
// 태그로 구분된 구현 함수template<typename T>void ProcessImpl(T value, IntegralTag){ std::cout << "정수 처리: " << value << " (비트: " << sizeof(T)*8 << ")\n";}
template<typename T>void ProcessImpl(T value, FloatingPointTag){ std::cout << "실수 처리: " << std::fixed << value << "\n";}
template<typename T>void ProcessImpl(T value, OtherTag){ std::cout << "기타 타입 처리\n";}
// 공개 인터페이스 — 태그를 자동으로 선택template<typename T>void Process(T value){ ProcessImpl(value, SelectTag<T>{});}
Process(42); // 정수 처리Process(3.14); // 실수 처리Process("hi"); // 기타 타입 처리STL에서 태그 디스패치 활용
Section titled “STL에서 태그 디스패치 활용”// std::advance — 반복자 카테고리에 따라 다른 구현 선택template<typename Iter>void AdvanceImpl(Iter& it, ptrdiff_t n, std::random_access_iterator_tag){ it += n; // O(1) — 랜덤 접근 반복자}
template<typename Iter>void AdvanceImpl(Iter& it, ptrdiff_t n, std::bidirectional_iterator_tag){ if (n > 0) while (n--) ++it; // O(n) else while (n++) --it;}
template<typename Iter>void AdvanceImpl(Iter& it, ptrdiff_t n, std::input_iterator_tag){ while (n--) ++it; // O(n), 정방향만}
template<typename Iter>void Advance(Iter& it, ptrdiff_t n){ AdvanceImpl(it, n, typename std::iterator_traits<Iter>::iterator_category{});}4. if constexpr — 현대적 대안 (C++17)
Section titled “4. if constexpr — 현대적 대안 (C++17)”C++17부터는 if constexpr이 태그 디스패치와 enable_if를 대부분 대체합니다.
template<typename T>void Process(T value){ if constexpr (std::is_integral_v<T>) { std::cout << "정수: " << value << " (비트: " << sizeof(T)*8 << ")\n"; } else if constexpr (std::is_floating_point_v<T>) { std::cout << "실수: " << std::fixed << value << "\n"; } else { std::cout << "기타\n"; }}세 가지 방법 비교
Section titled “세 가지 방법 비교”| 기법 | C++ 버전 | 가독성 | 오류 메시지 | 함수 분리 |
|---|---|---|---|---|
| enable_if | C++11 | 낮음 | 장황함 | 분리됨 |
| 태그 디스패치 | C++11 | 중간 | 명확 | 분리됨 |
| if constexpr | C++17 | 높음 | 명확 | 단일 함수 |
| Concepts | C++20 | 최고 | 최명확 | 선택 가능 |
5. 커스텀 Type Trait 작성
Section titled “5. 커스텀 Type Trait 작성”// 기본값: falsetemplate<typename T>struct HasToString : std::false_type {};
// T.ToString()이 있으면 truetemplate<typename T>struct HasToString<T, std::void_t<decltype(std::declval<T>().ToString())>> : std::true_type {};
template<typename T>inline constexpr bool HasToString_v = HasToString<T>::value;
// std::void_t 활용 — 표현식 유효성 검사template<typename, typename = void>struct IsIterable : std::false_type {};
template<typename T>struct IsIterable<T, std::void_t< decltype(std::begin(std::declval<T&>())), decltype(std::end(std::declval<T&>()))>> : std::true_type {};
template<typename T>inline constexpr bool IsIterable_v = IsIterable<T>::value;
static_assert(IsIterable_v<std::vector<int>>); // truestatic_assert(!IsIterable_v<int>); // false
// 사용template<typename T>void Print(const T& value){ if constexpr (HasToString_v<T>) std::cout << value.ToString() << "\n"; else if constexpr (IsIterable_v<T>) for (const auto& item : value) std::cout << item << " "; else std::cout << value << "\n";}6. std::declval 활용
Section titled “6. std::declval 활용”std::declval<T>()는 T의 인스턴스 없이도 T의 멤버 함수 반환 타입을 추론하는 데 사용됩니다. 평가되지 않는 문맥(unevaluated context)에서만 사용 가능합니다.
struct Foo { int GetValue() const;};
// Foo 생성자 없어도 멤버 함수 반환 타입 추론 가능using ReturnType = decltype(std::declval<Foo>().GetValue()); // int
// 활용: 연산 결과 타입 추론template<typename T, typename U>using AddResult = decltype(std::declval<T>() + std::declval<U>());
AddResult<int, double> r; // double7. 실전 패턴 — 범용 직렬화
Section titled “7. 실전 패턴 — 범용 직렬화”// 직렬화 가능 타입인지 확인하는 Traittemplate<typename T, typename = void>struct IsSerializable : std::false_type {};
template<typename T>struct IsSerializable<T, std::void_t< decltype(std::declval<T>().Serialize(std::declval<std::ostream&>()))>> : std::true_type {};
// 직렬화 함수 — 지원/미지원 타입 분기template<typename T>void Save(const T& value, std::ostream& os){ if constexpr (IsSerializable<T>::value) { value.Serialize(os); } else if constexpr (std::is_arithmetic_v<T>) { os.write(reinterpret_cast<const char*>(&value), sizeof(T)); } else if constexpr (std::is_same_v<T, std::string>) { size_t len = value.size(); os.write(reinterpret_cast<const char*>(&len), sizeof(len)); os.write(value.data(), len); } else { static_assert(sizeof(T) == 0, "직렬화 미지원 타입"); }}| 도구 | 주요 역할 |
|---|---|
<type_traits> | 컴파일 타임 타입 속성 질의 |
std::enable_if | SFINAE 기반 오버로드 활성화/비활성화 |
| 태그 디스패치 | 타입 태그로 구현 함수 분기 |
if constexpr | 단일 함수 내 컴파일 타임 조건 분기 (C++17 권장) |
std::void_t | 표현식 유효성 기반 커스텀 Trait 작성 |
std::declval | 인스턴스 없이 멤버 접근 타입 추론 |
선택 기준:
- C++17 이상:
if constexpr우선 - C++11/14: 태그 디스패치 >
enable_if(가독성 이유) - C++20 이상: Concepts로 가장 명확하게 표현