Type Traits 임베디드 활용 — SFINAE·is_pod·컴파일 타임 검사
#한 줄 요약
“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 doublestd::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, structstd::is_enum_v<T>std::is_union_v<T>#Composite categories
std::is_arithmetic_v<T> // integral || floating_pointstd::is_fundamental_v<T> // arithmetic || void || nullptr_tstd::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++20std::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 typestd::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); // OKwrite_register<std::string>(...) // ERROR — not trivially copyablewrite_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++20template<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 integerserialize(buf, 1.5f); // floatserialize(buf, MyEnum::Foo); // underlying intserialize(buf, MyPOD{...}); // memcpyserialize(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 만들기
// "포인터인가 + 정수인가" 조합 traittemplate<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 — SFINAEtemplate<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 constexprtemplate<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-06: Templates 기초
- Part 2-08: Static Polymorphism — CRTP
- Part 2-10: Concepts (C++20) — modern 대안
- Part 3-08: No-RTTI 설계 — type info 없이
#다음 글
Part 2-10: Concepts (C++20) — template 제약을 시그니처에 명시합니다. SFINAE보다 훨씬 읽기 좋은 컴파일 타임 type 검증을 다룹니다.
Embedded C++ for Real Systems · 18 of 41
- 1Embedded C++ for Real Systems — 임베디드 모던 C++ 시리즈 소개
- 2임베디드 C++ vs C — 런타임·코드 크기·ABI 관점 비교
- 3임베디드 C++ 컴파일러 플래그 분석 — -fno-rtti·-fno-exceptions·-Os
- 4임베디드 C++ 런타임 요구사항 — libstdc++·newlib·crt0 분석
- 5C++ 코드 크기 분석 — 가상 함수·템플릿·예외 비용 추적
- 6C++ ABI 호환성 — Itanium ABI·name mangling·vtable 레이아웃
- 7C++ 스타트업 코드 분석 — .init_array·전역 생성자 호출 순서
- 8임베디드 C++ 링커 스크립트 — vtable·정적 객체 배치 추적
- 9임베디드 C++ 표준 선택 가이드 — C++11/14/17/20/23 트레이드오프
- 10임베디드 RAII 기초 — 리소스 안전성과 결정적 소멸 보장
- 11임베디드 RAII 실전 패턴 — Lock·Pin·DMA·Power 관리
- 12constexpr 기초와 임베디드 적용 — 컴파일 타임 계산 활용
- 13constexpr 고급 활용 — 룩업 테이블·CRC·해시 컴파일 타임 생성
- 14consteval과 constinit 분석 — C++20 컴파일 타임 강제 메커니즘
- 15임베디드 Templates 기초 — 타입 안전과 코드 재사용 분석
- 16Template 비용 분석 — 코드 폭증·인스턴스화·디버그 정보 측정
- 17CRTP 패턴 분석 — vtable 없는 정적 다형성
- 18Type Traits 임베디드 활용 — SFINAE·is_pod·컴파일 타임 검사
- 19C++20 Concepts 활용 — 템플릿 제약과 가독성 개선
- 20동적 할당 없는 임베디드 C++ — placement new·정적 객체·풀
- 21Custom Allocator 기초 — std::allocator 인터페이스 분석
- 22Pool Allocator 구현 — Fixed-Size Block과 O(1) 보장
- 23std::pmr 임베디드 활용 — Polymorphic Memory Resource 분석
- 24No-Exception C++ 설계 — 코드 크기·결정성 트레이드오프
- 25임베디드 에러 처리 패턴 — Result·errno·optional 비교
- 26std::expected 분석 — C++23 결과 타입과 에러 전파
- 27No-RTTI C++ 설계 — dynamic_cast 제거와 정적 타입 분기
- 28임베디드 스마트 포인터 선택 — unique·shared·custom 비교
- 29임베디드 C++ 소유권 모델 — single·shared·borrow 패턴
- 30Intrusive Containers 분석 — 동적 할당 없는 컨테이너 설계
- 31ETL 라이브러리 분석 — Embedded Template Library의 STL 대체
- 32임베디드 Lock-free 기초 — atomic·memory ordering·CAS
- 33Lock-free Container 구현 — SPSC Queue·Ring Buffer
- 34Type-safe Flags 패턴 — Enum Class·Strong Typedef·Tag
- 35임베디드 State Machine 패턴 — Variant·Visitor·Table-driven 비교
- 36Compile-time FSM 구현 — 템플릿으로 상태 전이 검증
- 37Singleton 대안 패턴 — Service Locator·Static Init·Phantom
- 38MMIO Register 추상화 — 타입 안전한 비트 필드 접근
- 39GPIO 추상화 패턴 — Template·Concept으로 보드 독립성
- 40Peripheral 추상화 — UART·SPI·I2C 공통 인터페이스 설계
- 41임베디드 HAL 설계 패턴 — Static·Dynamic·Hybrid 비교
관련 글
Compile-time FSM 구현 — 템플릿으로 상태 전이 검증
constexpr state machine — 컴파일 타임에 전이 검증, runtime 코드 0.
consteval과 constinit 분석 — C++20 컴파일 타임 강제 메커니즘
C++20의 컴파일 타임 강제 — consteval은 함수 호출을, constinit은 변수 초기화를 컴파일 타임에 강제합니다.
constexpr 고급 활용 — 룩업 테이블·CRC·해시 컴파일 타임 생성
컴파일 타임 sort, search, 문자열 — constexpr 알고리즘의 한계와 가능성.