임베디드 Templates 기초 — 타입 안전과 코드 재사용 분석
#한 줄 요약
“Template은 컴파일 타임 다형성.” — 코드 한 번 작성으로 여러 타입에 적용하면서 런타임 비용을 0으로 유지합니다.
#어떤 문제를 푸는가
같은 알고리즘을 여러 타입에 적용하고 싶을 때 세 가지 선택지가 있습니다.
- 매크로 — 타입 안전이 없고 디버깅이 어렵습니다.
void*+ 함수 포인터 — 타입 캐스팅과 간접 호출 비용이 듭니다.- C++ 템플릿 — 타입 안전하며 컴파일 타임에 처리되어 비용이 0입니다.
// 매크로 — 위험#define MAX(a, b) ((a) > (b) ? (a) : (b))int x = MAX(1, 2);float y = MAX(1.5f, 2.5f);// MAX(i++, j++) — i와 j가 두 번 증가 (silent bug)// void* — 런타임 costint (*compare)(const void*, const void*);void qsort(void* base, size_t n, size_t size, int (*cmp)(const void*, const void*));// 매 호출이 간접 호출 + 비교 함수 호출 비용// 템플릿 — 안전 + 빠름template<typename T>T max(T a, T b) { return (a > b) ? a : b;}
int x = max(1, 2); // T = intfloat y = max(1.5f, 2.5f); // T = floatC++ 템플릿은 컴파일러가 타입별 코드를 생성합니다. 각 인스턴스가 전용 함수가 되며 간접 호출은 없습니다.
#함수 템플릿
가장 단순한 형태입니다.
template<typename T>T add(T a, T b) { return a + b;}
int a = add(1, 2); // T deduced as intfloat b = add(1.5f, 2.5f); // T deduced as floatdouble c = add<double>(1.0, 2); // 명시: T = double, 2가 double로 변환typename T는 템플릿 매개변수입니다. 컴파일러가 호출 시 T를 결정하고 해당 타입용 함수 본문을 생성합니다.
#어셈블리 결과
add<int>: add r0, r0, r1 bx lr
add<float>: vadd.f32 s0, s0, s1 bx lr각 타입별로 최적의 명령이 생성되고, 간접 호출이 없는 zero-cost입니다.
#클래스 템플릿
가장 흔한 패턴은 컨테이너입니다.
template<typename T, size_t N>class FixedVector { T data_[N]; size_t size_ = 0;
public: bool push_back(const T& value) { if (size_ >= N) return false; data_[size_++] = value; return true; }
T& operator[](size_t i) { return data_[i]; } size_t size() const { return size_; }};
FixedVector<int, 16> a;FixedVector<float, 32> b;FixedVector<Order, 8> c;각 인스턴스가 별개의 타입이 됩니다. a.push_back(1)은 int용 함수, b.push_back(1.5f)는 float용 함수입니다.
#Non-type template parameter
size_t N은 타입이 아닌 값으로 들어가는 컴파일 타임 상수입니다.
FixedVector<int, 16> a;FixedVector<int, 32> b;// a와 b는 *다른 타입* — 함께 못 섞음크기가 타입의 일부가 되어 컴파일 타임에 확정됩니다. 런타임에 크기를 바꿀 수는 없지만, 그게 곧 zero-cost의 비결입니다.
#임베디드 — Ring Buffer 템플릿
template<typename T, size_t N>class RingBuffer { static_assert(N > 0 && (N & (N - 1)) == 0, "N must be power of 2");
T buffer_[N]; size_t head_ = 0; size_t tail_ = 0; static constexpr size_t kMask = N - 1;
public: bool push(const T& value) { size_t next = (head_ + 1) & kMask; if (next == tail_) return false; // full buffer_[head_] = value; head_ = next; return true; }
bool pop(T& out) { if (tail_ == head_) return false; // empty out = buffer_[tail_]; tail_ = (tail_ + 1) & kMask; return true; }
size_t size() const { return (head_ - tail_) & kMask; }};
// 사용RingBuffer<uint8_t, 256> uart_rx;RingBuffer<LogEntry, 64> log_queue;(head_ + 1) & kMask로 modulo 없이 circular index를 계산합니다. N이 2의 거듭제곱이어야 하므로 static_assert로 강제합니다.
각 인스턴스(uart_rx, log_queue)는 별도 클래스가 됩니다. 멤버 함수 호출은 direct call이며 가상 함수가 없습니다.
#임베디드 — GPIO 추상화
template<uintptr_t Port, uint8_t Pin>class Gpio {public: static void set() { *reinterpret_cast<volatile uint32_t*>(Port + 0x18) = 1u << Pin; } static void clear() { *reinterpret_cast<volatile uint32_t*>(Port + 0x18) = 1u << (Pin + 16); } static bool read() { return (*reinterpret_cast<volatile uint32_t*>(Port + 0x10) >> Pin) & 1; }};
using LedRed = Gpio<0x40020000, 5>;using LedGreen = Gpio<0x40020000, 6>;using Button = Gpio<0x40020400, 13>;
LedRed::set();LedGreen::set();if (Button::read()) { /* */ }Port와 Pin이 컴파일 타임 상수이므로 컴파일러가 완전한 코드를 생성합니다.
LedRed::set: ldr r3, =0x40020018 movs r2, #32 ; 1 << 5 str r2, [r3] bx lrC 매크로 #define LED_RED_SET()의 결과와 동일하지만 타입 안전성이 더해집니다.
LedRed와 Button은 서로 다른 타입이므로 섞어 쓸 수 없습니다. C 매크로였다면 무심코 섞일 수 있었을 자리입니다.
#템플릿 specialization
특정 타입에 다른 구현을 제공하는 방식입니다.
template<typename T>struct Serializer { static size_t serialize(T value, uint8_t* buf) { // 기본 — memcpy std::memcpy(buf, &value, sizeof(T)); return sizeof(T); }};
// uint16_t — big-endian 직접template<>struct Serializer<uint16_t> { static size_t serialize(uint16_t value, uint8_t* buf) { buf[0] = (value >> 8) & 0xFF; buf[1] = value & 0xFF; return 2; }};
// uint32_t — big-endiantemplate<>struct Serializer<uint32_t> { static size_t serialize(uint32_t value, uint8_t* buf) { buf[0] = (value >> 24) & 0xFF; buf[1] = (value >> 16) & 0xFF; buf[2] = (value >> 8) & 0xFF; buf[3] = value & 0xFF; return 4; }};호출자는 동일한 syntax를 쓰고, 컴파일러가 타입별 specialization을 선택합니다.
Serializer<uint16_t>::serialize(0x1234, buf); // big-endian 직접Serializer<float>::serialize(1.5f, buf); // 기본 memcpy#템플릿과 auto (C++14+)
C++14부터 템플릿 함수의 반환 타입을 auto로 둘 수 있습니다.
template<typename T, typename U>auto add(T a, U b) { return a + b;}
auto x = add(1, 2.5); // doubleC++17에서는 lambda의 auto 매개변수가 가능합니다.
auto add = [](auto a, auto b) { return a + b; };auto x = add(1, 2.5);C++20부터는 함수 자체에도 auto 매개변수를 쓸 수 있습니다(abbreviated function template).
auto add(auto a, auto b) { return a + b; }// 동등: template<typename T, typename U> auto add(T a, U b) { return a + b; }#Variadic templates — 가변 인자
C의 printf는 타입 안전성이 없고 va_list 사용이 위험합니다.
printf("value: %d", 1.5f); // %d인데 float — undefined behaviorC++의 variadic template은 타입 안전한 가변 인자를 제공합니다.
template<typename... Args>void log(const char* fmt, Args... args) { // args를 처리 — 타입 정보 보존 print_each(args...);}
template<typename T>void print_one(T value) { // T가 무엇인지 컴파일 타임에 앎}
template<typename T, typename... Rest>void print_each(T first, Rest... rest) { print_one(first); if constexpr (sizeof...(rest) > 0) { print_each(rest...); }}
log("hello", 1, 2.5f, "world"); // 각 인자 타입 안전 처리GCC 11+의 std::format(C++20)이 type-safe printf 역할을 합니다. 다만 임베디드에서는 크기 부담이 있어, header-only이고 임베디드 친화적인 fmt::format을 자주 씁니다.
#임베디드 — Type-safe Print
template<typename T>void uart_print(T value);
// specializationtemplate<> void uart_print<int>(int v) { char buf[12]; int len = itoa(v, buf); uart_send(buf, len);}
template<> void uart_print<float>(float v) { char buf[16]; int len = ftoa(v, buf); uart_send(buf, len);}
template<> void uart_print<const char*>(const char* s) { uart_send(s, strlen(s));}
// fold expression으로 여러 인자template<typename... Args>void uart_log(Args&&... args) { (uart_print(args), ...); // C++17 fold expression}
uart_log("counter: ", 42, " freq: ", 168.0f, " MHz\n");%d, %f 같은 포맷 매칭 오류가 일어날 수 없습니다. 타입이 맞아야만 컴파일됩니다.
#자주 보는 함정과 안티패턴
#1. 템플릿이 header에 없음
템플릿 정의는 header에 있어야 인스턴스화가 가능합니다. .cpp에만 있으면 link error가 발생합니다.
template<typename T>void func(T x); // 선언만
// foo.cpptemplate<typename T>void func(T x) { /* */ } // 정의 — 다른 TU에서 인스턴스 불가정의를 header로 옮기거나 explicit instantiation을 사용합니다.
#2. 과도한 인스턴스화로 인한 code bloat
같은 함수를 수많은 타입으로 인스턴스화하면 각자의 코드가 쌓여 크기가 폭증합니다. 공통 부분을 분리해 해결합니다(Part 2-07).
#3. type-safe하지 않은 인자
template<typename T>void process(T* data, size_t n);
int* arr = ...;process(arr, 100); // 100 맞나? 컴파일러 모름대안으로 C++20의 std::span<T>를 씁니다.
#4. template error message 폭증
중첩 template 오류는 수십 줄짜리 메시지가 됩니다. C++20 concepts로 훨씬 깔끔해집니다.
#5. forward declaration만으로는 인스턴스화 불가
template<typename T> class Foo; // 선언만Foo<int> x; // ERROR — 인스턴스 불가정의가 필요하거나, 포인터/레퍼런스 형태로만 사용해야 합니다.
#6. 멤버 함수 override가 template
virtual 함수는 template이 될 수 없습니다. type erasure나 visitor로 우회합니다.
#측정 — 템플릿 인스턴스화 크기
같은 RingBuffer를 5가지 타입으로 사용한 결과입니다.
RingBuffer<uint8_t, 256> : push 24 B, pop 28 BRingBuffer<uint16_t, 256> : push 28 B, pop 32 BRingBuffer<uint32_t, 256> : push 32 B, pop 36 BRingBuffer<Order, 64> : push 80 B, pop 96 B (Order is 24 B)RingBuffer<LogEntry, 16> : push 64 B, pop 72 B (LogEntry is 16 B)
총 추가 코드: ~432 B5개 타입 사용으로 432 B가 늘었습니다. 대안인 void* 기반 ring buffer는 간접 호출과 캐스팅이 들어가 느리고 타입 안전성도 떨어집니다.
5개로 분해된 타입 안전 코드라도 전체 프로젝트에서는 1% 이내입니다. 트레이드오프가 유리한 쪽입니다.
#정리
- 템플릿은 컴파일 타임 다형성으로, 타입별 전용 코드를 생성합니다.
- 함수, 클래스, non-type 매개변수를 모두 지원합니다.
- 임베디드에서는 RingBuffer, GPIO 추상화, type-safe print에 활용합니다.
- 가상 함수와 간접 호출이 없는 zero-cost입니다.
- 비용은 컴파일 시간과 인스턴스별 코드 크기에서 발생하며 적절히 관리해야 합니다 (Part 2-07).
#관련 항목
- Part 2-07: Templates 비용 분석 — code bloat 추적
- Part 2-08: Static Polymorphism — CRTP
- Part 2-09: Type Traits 활용 — SFINAE
- Part 2-10: Concepts (C++20) — template 제약
- Part 5-02: GPIO 추상화 — 템플릿 GPIO
#다음 글
Part 2-07: Templates 비용 분석 — 같은 함수가 여러 타입에 쓰일 때 발생하는 코드 bloat를 추적하고 통제하는 방법을 다룹니다.
Embedded C++ for Real Systems · 15 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 비교
관련 글
이 글을 참조하는 글 (7)
- GPIO 추상화 패턴 — Template·Concept으로 보드 독립성— Embedded C++ for Real Systems
- MMIO Register 추상화 — 타입 안전한 비트 필드 접근— Embedded C++ for Real Systems
- C++20 Concepts 활용 — 템플릿 제약과 가독성 개선— Embedded C++ for Real Systems
- Type Traits 임베디드 활용 — SFINAE·is_pod·컴파일 타임 검사— Embedded C++ for Real Systems
- CRTP 패턴 분석 — vtable 없는 정적 다형성— Embedded C++ for Real Systems
- Template 비용 분석 — 코드 폭증·인스턴스화·디버그 정보 측정— Embedded C++ for Real Systems
- consteval과 constinit 분석 — C++20 컴파일 타임 강제 메커니즘— Embedded C++ for Real Systems