C++ Type Traits 완벽 가이드 | std::is_integral·std::enable_if

C++ Type Traits 완벽 가이드 | std::is_integral·std::enable_if

이 글의 핵심

C++ type_traits로 컴파일 타임에 타입을 검사하고 분기하는 방법. std::is_integral, std::is_same, std::enable_if, 커스텀 트레이트, SFINAE, 자주 겪는 에러와 프로덕션 패턴까지.

들어가며: 템플릿에 잘못된 타입이 넘어가면 에러 메시지가 난해하다

”정수만 받고 싶은데 vector를 넘기면 50줄 에러가 나요”

템플릿 함수는 어떤 타입이든 받을 수 있어서 유연하지만, 의도하지 않은 타입이 넘어가면 컴파일러는 긴 인스턴스화 스택을 출력합니다. “이 함수는 정수만 받는다”고 컴파일 시점에 검사하고 싶을 때, type traits(타입 트레이트)를 사용합니다. 비유하면 “이 자리에는 정수형만 넣을 수 있다”라고 규칙을 적어 두면, 실수로 std::vector를 넣었을 때 “정수형이 아님”이라고 바로 짚어 주는 것과 같습니다.

문제의 코드:

template <typename T>
T add(T a, T b) {
    return a + b;
}

int main() {
    add(3, 5);                           // OK
    add(std::vector<int>{}, std::vector<int>{});  // 50줄 넘는 에러...
}

위 코드 설명: add는 어떤 타입 T든 받을 수 있어서, std::vector를 넘기면 operator+가 없다는 에러가 나옵니다. 이때 에러 메시지는 템플릿 인스턴스화 스택이 길게 이어져 원인 파악이 어렵습니다.

type traits로 해결:

#include <type_traits>
#include <iostream>

template <typename T>
std::enable_if_t<std::is_integral_v<T>, T> add(T a, T b) {
    return a + b;
}

int main() {
    std::cout << add(3, 5) << "\n";   // 8
    // add(std::vector<int>{}, std::vector<int>{});  // 컴파일 에러: is_integral 불만족
}

위 코드 설명: std::enable_if_t<std::is_integral_v<T>, T>는 “T가 정수형일 때만 이 함수를 사용 가능하게 하고, 반환 타입은 T”라는 의미입니다. std::vector를 넘기면 is_integral_v가 false가 되어 이 오버로드가 제외되고, “no matching function” 에러가 나오지만, 안 되는지가 더 명확해집니다.

추가 문제 시나리오

시나리오 1: 직렬화 라이브러리
JSON으로 직렬화할 때 int는 그대로, std::string은 따옴표로 감싸고, booltrue/false 문자열로 출력해야 합니다. 타입별로 완전히 다른 직렬화 로직이 필요합니다. type traits로 타입을 검사해 분기합니다.

시나리오 2: 포인터 역참조
로깅 함수에서 int는 값 그대로, int*는 역참조해서 출력하고 싶습니다. std::is_pointer_v로 포인터 여부를 검사해 if constexpr로 분기합니다.

시나리오 3: 컨테이너 알고리즘
std::vector[]로 O(1) 접근이 가능하지만, std::list는 불가능합니다. has_random_access 같은 커스텀 트레이트로 컨테이너 종류를 구분해 최적화된 구현을 선택합니다.

시나리오 4: 설정 파싱
설정값을 int, double, bool, std::string으로 파싱할 때, 타입별로 “true”/“yes”/“1” 처리 등 다른 로직이 필요합니다. std::is_same_v, std::is_integral_v로 타입을 구분합니다.

시나리오 5: 메모리 풀
std::is_trivially_destructible_v로 trivial 소멸 가능 타입만 풀에 넣어, 소멸자 호출 없이 메모리만 반환하는 최적화를 적용합니다.

type traits 동작 원리 시각화

flowchart TD
    A[템플릿 인스턴스화] --> B{type trait 검사}
    B -->|true| C[해당 오버로드/특수화 선택]
    B -->|false| D[SFINAE로 제외 또는 다른 분기]
    C --> E[컴파일 성공]
    D --> F{다른 오버로드 있음?}
    F -->|예| C
    F -->|아니오| G[컴파일 에러]

위 다이어그램 설명: type trait이 true면 해당 구현이 선택되고, false면 SFINAE로 제외되거나 if constexpr의 다른 분기가 선택됩니다. 모든 오버로드가 제외되면 컴파일 에러가 발생합니다.

이 글을 읽으면

  • std::is_integral, std::is_same, std::enable_if 등 표준 type traits를 활용할 수 있습니다.
  • 커스텀 type traits를 만들어 멤버 함수·타입 존재 여부를 검사할 수 있습니다.
  • SFINAE와 enable_if로 조건부 오버로드를 구현할 수 있습니다.
  • 자주 겪는 에러와 해결법을 알 수 있습니다.
  • 프로덕션에서 쓰는 패턴을 적용할 수 있습니다.

자주 묻는 질문 (FAQ)

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

A. C++ type_traits로 컴파일 타임에 타입을 검사하고 분기하는 방법. std::is_integral, std::is_same, std::enable_if, 커스텀 트레이트, SFINAE, 자주 겪는 에러와 프… 실무에서는 위 본문의 예제와 선택 가이드를 참고해 적용하면 됩니다.

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

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

Q. 더 깊이 공부하려면?

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

이전 글: C++ constexpr 기초 (#26-1)에서 컴파일 타임 상수를 다뤘습니다.


목차

  1. type traits 개요
  2. 표준 type traits 상세
  3. std::enable_if 완전 가이드
  4. 커스텀 type traits
  5. 완전한 예제 모음
  6. 자주 발생하는 에러와 해결법
  7. 모범 사례와 주의점
  8. 프로덕션 패턴
  9. 구현 체크리스트

1. type traits 개요

type traits란?

type traits컴파일 시점에 타입의 속성을 검사하는 메타프로그래밍 도구입니다. <type_traits> 헤더에 정의되어 있으며, C++11에서 표준화되었습니다. 각 trait은 ::value(bool) 또는 ::type(타입 별칭)을 제공하고, C++17부터는 _v, _t 접미사로 간편하게 접근할 수 있습니다.

#include <type_traits>
#include <iostream>

int main() {
    // ::value (C++11)
    std::cout << std::is_integral<int>::value << "\n";      // 1
    std::cout << std::is_integral<double>::value << "\n";   // 0

    // _v (C++17)
    std::cout << std::is_integral_v<int> << "\n";           // 1
    std::cout << std::is_integral_v<double> << "\n";        // 0

    // ::type (C++11)
    using T1 = std::remove_const_t<const int>;              // int
    static_assert(std::is_same_v<T1, int>);
}

위 코드 설명: std::is_integral은 정수형(int, long, unsigned 등)이면 true, 아니면 false를 반환합니다. std::remove_const_t는 const를 제거한 타입을 반환합니다.

trait 분류

분류예시용도
타입 분류is_integral, is_floating_point, is_pointer타입 종류 검사
타입 비교is_same, is_base_of, is_convertible타입 관계 검사
타입 변환remove_const, remove_reference, decay타입 변형
조건부enable_if, conditional조건에 따른 타입/함수 선택

위 표 설명: trait은 “이 타입이 어떤 종류인지”, “두 타입이 같은지”, “타입을 어떻게 변형할지”, “조건에 따라 무엇을 선택할지”를 컴파일 시점에 결정합니다.


2. 표준 type traits 상세

std::is_integral

정수형(char, short, int, long, long long 및 unsigned 변형)인지 검사합니다. bool도 정수형으로 분류됩니다.

#include <type_traits>

// 정수형
static_assert(std::is_integral_v<int>);
static_assert(std::is_integral_v<unsigned long>);
static_assert(std::is_integral_v<char>);
static_assert(std::is_integral_v<bool>);

// 정수형 아님
static_assert(!std::is_integral_v<double>);
static_assert(!std::is_integral_v<float>);
static_assert(!std::is_integral_v<std::string>);
static_assert(!std::is_integral_v<int*>);

실전 활용: 정수만 받는 함수, 비트 연산이 필요한 함수, 나머지 연산이 유효한 타입만 허용할 때 사용합니다.

template <typename T>
std::enable_if_t<std::is_integral_v<T>, T> mod(T a, T b) {
    return a % b;  // 정수형에서만 유효
}

std::is_same

두 타입이 완전히 동일한지 검사합니다. const, 참조, volatile까지 구분합니다.

#include <type_traits>

static_assert(std::is_same_v<int, int>);
static_assert(!std::is_same_v<int, long>);
static_assert(!std::is_same_v<int, const int>);
static_assert(!std::is_same_v<int, int&>);

// decay 후 비교가 필요할 때
static_assert(std::is_same_v<std::decay_t<int&>, int>);
static_assert(std::is_same_v<std::decay_t<const int>, int>);

실전 활용: 특정 타입만 다르게 처리할 때, 템플릿 인자가 예상 타입인지 검증할 때 사용합니다.

template <typename T>
void process(T value) {
    if constexpr (std::is_same_v<T, std::string>) {
        std::cout << "String: " << value << "\n";
    } else if constexpr (std::is_same_v<T, bool>) {
        std::cout << "Bool: " << (value ? "true" : "false") << "\n";
    } else {
        std::cout << "Other: " << value << "\n";
    }
}

std::is_pointer, std::is_reference

포인터·참조 타입인지 검사합니다.

#include <type_traits>

static_assert(std::is_pointer_v<int*>);
static_assert(std::is_pointer_v<std::vector<int>*>);
static_assert(!std::is_pointer_v<int>);

static_assert(std::is_lvalue_reference_v<int&>);
static_assert(std::is_rvalue_reference_v<int&&>);
static_assert(std::is_reference_v<int&>);
static_assert(!std::is_reference_v<int>);

std::is_arithmetic, std::is_floating_point

산술 타입(정수+실수), 실수 타입만 검사합니다.

#include <type_traits>

static_assert(std::is_arithmetic_v<int>);
static_assert(std::is_arithmetic_v<double>);
static_assert(!std::is_arithmetic_v<std::string>);

static_assert(std::is_floating_point_v<float>);
static_assert(std::is_floating_point_v<double>);
static_assert(!std::is_floating_point_v<int>);

std::remove_reference, std::remove_const, std::decay

타입에서 참조·const·volatile을 제거하거나, 함수 인자로 받을 때의 “원본 타입”을 추출합니다.

#include <type_traits>

// 참조 제거
static_assert(std::is_same_v<std::remove_reference_t<int&>, int>);
static_assert(std::is_same_v<std::remove_reference_t<int&&>, int>);

// const 제거
static_assert(std::is_same_v<std::remove_const_t<const int>, int>);

// decay: T& -> T, T[] -> T*, 함수 -> 함수 포인터
static_assert(std::is_same_v<std::decay_t<int&>, int>);
static_assert(std::is_same_v<std::decay_t<int[5]>, int*>);

// C++20: remove_cvref (const, volatile, 참조 한 번에 제거)
static_assert(std::is_same_v<std::remove_cvref_t<const int&>, int>);

위 코드 설명: std::decay는 함수 인자로 T를 받을 때 “실제로 저장되는 타입”을 얻을 때 유용합니다. 배열은 포인터로, 함수는 함수 포인터로 변환됩니다.

std::is_convertible

한 타입이 다른 타입으로 암시적 변환 가능한지 검사합니다.

#include <type_traits>

static_assert(std::is_convertible_v<int, double>);
static_assert(std::is_convertible_v<double, int>);  // truncation 있음
static_assert(!std::is_convertible_v<double*, int>);
static_assert(std::is_convertible_v<int, std::string>);  // C++11부터

3. std::enable_if 완전 가이드

enable_if 기본 원리

std::enable_if<Condition, T>Condition이 true일 때 T::type으로 제공하고, false일 때 ::type이 없어서 SFINAE(Substitution Failure Is Not An Error)가 발생합니다. 이 오버로드가 제외되고, 다른 오버로드를 찾게 됩니다.

#include <type_traits>

// Condition이 true일 때
using T1 = typename std::enable_if<true, int>::type;   // T1 = int
using T2 = std::enable_if_t<true, int>;               // T2 = int (C++14)

// Condition이 false일 때
// using T3 = std::enable_if_t<false, int>;  // ::type 없음 → 치환 실패

반환 타입에 enable_if 적용

#include <type_traits>
#include <iostream>

template <typename T>
std::enable_if_t<std::is_integral_v<T>, T> add(T a, T b) {
    return a + b;
}

template <typename T>
std::enable_if_t<std::is_floating_point_v<T>, T> add(T a, T b) {
    return a + b;
}

int main() {
    std::cout << add(3, 5) << "\n";      // 8 (정수 오버로드)
    std::cout << add(3.14, 2.0) << "\n"; // 5.14 (실수 오버로드)
    // add("a", "b");  // 컴파일 에러: 두 오버로드 모두 불만족
}

위 코드 설명: 정수형이면 첫 번째, 실수형이면 두 번째 add가 선택됩니다. 문자열을 넘기면 두 오버로드 모두 enable_if 조건이 false가 되어 SFINAE로 제외되고, “no matching function” 에러가 발생합니다.

기본 템플릿 인자에 enable_if 적용

#include <type_traits>

template <typename T,
          typename = std::enable_if_t<std::is_integral_v<T>>>
T mod(T a, T b) {
    return a % b;
}

// 사용
// mod(10, 3);        // OK
// mod(10.0, 3.0);    // 에러: 두 번째 템플릿 인자 치환 실패

주의: 기본 템플릿 인자에 enable_if를 쓰면, 같은 시그니처의 다른 오버로드와 충돌할 수 있습니다. 서로 다른 “더미” 타입을 써서 구분합니다.

template <typename T,
          std::enable_if_t<std::is_integral_v<T>, int> = 0>
T mod_integral(T a, T b) {
    return a % b;
}

template <typename T,
          std::enable_if_t<std::is_floating_point_v<T>, int> = 0>
T mod_floating(T a, T b) {
    return std::fmod(a, b);
}

위 코드 설명: = 0은 기본값으로, enable_ifint를 반환할 때 이 인자가 0으로 채워집니다. 조건이 false면 ::type이 없어 치환 실패가 됩니다.

함수 매개변수에 enable_if 적용

#include <type_traits>

template <typename T>
void process(T value, std::enable_if_t<std::is_pointer_v<T>, int> = 0) {
    std::cout << "Pointer: " << *value << "\n";
}

template <typename T>
void process(T value, std::enable_if_t<!std::is_pointer_v<T>, int> = 0) {
    std::cout << "Value: " << value << "\n";
}

int main() {
    int x = 42;
    process(x);    // Value: 42
    process(&x);   // Pointer: 42
}

enable_if vs if constexpr

방식enable_ifif constexpr
문법반환 타입/인자에 복잡한 표현함수 내부에서 분기
오버로드타입별로 다른 함수 시그니처하나의 함수 템플릿
가독성낮음높음
C++ 버전C++11C++17

권장: C++17 이상에서는 if constexpr가 더 읽기 쉽습니다. 오버로드 해석으로 반환 타입을 다르게 해야 할 때만 enable_if를 사용합니다.

// C++17: if constexpr가 더 깔끔
template <typename T>
void process(T value) {
    if constexpr (std::is_pointer_v<T>) {
        std::cout << *value << "\n";
    } else {
        std::cout << value << "\n";
    }
}

4. 커스텀 type traits

has_member 패턴 (std::void_t)

특정 멤버 함수멤버 타입이 있는지 검사합니다. std::void_t와 SFINAE를 활용합니다.

#include <type_traits>

// T에 size() 멤버 함수가 있는지 검사
template <typename T, typename = void>
struct has_size_member : std::false_type {};

template <typename T>
struct has_size_member<T, std::void_t<decltype(std::declval<T>().size())>>
    : std::true_type {};

template <typename T>
inline constexpr bool has_size_member_v = has_size_member<T>::value;

// 사용
static_assert(has_size_member_v<std::vector<int>>);
static_assert(has_size_member_v<std::string>);
static_assert(!has_size_member_v<int>);

위 코드 설명:

  • std::declval<T>(): T의 인스턴스를 “가상으로” 만들어 .size()를 호출할 수 있는지 확인합니다.
  • decltype(...): 그 표현식의 타입을 추출합니다. 유효하면 치환 성공, 아니면 SFINAE로 제외됩니다.
  • std::void_t<...>: 어떤 타입이든 void로 변환합니다. 조건이 되는 타입만 받아들이는 “필터” 역할을 합니다.

has_member_type 패턴

멤버 타입이 있는지 검사합니다.

#include <type_traits>

template <typename T, typename = void>
struct has_value_type : std::false_type {};

template <typename T>
struct has_value_type<T, std::void_t<typename T::value_type>>
    : std::true_type {};

template <typename T>
inline constexpr bool has_value_type_v = has_value_type<T>::value;

static_assert(has_value_type_v<std::vector<int>>);   // value_type 있음
static_assert(has_value_type_v<std::map<int,int>>);  // value_type 있음
static_assert(!has_value_type_v<int>);

is_container 커스텀 트레이트

value_type, begin(), end()가 있는 타입을 컨테이너로 판단합니다.

#include <type_traits>
#include <vector>
#include <list>
#include <string>

template <typename T, typename = void>
struct is_container : std::false_type {};

template <typename T>
struct is_container<T, std::void_t<
    typename T::value_type,
    decltype(std::declval<T>().begin()),
    decltype(std::declval<T>().end())
>> : std::true_type {};

template <typename T>
inline constexpr bool is_container_v = is_container<T>::value;

static_assert(is_container_v<std::vector<int>>);
static_assert(is_container_v<std::list<double>>);
static_assert(is_container_v<std::string>);
static_assert(!is_container_v<int>);

is_callable 트레이트

주어진 인자로 호출 가능한지 검사합니다.

#include <type_traits>

template <typename F, typename... Args>
struct is_callable {
private:
    template <typename G>
    static auto test(int) -> decltype(
        std::declval<G>()(std::declval<Args>()...),
        std::true_type{}
    );
    template <typename>
    static std::false_type test(...);

public:
    static constexpr bool value = decltype(test<F>(0))::value;
};

template <typename F, typename... Args>
inline constexpr bool is_callable_v = is_callable<F, Args...>::value;

// 사용
static_assert(is_callable_v<int(*)(int), int>);
static_assert(is_callable_v<decltype({ return x; }), int>);
static_assert(!is_callable_v<int, int>);

5. 완전한 예제 모음

예제 1: 타입별 직렬화 (std::is_integral, std::is_same, if constexpr)

#include <iostream>
#include <string>
#include <type_traits>

template <typename T>
std::string serialize(const T& value) {
    if constexpr (std::is_integral_v<T>) {
        return std::to_string(value);
    } else if constexpr (std::is_floating_point_v<T>) {
        return std::to_string(value);
    } else if constexpr (std::is_same_v<T, bool>) {
        return value ? "true" : "false";
    } else if constexpr (std::is_same_v<T, std::string>) {
        return "\"" + value + "\"";
    } else if constexpr (std::is_pointer_v<T>) {
        return value ? std::to_string(reinterpret_cast<uintptr_t>(value)) : "null";
    } else {
        return "[unknown]";
    }
}

int main() {
    std::cout << serialize(42) << "\n";           // 42
    std::cout << serialize(3.14) << "\n";         // 3.140000
    std::cout << serialize(true) << "\n";         // true
    std::cout << serialize(std::string("hi")) << "\n";  // "hi"
    int x = 10;
    std::cout << serialize(&x) << "\n";           // 주소
}

예제 2: enable_if로 정수/실수 오버로드 분리

#include <iostream>
#include <type_traits>
#include <cmath>

template <typename T>
std::enable_if_t<std::is_integral_v<T>, T> safe_divide(T a, T b) {
    if (b == 0) throw std::runtime_error("division by zero");
    return a / b;
}

template <typename T>
std::enable_if_t<std::is_floating_point_v<T>, T> safe_divide(T a, T b) {
    if (std::fpclassify(b) == FP_ZERO) throw std::runtime_error("division by zero");
    return a / b;
}

int main() {
    std::cout << safe_divide(10, 3) << "\n";     // 3
    std::cout << safe_divide(10.0, 3.0) << "\n";  // 3.333...
}

예제 3: has_to_string + if constexpr

#include <iostream>
#include <string>
#include <type_traits>

template <typename T, typename = void>
struct has_to_string : std::false_type {};

template <typename T>
struct has_to_string<T, std::void_t<decltype(std::declval<T>().to_string())>>
    : std::true_type {};

template <typename T>
std::string to_string_impl(const T& value) {
    if constexpr (has_to_string<T>::value) {
        return value.to_string();
    } else if constexpr (std::is_arithmetic_v<T>) {
        return std::to_string(value);
    } else {
        return "[no string representation]";
    }
}

struct Point {
    int x, y;
    std::string to_string() const {
        return "(" + std::to_string(x) + "," + std::to_string(y) + ")";
    }
};

int main() {
    std::cout << to_string_impl(42) << "\n";           // 42
    std::cout << to_string_impl(Point{1, 2}) << "\n";  // (1,2)
}

예제 4: decay, remove_cvref 활용

#include <type_traits>

template <typename T>
void foo(T&& arg) {
    using decayed = std::decay_t<T>;
    using clean = std::remove_cvref_t<T>;
    // decayed: 배열→포인터, 함수→함수포인터, 참조 제거
    // clean: const, volatile, 참조만 제거 (배열/함수는 그대로)
}

// C++20 remove_cvref
static_assert(std::is_same_v<std::remove_cvref_t<const int&>, int>);
static_assert(std::is_same_v<std::remove_cvref_t<volatile int&&>, int>);

6. 자주 발생하는 에러와 해결법

에러 1: SFINAE 조건이 잘못되어 모든 오버로드 제외

증상: no matching function for call 에러. 모든 오버로드가 치환 실패로 제외됨.

원인: enable_if 조건이 너무 엄격하거나, std::void_t 패턴에서 decltype 표현식이 잘못됨.

// ❌ 잘못된 코드: T::foo가 멤버 변수면 OK, 함수면 다름
template <typename T>
struct has_foo<T, std::void_t<decltype(T::foo)>> : std::true_type {};

해결법:

// ✅ 올바른 코드: 호출 가능한지 검사
template <typename T>
struct has_foo<T, std::void_t<decltype(std::declval<T>().foo())>>
    : std::true_type {};

에러 2: enable_if 기본 인자 충돌

증상: template parameter redefinition 또는 오버로드가 구분되지 않음.

원인: 여러 오버로드가 typename = std::enable_if_t<...> 형태로 같은 기본 인자를 사용하면, 컴파일러가 같은 템플릿으로 인식합니다.

// ❌ 잘못된 코드
template <typename T, typename = std::enable_if_t<std::is_integral_v<T>>>
void foo(T);

template <typename T, typename = std::enable_if_t<std::is_floating_point_v<T>>>
void foo(T);  // 재정의로 인식됨!

해결법:

// ✅ 올바른 코드: 서로 다른 더미 타입 사용
template <typename T, std::enable_if_t<std::is_integral_v<T>, int> = 0>
void foo(T);

template <typename T, std::enable_if_t<std::is_floating_point_v<T>, long> = 0>
void foo(T);

에러 3: if constexpr에서 선택되지 않은 분기의 문법 오류

증상: if constexpr의 false 분기에서 문법 오류가 있으면 컴파일 에러.

원인: C++17에서 선택되지 않은 분기도 템플릿이 인스턴스화되는 경우 문법 검사를 합니다. 의존적이지 않은 코드는 항상 검사됩니다.

// ❌ 잘못된 코드: T가 std::string일 때 std::to_string(std::string) 없음
template <typename T>
void print(T x) {
    if constexpr (std::is_integral_v<T>) {
        std::cout << std::to_string(x) << "\n";
    } else {
        std::cout << std::to_string(x) << "\n";  // T=string이면 에러!
    }
}

해결법:

// ✅ 올바른 코드: 각 분기에서 유효한 표현식만 사용
template <typename T>
void print(T x) {
    if constexpr (std::is_integral_v<T>) {
        std::cout << std::to_string(x) << "\n";
    } else if constexpr (std::is_same_v<T, std::string>) {
        std::cout << x << "\n";
    } else {
        std::cout << "[?]\n";
    }
}

에러 4: declval을 odr-use

증상: 링크 에러 또는 정의 필요 에러.

원인: std::declval<T>()선언만 있고 정의가 없습니다. sizeof, decltype 안에서만 사용해야 합니다.

// ❌ 잘못된 코드
auto x = std::declval<int>();  // odr-use → 정의 필요

// ✅ 올바른 코드
using T = decltype(std::declval<int>());  // 선언만 사용

에러 5: cv-qualifier 무시

증상: const T&를 넘겼을 때 trait이 예상과 다르게 동작.

원인: std::is_same_v<T, int>const intint를 다르게 봅니다.

해결법: std::remove_cv_t 또는 std::decay_t로 정규화 후 비교합니다.

template <typename T>
void process(const T& value) {
    using U = std::remove_cvref_t<T>;
    if constexpr (std::is_same_v<U, int>) {
        // ...
    }
}

7. 모범 사례와 주의점

1. C++20에서는 Concepts 우선

C++20 이상에서는 std::integral, std::floating_point 같은 Concepts를 사용하면 가독성과 에러 메시지가 크게 개선됩니다.

// C++20 Concepts
template <std::integral T>
T add(T a, T b) {
    return a + b;
}

위 코드 설명: template <std::integral T> 한 줄로 “정수만 받는다”는 의도가 명확합니다. type traits + enable_if보다 읽기 쉽습니다.

2. value/type 접미사 활용

C++14/17의 _v, _t를 사용해 ::value, ::type 접근을 간소화합니다.

// ❌ 장황함
typename std::enable_if<std::is_integral<T>::value, T>::type

// ✅ 간결함
std::enable_if_t<std::is_integral_v<T>, T>

3. static_assert로 조기 검증

템플릿 함수 시작 부분에서 static_assert로 요구사항을 검사하면, 잘못된 타입 사용 시 바로 명확한 에러 메시지를 줄 수 있습니다.

template <typename T>
T mod(T a, T b) {
    static_assert(std::is_integral_v<T>, "mod() requires integral types");
    return a % b;
}

4. 커스텀 trait 네이밍

has_xxx, is_xxx 형태로 일관되게 네이밍합니다. _v 변수 템플릿도 함께 제공합니다.

template <typename T>
inline constexpr bool has_size_member_v = has_size_member<T>::value;

5. trait 조합 시 괄호 주의

std::is_same_v<A, B> && std::is_integral_v<T>처럼 여러 trait을 조합할 때, enable_if 안에서는 괄호로 명확히 묶습니다.

template <typename T>
std::enable_if_t<(std::is_integral_v<T> && sizeof(T) >= 4), T>
foo(T x) { return x; }

8. 프로덕션 패턴

패턴 1: 태그 디스패치

type trait으로 “태그” 타입을 선택하고, 오버로드로 구현을 분리합니다.

#include <type_traits>
#include <vector>
#include <list>
#include <iostream>

struct vector_tag {};
struct list_tag {};

template <typename T>
struct container_category {
    using type = void;
};

template <typename T, typename A>
struct container_category<std::vector<T, A>> {
    using type = vector_tag;
};

template <typename T, typename A>
struct container_category<std::list<T, A>> {
    using type = list_tag;
};

template <typename C>
void algorithm_impl(const C& c, vector_tag) {
    std::cout << "vector: O(1) random access\n";
}

template <typename C>
void algorithm_impl(const C& c, list_tag) {
    std::cout << "list: sequential access\n";
}

template <typename C>
void algorithm(const C& c) {
    algorithm_impl(c, typename container_category<C>::type{});
}

int main() {
    algorithm(std::vector<int>{1,2,3});
    algorithm(std::list<int>{1,2,3});
}

패턴 2: 직렬화 디스패처

타입별 직렬화를 trait + 특수화로 분리합니다.

#include <iostream>
#include <string>
#include <type_traits>
#include <vector>

template <typename T, typename = void>
struct Serializer {
    static std::string serialize(const T& value) {
        return std::to_string(value);  // 기본: 산술 타입
    }
};

template <>
struct Serializer<bool> {
    static std::string serialize(bool value) {
        return value ? "true" : "false";
    }
};

template <>
struct Serializer<std::string> {
    static std::string serialize(const std::string& value) {
        return "\"" + value + "\"";
    }
};

template <typename T>
struct Serializer<std::vector<T>> {
    static std::string serialize(const std::vector<T>& vec) {
        std::string result = "[";
        for (size_t i = 0; i < vec.size(); ++i) {
            if (i > 0) result += ",";
            result += Serializer<T>::serialize(vec[i]);
        }
        return result + "]";
    }
};

template <typename T>
std::string serialize(const T& value) {
    return Serializer<T>::serialize(value);
}

패턴 3: 안전한 포인터 역참조

#include <iostream>
#include <type_traits>

template <typename T>
void log_value(const T& value) {
    if constexpr (std::is_pointer_v<T>) {
        if (value) {
            std::cout << "*ptr = " << *value << "\n";
        } else {
            std::cout << "null pointer\n";
        }
    } else {
        std::cout << "value = " << value << "\n";
    }
}

int main() {
    int x = 42;
    log_value(x);
    log_value(&x);
}

패턴 4: trivial 타입 최적화

std::is_trivially_copyable_v, std::is_trivially_destructible_v로 메모리 복사·소멸 최적화를 적용합니다.

#include <type_traits>
#include <cstring>

template <typename T>
void copy_range(T* dest, const T* src, size_t n) {
    if constexpr (std::is_trivially_copyable_v<T>) {
        std::memcpy(dest, src, n * sizeof(T));
    } else {
        for (size_t i = 0; i < n; ++i) {
            dest[i] = src[i];
        }
    }
}

9. 구현 체크리스트

type traits 적용 체크리스트

  • #include <type_traits> 포함
  • C++17 이상이면 _v, _t 접미사 사용
  • enable_if 대신 if constexpr 가능한지 검토
  • static_assert로 요구사항 조기 검증
  • 커스텀 trait은 has_xxx, is_xxx 네이밍
  • std::declvalsizeof/decltype 안에서만 사용
  • if constexpr 각 분기에서 유효한 표현식만 사용
  • C++20이면 Concepts 우선 고려

프로덕션 배포 전 확인

  • 의도하지 않은 타입으로 호출 시 에러 메시지 확인
  • const, 참조 타입 처리 검증
  • 컴파일 시간 영향 확인 (과도한 trait 인스턴스화 시)

참고 자료


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

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

  • C++ 템플릿 특수화 완벽 가이드 | 완전·부분 특수화, 문제 시나리오, 프로덕션 패턴
  • C++ constexpr 함수와 변수 | 컴파일 타임에 계산하기 [#26-1]
  • C++20 Concepts | 템플릿 에러 메시지를 읽기 쉽게 만드는 방법

실전 체크리스트

실무에서 이 개념을 적용할 때 확인해야 할 사항입니다.

코드 작성 전

  • 이 기법이 현재 문제를 해결하는 최선의 방법인가?
  • 팀원들이 이 코드를 이해하고 유지보수할 수 있는가?
  • 성능 요구사항을 만족하는가?

코드 작성 중

  • 컴파일러 경고를 모두 해결했는가?
  • 엣지 케이스를 고려했는가?
  • 에러 처리가 적절한가?

코드 리뷰 시

  • 코드의 의도가 명확한가?
  • 테스트 케이스가 충분한가?
  • 문서화가 되어 있는가?

이 체크리스트를 활용하여 실수를 줄이고 코드 품질을 높이세요.


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

C++, type_traits, 타입트레이트, std::is_integral, std::is_same, std::enable_if, SFINAE, 템플릿메타프로그래밍, C++11, C++17 등으로 검색하시면 이 글이 도움이 됩니다.


관련 글

  • C++ 컴파일 타임 프로그래밍 기법 | 런타임 오버헤드 제거와 constexpr·consteval 실전
  • C++ constexpr 함수와 변수 | 컴파일 타임에 계산하기 [#26-1]
  • C++26 리플렉션 기초 | ^^ 연산자·std::meta::info로 타입 정보 조회하기
  • C++ 컴파일 타임 리플렉션 | C++26 Reflection·magic_enum·매크로 직렬화·검증
  • C++20 Ranges | begin/end 반복 탈출하고 ranges 알고리즘 쓰기