Skip to content

Type Traits & SFINAE — enable_if·if constexpr·태그 디스패치

Type Traits는 컴파일 타임에 타입의 속성을 질의하는 메타함수의 집합입니다 (<type_traits> 헤더). 예를 들어 std::is_integral<T>::value는 T가 정수 타입인지를 컴파일 타임 bool로 반환합니다.

**SFINAE(Substitution Failure Is Not An Error)**는 템플릿 파라미터 대입 실패가 컴파일 오류가 아닌 후보 제거(overload removal)로 처리되는 규칙입니다. 이를 이용해 타입 조건에 따른 함수 오버로드 선택을 구현합니다.


1.1 타입 분류 (Primary type categories)

Section titled “1.1 타입 분류 (Primary type categories)”
#include <type_traits>
// 정수·실수 분류
static_assert(std::is_integral_v<int>); // true
static_assert(std::is_integral_v<bool>); // true
static_assert(std::is_floating_point_v<double>); // true
static_assert(std::is_arithmetic_v<float>); // true (integral || floating_point)
// 포인터·참조 분류
static_assert(std::is_pointer_v<int*>); // true
static_assert(std::is_reference_v<int&>); // true
static_assert(std::is_lvalue_reference_v<int&>); // true
static_assert(std::is_rvalue_reference_v<int&&>); // true
// 클래스·열거형
static_assert(std::is_class_v<std::string>); // true
static_assert(std::is_enum_v<std::byte>); // true
static_assert(std::is_function_v<int(int)>); // true
// void
static_assert(std::is_void_v<void>); // true
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/volatile
static_assert(std::is_const_v<const int>);
static_assert(std::is_volatile_v<volatile int>);
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>); // 암시적 변환 가능
// 수정자 제거
using NoRef = std::remove_reference_t<int&>; // int
using NoCV = std::remove_cv_t<const volatile int>; // int
using NoPtr = std::remove_pointer_t<int*>; // int
using NoCRef = std::remove_cvref_t<const int&>; // int (C++20)
// 수정자 추가
using AddConst = std::add_const_t<int>; // const int
using 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>; // double

2. 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"); // 오류 — 어떤 버전도 매칭 안 됨
// 위치 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"; }

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"); // 기타 타입 처리
// 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";
}
}
기법C++ 버전가독성오류 메시지함수 분리
enable_ifC++11낮음장황함분리됨
태그 디스패치C++11중간명확분리됨
if constexprC++17높음명확단일 함수
ConceptsC++20최고최명확선택 가능

// 기본값: false
template<typename T>
struct HasToString : std::false_type {};
// T.ToString()이 있으면 true
template<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>>); // true
static_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";
}

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; // double

// 직렬화 가능 타입인지 확인하는 Trait
template<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_ifSFINAE 기반 오버로드 활성화/비활성화
태그 디스패치타입 태그로 구현 함수 분기
if constexpr단일 함수 내 컴파일 타임 조건 분기 (C++17 권장)
std::void_t표현식 유효성 기반 커스텀 Trait 작성
std::declval인스턴스 없이 멤버 접근 타입 추론

선택 기준:

  • C++17 이상: if constexpr 우선
  • C++11/14: 태그 디스패치 > enable_if (가독성 이유)
  • C++20 이상: Concepts로 가장 명확하게 표현