본문으로 건너뛰기
Embedded C++ for Real Systems · 17/41

Type Traits 임베디드 활용 — SFINAE·is_pod·컴파일 타임 검사

· Hawk · 4분 읽기

#한 줄 요약

“Type Traits = type에 대한 컴파일 타임 query.” — 이 타입이 integral인가, signed인가, trivial인가를 묻고 그에 따라 분기합니다.

#어떤 문제를 푸는가

generic 코드에서는 타입에 따라 다른 동작이 필요합니다.

template<typename T>
void process(T value) {
// T가 integral이면 → bit operation
// T가 floating point면 → epsilon 비교
// T가 pointer면 → null check
}

C에서는 불가능합니다(타입 정보가 없음). C++ Type Traits가 컴파일 타임 type query를 제공합니다.

template<typename T>
void process(T value) {
if constexpr (std::is_integral_v<T>) {
// integral 전용
} else if constexpr (std::is_floating_point_v<T>) {
// float 전용
} else if constexpr (std::is_pointer_v<T>) {
// pointer 전용
}
}

if constexpr과 함께 쓰면 런타임 코드 분기가 사라지고, 해당 타입에 맞는 코드만 컴파일됩니다.

#표준 Type Traits (<type_traits>)

#Primary categories

std::is_void_v<T>
std::is_integral_v<T> // bool, char, int, long ...
std::is_floating_point_v<T> // float, double, long double
std::is_array_v<T>
std::is_pointer_v<T>
std::is_reference_v<T>
std::is_function_v<T>
std::is_class_v<T> // class, struct
std::is_enum_v<T>
std::is_union_v<T>

#Composite categories

std::is_arithmetic_v<T> // integral || floating_point
std::is_fundamental_v<T> // arithmetic || void || nullptr_t
std::is_object_v<T> // 변수가 될 수 있는 타입
std::is_scalar_v<T> // arithmetic || enum || pointer

#Type properties

std::is_const_v<T>
std::is_volatile_v<T>
std::is_signed_v<T>
std::is_unsigned_v<T>
std::is_trivially_copyable_v<T>
std::is_standard_layout_v<T>
std::is_pod_v<T> // deprecated in C++20
std::is_empty_v<T>
std::is_final_v<T>
std::is_abstract_v<T>

#Relationships

std::is_same_v<T, U>
std::is_base_of_v<Base, Derived>
std::is_convertible_v<From, To>

#Type transformations

std::remove_const_t<T>
std::remove_reference_t<T>
std::remove_pointer_t<T>
std::add_const_t<T>
std::decay_t<T> // 함수/배열 → 포인터, ref/cv 제거
std::underlying_type_t<E> // enum의 underlying type
std::make_signed_t<T>
std::make_unsigned_t<T>

_v는 C++17의 variable template, _t는 C++14의 alias template입니다. 짧고 명확해 사용이 편합니다.

#임베디드 — type-safe register access

template<typename T>
void write_register(uintptr_t addr, T value) {
static_assert(std::is_trivially_copyable_v<T>,
"Only trivially copyable types");
static_assert(sizeof(T) <= 4,
"Register write up to 4 bytes only");
*reinterpret_cast<volatile T*>(addr) = value;
}
write_register<uint32_t>(GPIO_BASE + ODR, 0xFF); // OK
write_register<std::string>(...) // ERROR — not trivially copyable
write_register<uint64_t>(...) // ERROR — too large

잘못된 사용을 컴파일 시점에 차단합니다.

#SFINAE — Substitution Failure Is Not An Error

C++의 오래된 idiom입니다. 템플릿 인스턴스화 실패가 컴파일 에러가 아니라 무시 처리되며, 이를 활용해 조건부 오버로드를 구성합니다.

// 정수 전용
template<typename T,
std::enable_if_t<std::is_integral_v<T>, int> = 0>
void process(T value) {
// integral logic
}
// 부동소수 전용
template<typename T,
std::enable_if_t<std::is_floating_point_v<T>, int> = 0>
void process(T value) {
// float logic
}
process(5); // OK — int 버전
process(1.5f); // OK — float 버전
process("str"); // ERROR — 둘 다 안 맞음

std::enable_if는 조건이 true이면 정의를 남기고, false이면 substitution failure로 처리해 해당 오버로드를 무시합니다.

C++17의 if constexpr이 대부분의 SFINAE를 대체합니다. 새 코드에서는 if constexpr을 우선합니다.

// if constexpr — 훨씬 깔끔
template<typename T>
void process(T value) {
if constexpr (std::is_integral_v<T>) {
// integral logic
} else if constexpr (std::is_floating_point_v<T>) {
// float logic
} else {
static_assert(sizeof(T) == 0, "Unsupported type");
}
}

#void_t — SFINAE 검출 idiom

타입에 특정 멤버나 함수가 있는지를 컴파일 타임에 확인합니다.

template<typename, 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>
constexpr bool has_to_string_v = has_to_string<T>::value;
struct Foo { std::string to_string() const { return ""; } };
struct Bar {};
static_assert(has_to_string_v<Foo>);
static_assert(!has_to_string_v<Bar>);
template<typename T>
void log(T value) {
if constexpr (has_to_string_v<T>) {
write_log(value.to_string());
} else {
write_log("(unsupported type)");
}
}

C++20의 concepts가 훨씬 단순한 syntax를 제공합니다.

// C++20
template<typename T>
concept Stringifiable = requires(T t) {
{ t.to_string() } -> std::convertible_to<std::string>;
};
template<typename T>
void log(T value) {
if constexpr (Stringifiable<T>) {
write_log(value.to_string());
} else {
write_log("(unsupported)");
}
}

#임베디드 — Serialization with traits

타입에 따라 다른 직렬화 방법을 골라 씁니다.

template<typename T>
size_t serialize(uint8_t* buf, T value) {
if constexpr (std::is_integral_v<T>) {
// big-endian으로 정수 직렬화
for (int i = sizeof(T) - 1; i >= 0; --i) {
buf[sizeof(T) - 1 - i] = (value >> (i * 8)) & 0xFF;
}
return sizeof(T);
}
else if constexpr (std::is_floating_point_v<T>) {
// IEEE 754 그대로 (host endian)
std::memcpy(buf, &value, sizeof(T));
return sizeof(T);
}
else if constexpr (std::is_enum_v<T>) {
// underlying type으로
using U = std::underlying_type_t<T>;
return serialize(buf, static_cast<U>(value));
}
else if constexpr (std::is_trivially_copyable_v<T>) {
// POD struct
std::memcpy(buf, &value, sizeof(T));
return sizeof(T);
}
else {
static_assert(sizeof(T) == 0, "Unsupported type for serialization");
}
}
uint8_t buf[16];
serialize(buf, uint16_t(0x1234)); // big-endian integer
serialize(buf, 1.5f); // float
serialize(buf, MyEnum::Foo); // underlying int
serialize(buf, MyPOD{...}); // memcpy
serialize(buf, std::string{}); // ERROR — not trivially copyable

한 함수가 모든 타입을 처리하며, 컴파일러는 해당 분기만 생성합니다.

#std::declval — 인스턴스 없이 type query

std::declval<T>()는 T의 인스턴스가 있다고 가정합니다. 실제로 호출하지 않으며 type만 추론합니다.

template<typename T>
using result_type = decltype(std::declval<T>().method());
// T::method()의 반환 타입을 컴파일 타임에 알아냄

decltype과 함께 사용합니다. 호출 시점이 아닌 declare 시점에 유효합니다.

#Custom traits 만들기

// "포인터인가 + 정수인가" 조합 trait
template<typename T>
struct is_pointer_or_integer
: std::bool_constant<std::is_pointer_v<T> || std::is_integral_v<T>> {};
template<typename T>
constexpr bool is_pointer_or_integer_v = is_pointer_or_integer<T>::value;
static_assert(is_pointer_or_integer_v<int>);
static_assert(is_pointer_or_integer_v<int*>);
static_assert(!is_pointer_or_integer_v<float>);

표준 trait들의 조합으로, 도메인 특화 검증에 활용합니다.

#임베디드 — register-safe 타입

template<typename T>
constexpr bool is_register_safe_v =
std::is_trivially_copyable_v<T> &&
!std::is_pointer_v<T> &&
sizeof(T) <= 4 &&
alignof(T) <= 4;
template<typename T>
void write_register(uintptr_t addr, T value) {
static_assert(is_register_safe_v<T>,
"Type unsafe for register access");
*reinterpret_cast<volatile T*>(addr) = value;
}

여러 검증을 하나의 trait로 묶어 새 함수에서 재사용합니다.

#Tag dispatch — SFINAE 대안

함수 오버로드를 tag 객체로 dispatch합니다.

struct integral_tag {};
struct floating_tag {};
template<typename T>
auto get_tag() {
if constexpr (std::is_integral_v<T>) return integral_tag{};
else if constexpr (std::is_floating_point_v<T>) return floating_tag{};
}
void process_impl(int value, integral_tag) {
// integral
}
void process_impl(float value, floating_tag) {
// float
}
template<typename T>
void process(T value) {
process_impl(value, get_tag<T>());
}

if constexpr보다 약간 복잡하지만, 오버로드가 명확해 문서로서의 가치가 있습니다. 일반적으로는 if constexpr을 권장합니다.

#자주 보는 함정과 안티패턴

#1. 런타임 if로 type check

template<typename T>
void f(T x) {
if (typeid(T) == typeid(int)) { /* */ } // runtime, RTTI 필요
}

if constexpr (std::is_same_v<T, int>)로 컴파일 타임에 처리합니다.

#2. trait를 잘못 작성

template<typename T>
struct has_foo {
static constexpr bool value = /* */ ;
};

조건 누락이나 false negative가 생길 수 있으므로, static_assert로 단위 테스트를 작성해 검증합니다.

#3. void_t 패턴 오용

SFINAE 검출이 컴파일러마다 다른 결과를 낼 수 있습니다. 표준 trait나 concept을 우선합니다.

#4. type_traits 미include

template<typename T>
void f() { static_assert(std::is_integral_v<T>); }
// ERROR — <type_traits> 안 include

#5. if constexpr 외 분기에 invalid code

template<typename T>
void f(T x) {
if constexpr (std::is_integral_v<T>) {
// OK
} else {
x.bar(); // T가 integral이면 — 분기 안 들어가지만 검사
}
}

if constexpr의 false branch는 인스턴스화되지 않으므로 의미상 잘못된 코드도 통과할 수 있지만, syntax 자체는 valid해야 합니다.

#6. 사용하지 않는 trait 헤더 include

<type_traits>는 작지만 컴파일 시간을 추가합니다. 쓰지 않으면 제거합니다.

#측정 — type traits의 코드 크기

같은 함수를 virtual, SFINAE, if constexpr로 비교합니다.

// V1 — virtual (런타임 분기)
class Processor {
virtual void process(int) = 0;
virtual void process(float) = 0;
};
// V2 — SFINAE
template<typename T, std::enable_if_t<std::is_integral_v<T>, int> = 0>
void process(T);
template<typename T, std::enable_if_t<std::is_floating_point_v<T>, int> = 0>
void process(T);
// V3 — if constexpr
template<typename T>
void process(T x) {
if constexpr (std::is_integral_v<T>) { /* */ }
else if constexpr (std::is_floating_point_v<T>) { /* */ }
}

5개 type별 호출 기준의 코드 크기입니다(STM32F4).

V1 (virtual): 460 B (vtable + 가상 호출)
V2 (SFINAE): 320 B (per-type 인스턴스)
V3 (if constexpr): 280 B (인라인 분기 제거)

if constexpr이 가장 작고 빠르며, modern C++에서 권장됩니다.

#정리

  • Type Traits는 컴파일 타임 type query이며 <type_traits> 표준 헤더가 제공합니다.
  • 주요 trait는 is_integral, is_pointer, is_trivially_copyable, is_same, decay 등이 있습니다.
  • SFINAE와 enable_if로 조건부 오버로드를 만들며, C++17의 if constexpr이 이를 대체합니다.
  • 임베디드에서는 type-safe register access, serialization, custom domain traits에 활용합니다.
  • C++20 concepts가 훨씬 깔끔한 syntax이므로 가능하면 concept을 권장합니다.

#관련 항목

#다음 글

Part 2-10: Concepts (C++20) — template 제약을 시그니처에 명시합니다. SFINAE보다 훨씬 읽기 좋은 컴파일 타임 type 검증을 다룹니다.

Embedded C++ for Real Systems · 18 of 41

  1. 1Embedded C++ for Real Systems — 임베디드 모던 C++ 시리즈 소개
  2. 2임베디드 C++ vs C — 런타임·코드 크기·ABI 관점 비교
  3. 3임베디드 C++ 컴파일러 플래그 분석 — -fno-rtti·-fno-exceptions·-Os
  4. 4임베디드 C++ 런타임 요구사항 — libstdc++·newlib·crt0 분석
  5. 5C++ 코드 크기 분석 — 가상 함수·템플릿·예외 비용 추적
  6. 6C++ ABI 호환성 — Itanium ABI·name mangling·vtable 레이아웃
  7. 7C++ 스타트업 코드 분석 — .init_array·전역 생성자 호출 순서
  8. 8임베디드 C++ 링커 스크립트 — vtable·정적 객체 배치 추적
  9. 9임베디드 C++ 표준 선택 가이드 — C++11/14/17/20/23 트레이드오프
  10. 10임베디드 RAII 기초 — 리소스 안전성과 결정적 소멸 보장
  11. 11임베디드 RAII 실전 패턴 — Lock·Pin·DMA·Power 관리
  12. 12constexpr 기초와 임베디드 적용 — 컴파일 타임 계산 활용
  13. 13constexpr 고급 활용 — 룩업 테이블·CRC·해시 컴파일 타임 생성
  14. 14consteval과 constinit 분석 — C++20 컴파일 타임 강제 메커니즘
  15. 15임베디드 Templates 기초 — 타입 안전과 코드 재사용 분석
  16. 16Template 비용 분석 — 코드 폭증·인스턴스화·디버그 정보 측정
  17. 17CRTP 패턴 분석 — vtable 없는 정적 다형성
  18. 18Type Traits 임베디드 활용 — SFINAE·is_pod·컴파일 타임 검사
  19. 19C++20 Concepts 활용 — 템플릿 제약과 가독성 개선
  20. 20동적 할당 없는 임베디드 C++ — placement new·정적 객체·풀
  21. 21Custom Allocator 기초 — std::allocator 인터페이스 분석
  22. 22Pool Allocator 구현 — Fixed-Size Block과 O(1) 보장
  23. 23std::pmr 임베디드 활용 — Polymorphic Memory Resource 분석
  24. 24No-Exception C++ 설계 — 코드 크기·결정성 트레이드오프
  25. 25임베디드 에러 처리 패턴 — Result·errno·optional 비교
  26. 26std::expected 분석 — C++23 결과 타입과 에러 전파
  27. 27No-RTTI C++ 설계 — dynamic_cast 제거와 정적 타입 분기
  28. 28임베디드 스마트 포인터 선택 — unique·shared·custom 비교
  29. 29임베디드 C++ 소유권 모델 — single·shared·borrow 패턴
  30. 30Intrusive Containers 분석 — 동적 할당 없는 컨테이너 설계
  31. 31ETL 라이브러리 분석 — Embedded Template Library의 STL 대체
  32. 32임베디드 Lock-free 기초 — atomic·memory ordering·CAS
  33. 33Lock-free Container 구현 — SPSC Queue·Ring Buffer
  34. 34Type-safe Flags 패턴 — Enum Class·Strong Typedef·Tag
  35. 35임베디드 State Machine 패턴 — Variant·Visitor·Table-driven 비교
  36. 36Compile-time FSM 구현 — 템플릿으로 상태 전이 검증
  37. 37Singleton 대안 패턴 — Service Locator·Static Init·Phantom
  38. 38MMIO Register 추상화 — 타입 안전한 비트 필드 접근
  39. 39GPIO 추상화 패턴 — Template·Concept으로 보드 독립성
  40. 40Peripheral 추상화 — UART·SPI·I2C 공통 인터페이스 설계
  41. 41임베디드 HAL 설계 패턴 — Static·Dynamic·Hybrid 비교