ETL 라이브러리 분석 — Embedded Template Library의 STL 대체
#한 줄 요약
“ETL은 STL의 임베디드 친화 대체입니다.” 모든 컨테이너가 고정 크기이고 heap을 쓰지 않으며, FSM과 message router와 factory도 함께 제공합니다.
#어떤 문제를 푸는가
C++ STL은 대부분 heap을 사용하지만, 임베디드에서는 heap을 회피해야 합니다. ETL은 STL과 비슷한 인터페이스를 fixed-size + zero heap으로 제공합니다.
#include <etl/vector.h>
etl::vector<int, 32> v; // 최대 32개v.push_back(1);if (v.full()) { /* */ }추가로 다음을 제공합니다.
- Fixed-size FSM
- Fixed-size message router
etl::delegate(std::function 대체)etl::observer(Observer 패턴)etl::string(fixed-size)
홈은 etlcpp.com이며 MIT 라이선스의 header-only 라이브러리입니다.
#기본 컨테이너
#etl::vector<T, N>
#include <etl/vector.h>
etl::vector<int, 32> v;
v.push_back(1);v.push_back(2);
for (auto x : v) { /* */ }
if (v.full()) { /* 32 도달 */ }if (v.empty()) { /* */ }
v.erase(v.begin());v.clear();내부적으로는 T storage[N]과 size만 갖고 있어 heap을 쓰지 않습니다.
#etl::string<N>
#include <etl/string.h>
etl::string<64> s = "hello";s += " world";
if (s.size() > 64) { /* truncated */ }
const char* cstr = s.c_str();고정 크기 char array이며 길이가 초과되면 잘립니다.
#etl::list<T, N>
#include <etl/list.h>
etl::list<Order, 16> orders;
orders.push_back(o);orders.pop_front();
for (auto& o : orders) { /* */ }doubly linked list이며 고정 크기 pool을 내부에 두므로 heap을 쓰지 않습니다.
#etl::map<K, V, N>
#include <etl/map.h>
etl::map<int, Order, 16> m;m.insert({1, Order{}});m[5] = Order{};
auto it = m.find(1);if (it != m.end()) { /* */ }RB-tree 기반이며 노드 pool도 고정 크기입니다.
#etl::queue<T, N>
#include <etl/queue.h>
etl::queue<int, 8> q;q.push(1);int v = q.front();q.pop();ring buffer 기반입니다.
#etl::delegate — std::function 대체
std::function은 내부에서 heap을 쓸 수 있지만, etl::delegate는 fixed-size입니다.
#include <etl/delegate.h>
class Handler {public: void on_event(int e) { /* */ }};
Handler h;
etl::delegate<void(int)> d;d = etl::delegate<void(int)>::create<Handler, &Handler::on_event>(h);
d(42); // h.on_event(42) 호출크기는 object와 method 두 pointer만큼이며 heap을 쓰지 않습니다.
함수와 lambda도 받을 수 있습니다.
auto d1 = etl::delegate<int(int, int)>::create<add_function>();auto d2 = etl::delegate<void()>::create<&Class::static_method>();#etl::observer — Observer 패턴
#include <etl/observer.h>
struct TempEvent { float celsius; };
class TempObserver : public etl::observer<TempEvent> {public: void notification(TempEvent e) override { if (e.celsius > 80) alarm(); }};
class TempSensor : public etl::observable<TempObserver, 4> { // 최대 4 observerpublic: void update() { notify_observers(TempEvent{read_temperature()}); }};
TempSensor sensor;TempObserver obs;sensor.add_observer(obs);
sensor.update(); // obs.notification 호출heap 없는 pub/sub이며 최대 observer 수가 고정되어 있습니다.
#etl::fsm — Finite State Machine
ETL의 대표 기능으로 fixed-size FSM을 제공합니다.
#include <etl/fsm.h>
// Event 정의struct events { enum { start, stop, error, };};
class StartEvent : public etl::message<events::start> {};class StopEvent : public etl::message<events::stop> {};
// State 정의enum class StateId : etl::fsm_state_id_t { idle, running, error,};
class MyFsm : public etl::fsm {public: MyFsm() : fsm(0) {}};
class IdleState : public etl::fsm_state<MyFsm, IdleState, (etl::fsm_state_id_t)StateId::idle, StartEvent> {public: etl::fsm_state_id_t on_event(const StartEvent&) { log("starting"); return (etl::fsm_state_id_t)StateId::running; }
etl::fsm_state_id_t on_event_unknown(const etl::imessage&) { return STATE_ID; // 무시 }};
class RunningState : public etl::fsm_state<MyFsm, RunningState, (etl::fsm_state_id_t)StateId::running, StopEvent> {public: etl::fsm_state_id_t on_event(const StopEvent&) { log("stopping"); return (etl::fsm_state_id_t)StateId::idle; } // ...};
// 사용IdleState idle;RunningState running;etl::ifsm_state* states[] = {&idle, &running};
MyFsm fsm;fsm.set_states(states, etl::size(states));fsm.start();
fsm.receive(StartEvent{}); // idle → runningfsm.receive(StopEvent{}); // running → idleheap을 쓰지 않고, type-safe event dispatch가 가능하며, 상태 전이가 명확합니다.
자세한 FSM 패턴은 Part 4-06에서 다룹니다.
#etl::message_router — Type-safe routing
#include <etl/message_router.h>
struct TempMessage : public etl::message<1> { float celsius; };struct PressureMessage : public etl::message<2> { float kpa; };
class SensorRouter : public etl::message_router<SensorRouter, TempMessage, PressureMessage> {public: void on_receive(const TempMessage& m) { log_temp(m.celsius); }
void on_receive(const PressureMessage& m) { log_pressure(m.kpa); }
void on_receive_unknown(const etl::imessage&) { // 무시 }};
SensorRouter router;router.receive(TempMessage{25.0f});router.receive(PressureMessage{101.3f});message type별 dispatch가 컴파일 타임에 결정되며 RTTI를 쓰지 않습니다.
#etl::array vs std::array
ETL은 etl::array도 제공합니다. 차이는 주로 예외 환경에서 드러납니다.
etl::array<int, 8> a = {1, 2, 3};// std::array와 거의 동일 API// 단 -fno-exceptions에서 .at() 동작이 차이대부분의 경우 std::array로 충분합니다.
#etl::optional
#include <etl/optional.h>
etl::optional<int> x;if (x) { /* */ }x = 42;*x = 10;C++17 이전 환경에서 std::optional의 대체로 쓰며, C++17 이상에서는 표준을 사용합니다.
#설정 매크로
ETL은 컴파일러와 플랫폼 설정이 필요합니다.
#define ETL_NO_STL // STL 의존 끔#define ETL_NO_LARGE_CHAR_SUPPORT#define ETL_LOG_ERRORS // 로깅#define ETL_VERBOSE_ERRORS // 상세 에러#define ETL_DEBUG_COUNT // 디버깅 통계// 또는 컴파일러 플래그CXXFLAGS += -DETL_NO_STL -DETL_LOG_ERRORS#에러 처리
ETL은 예외 또는 abort를 선택할 수 있습니다.
// 예외 모드 (기본)etl::vector<int, 4> v;v.push_back(1); v.push_back(2); v.push_back(3); v.push_back(4);v.push_back(5); // throws etl::vector_full
// no-exception 모드#define ETL_NO_EXCEPTIONS
// → push_back에 5번째는 그냥 *무시* 또는 ETL_ASSERT triggerETL_ASSERT 매크로를 정의해 fail 동작을 직접 통제할 수 있습니다.
#define ETL_ASSERT(condition, error) \ if (!(condition)) { fatal_error(error.what()); }#측정 — ETL vs STL
같은 코드를 STL과 ETL로 비교한 결과입니다 (STM32F4).
std::vector<int> + 16 elements: .text: 6.2 KB (vector + heap) .heap usage: ~64 B alloc cost: 변동 (realloc 가능)
etl::vector<int, 16>: .text: 0.4 KB (간단한 wrapper) .heap usage: 0 alloc cost: 0 (compile-time fixed)코드 크기가 15배 작고 heap도 쓰지 않습니다.
#자주 보는 함정과 안티패턴
#1. fixed-size 초과
etl::vector<int, 4> v;for (int i = 0; i < 10; ++i) v.push_back(i); // 4 이후 failfull()을 먼저 체크하거나 capacity를 늘립니다.
#2. STL과 혼용
std::vector<etl::string<32>> v; // STL vector는 heap완전히 ETL을 쓰거나, STL이 허용되는 환경에서만 섞습니다.
#3. exception 가정
ETL은 기본적으로 exception을 던집니다. -fno-exceptions 환경에서는 abort합니다. 이때는 ETL_NO_EXCEPTIONS를 정의합니다.
#4. 큰 N으로 .bss 폭증
etl::vector<HugeStruct, 1000> v;sizeof(HugeStruct) * 1000이 그대로 .bss에 잡힙니다. 측정 후 적정 N을 선택합니다.
#5. Boost.Intrusive와 비교하지 않음
ETL은 가볍고 Boost는 더 강력합니다. 프로젝트의 요구에 맞게 선택합니다.
#6. Header-only 의존
ETL은 header-only라서 컴파일 시간이 늘어납니다. PCH를 활용합니다.
#ETL의 장점 vs 단점
| 장점 | 단점 |
|---|---|
| Heap 0 | API가 STL과 일부 다름 |
| 인증 친화 (MISRA 가까움) | C++11/14 위주 (C++20 일부) |
| Header-only | 컴파일 시간 |
| Fixed-size 명시 | 동적 크기 불가 |
| FSM, router 포함 | 학습 곡선 |
| Active maintenance | Boost만큼 풍부하진 않음 |
#정리
- ETL은 임베디드용 STL 대체 라이브러리로, 모두 fixed-size이며 heap을 쓰지 않습니다.
- 기본 컨테이너로
etl::vector,etl::string,etl::list,etl::map,etl::queue를 제공합니다. - 특수 도구로
etl::delegate(std::function 대체),etl::observer,etl::fsm,etl::message_router가 있습니다. -fno-exceptions환경에서는ETL_NO_EXCEPTIONS매크로를 정의합니다.- 측정 가능한 fixed-size이므로
.bss영향을 추적할 수 있습니다. - MIT 라이선스이며 header-only로 배포됩니다.
#관련 항목
#다음 글
Part 4-03: Lock-free 기초 — mutex 없이 동시성. atomic, CAS, memory order.
Embedded C++ for Real Systems · 31 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 비교
관련 글
임베디드 State Machine 패턴 — Variant·Visitor·Table-driven 비교
타입 안전한 상태 머신 — enum + switch부터 std::variant, etl::fsm까지.
Compile-time FSM 구현 — 템플릿으로 상태 전이 검증
constexpr state machine — 컴파일 타임에 전이 검증, runtime 코드 0.
동적 할당 없는 임베디드 C++ — placement new·정적 객체·풀
임베디드의 첫 번째 원칙 — new/malloc 없이 modern C++의 STL 같은 컨테이너 활용.
이 글을 참조하는 글 (7)
- C++ in RTOS — RAII·std::thread·ETL·Coroutine— Practical RTOS Internals
- 임베디드 State Machine 패턴 — Variant·Visitor·Table-driven 비교— Embedded C++ for Real Systems
- Intrusive Containers 분석 — 동적 할당 없는 컨테이너 설계— Embedded C++ for Real Systems
- Custom Allocator 기초 — std::allocator 인터페이스 분석— Embedded C++ for Real Systems
- 동적 할당 없는 임베디드 C++ — placement new·정적 객체·풀— Embedded C++ for Real Systems
- Template 비용 분석 — 코드 폭증·인스턴스화·디버그 정보 측정— Embedded C++ for Real Systems
- C++ 코드 크기 분석 — 가상 함수·템플릿·예외 비용 추적— Embedded C++ for Real Systems