콘텐츠로 이동

C++ Type Traits & SFINAE

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)

섹션 제목: “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 기반 오버로드

섹션 제목: “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)

섹션 제목: “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로 가장 명확하게 표현
#include <type_traits>
// C++20 신규 type traits
// 1. std::remove_cvref_t — const/volatile/reference 한꺼번에 제거
using T1 = std::remove_cvref_t<const int&>; // int
using 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_v
static_assert(std::is_bounded_array_v<int[5]>); // true
static_assert(std::is_unbounded_array_v<int[]>); // true
static_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 (같은 레이아웃)

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>); // true
static_assert(is_detected_v<std::vector<int>, has_reserve_t>); // true
static_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; // float
NumberType<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_ifSFINAE 기반 오버로드 활성화/비활성화
태그 디스패치타입 태그로 구현 함수 분기
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)