C++ Type Traits & SFINAE
개요 — Type Traits와 SFINAE
섹션 제목: “개요 — 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 카탈로그
섹션 제목: “1. Type Traits 카탈로그”1.1 타입 분류 (Primary type categories)
섹션 제목: “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)
섹션 제목: “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)
섹션 제목: “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)
섹션 제목: “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 기반 오버로드
섹션 제목: “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 위치별 문법
섹션 제목: “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)
섹션 제목: “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에서 태그 디스패치 활용
섹션 제목: “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)
섹션 제목: “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"; }}세 가지 방법 비교
섹션 제목: “세 가지 방법 비교”| 기법 | C++ 버전 | 가독성 | 오류 메시지 | 함수 분리 |
|---|---|---|---|---|
| enable_if | C++11 | 낮음 | 장황함 | 분리됨 |
| 태그 디스패치 | C++11 | 중간 | 명확 | 분리됨 |
| if constexpr | C++17 | 높음 | 명확 | 단일 함수 |
| Concepts | C++20 | 최고 | 최명확 | 선택 가능 |
5. 커스텀 Type Trait 작성
섹션 제목: “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 활용
섹션 제목: “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. 실전 패턴 — 범용 직렬화
섹션 제목: “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, "직렬화 미지원 타입"); }}8. 정리
섹션 제목: “8. 정리”| 도구 | 주요 역할 |
|---|---|
<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로 가장 명확하게 표현
9. C++20 표준 Type Traits 신규 추가
섹션 제목: “9. C++20 표준 Type Traits 신규 추가”#include <type_traits>
// C++20 신규 type traits// 1. std::remove_cvref_t — const/volatile/reference 한꺼번에 제거using T1 = std::remove_cvref_t<const int&>; // intusing T2 = std::remove_cvref_t<volatile int&&>; // int
// 2. std::type_identity_t — 타입을 그대로 전달 (추론 억제에 유용)template<typename T>void func(T a, std::type_identity_t<T> b){ // b의 타입은 a에서 추론된 T로 고정 (b 자체로 추론되지 않음)}func(1, 2); // T=int, OK// func(1, 2.0); // T=int, b는 int로 고정 — 암시적 변환 발생
// 3. std::is_bounded_array_v / std::is_unbounded_array_vstatic_assert(std::is_bounded_array_v<int[5]>); // truestatic_assert(std::is_unbounded_array_v<int[]>); // truestatic_assert(!std::is_bounded_array_v<int>); // false
// 4. std::is_nothrow_convertible_v (C++20)static_assert(std::is_nothrow_convertible_v<int, double>); // true
// 5. std::is_layout_compatible_v (C++20) — 레이아웃 호환성struct A { int x; int y; };struct B { int a; int b; };static_assert(std::is_layout_compatible_v<A, B>); // true (같은 레이아웃)10. 감지 이디엄 (Detection Idiom)
섹션 제목: “10. 감지 이디엄 (Detection Idiom)”std::void_t를 활용해 멤버·함수 존재 여부를 체크하는 고급 패턴입니다.
#include <type_traits>
// 기본 감지 헬퍼template<typename, template<typename> class, typename = void>struct is_detected : std::false_type {};
template<typename T, template<typename> class Op>struct is_detected<T, Op, std::void_t<Op<T>>> : std::true_type {};
template<typename T, template<typename> class Op>inline constexpr bool is_detected_v = is_detected<T, Op>::value;
// 사용할 연산 정의template<typename T> using has_begin_t = decltype(std::declval<T>().begin());template<typename T> using has_size_t = decltype(std::declval<T>().size());template<typename T> using has_reserve_t = decltype(std::declval<T>().reserve(0));template<typename T> using has_push_back_t = decltype(std::declval<T>().push_back(std::declval<typename T::value_type>()));
// 타입 속성 확인static_assert(is_detected_v<std::vector<int>, has_begin_t>); // truestatic_assert(is_detected_v<std::vector<int>, has_reserve_t>); // truestatic_assert(!is_detected_v<std::list<int>, has_reserve_t>); // false
// 활용: 컨테이너 타입에 따른 최적화template<typename Container, typename T>void smart_push(Container& c, T&& value){ if constexpr (is_detected_v<Container, has_reserve_t>) { // reserve 가능한 컨테이너 (vector, string 등) if (c.size() == c.capacity()) c.reserve(c.capacity() * 2 + 1); } c.push_back(std::forward<T>(value));}11. std::conditional — 컴파일 타임 타입 선택
섹션 제목: “11. std::conditional — 컴파일 타임 타입 선택”#include <type_traits>
// 조건에 따라 타입 선택template<bool UseFloat>using NumberType = std::conditional_t<UseFloat, float, int>;
NumberType<true> f_val = 3.14f; // floatNumberType<false> i_val = 42; // int
// 중첩 conditional — 다중 조건template<typename T>using BestContainer = std::conditional_t< std::is_trivially_copyable_v<T>, std::conditional_t<(sizeof(T) <= 8), std::array<T, 8>, std::vector<T>>, std::list<T> // 복잡한 타입은 list>;
// 64비트 / 32비트 환경 분기using PlatformInt = std::conditional_t<sizeof(void*) == 8, int64_t, int32_t>;
// 활용: 스토리지 최적화template<typename T>class OptionalStorage{ // T가 8바이트 이하면 인라인 저장, 아니면 힙 저장 using Storage = std::conditional_t< sizeof(T) <= 8 && std::is_trivially_copyable_v<T>, T, std::unique_ptr<T> >; Storage storage_;};| 도구 | 주요 역할 |
|---|---|
<type_traits> | 컴파일 타임 타입 속성 질의 |
std::enable_if | SFINAE 기반 오버로드 활성화/비활성화 |
| 태그 디스패치 | 타입 태그로 구현 함수 분기 |
if constexpr | 단일 함수 내 컴파일 타임 조건 분기 (C++17 권장) |
std::void_t | 표현식 유효성 기반 커스텀 Trait 작성 |
std::declval | 인스턴스 없이 멤버 접근 타입 추론 |
std::conditional_t | 조건에 따른 컴파일 타임 타입 선택 |
| 감지 이디엄 | 멤버/함수 존재 여부 체크 |
선택 기준:
- C++17 이상:
if constexpr우선 - C++11/14: 태그 디스패치 >
enable_if(가독성 이유) - C++20 이상: Concepts로 가장 명확하게 표현
- 멤버 존재 여부 체크: 감지 이디엄 또는
requires표현식(C++20)