constexpr 기초와 임베디드 적용 — 컴파일 타임 계산 활용
#한 줄 요약
“런타임에 할 일을 컴파일러에게 시킵니다.” — 코드와 데이터 모두 컴파일 타임으로 옮길 수 있습니다.
#어떤 문제를 푸는가
런타임 비용은 두 형태로 나타납니다.
- CPU 사이클 — 함수 실행 시간
- 메모리 — 코드와 데이터가 차지하는 공간
constexpr은 컴파일 타임에 결과를 계산해 runtime cost를 0으로 만들어 줍니다.
// 런타임 계산int factorial(int n) { return (n <= 1) ? 1 : n * factorial(n - 1);}
int x = factorial(10); // runtime에 계산// 컴파일 타임 계산constexpr int factorial(int n) { return (n <= 1) ? 1 : n * factorial(n - 1);}
constexpr int x = factorial(10); // 컴파일러가 3628800으로 치환어셈블리에서 x는 그냥 상수 3628800이 됩니다. 함수 호출도, 계산도 남지 않습니다.
#constexpr이 적용되는 두 곳
- 변수 — 컴파일 타임 상수
- 함수 — 컴파일 타임 호출 가능
#constexpr 변수
constexpr int kBufferSize = 1024; // 정수constexpr float kPi = 3.14159f; // 실수constexpr const char* kVersion = "1.2"; // 포인터/문자열
// 컴파일 타임에 계산constexpr int kEntries = kBufferSize / sizeof(int);const와의 차이는 다음과 같습니다.
const는 읽기 전용이라는 의미이며, 값은 컴파일 타임이나 런타임에 결정될 수 있습니다.constexpr은 컴파일 타임에 값이 알려진다는 강한 보장입니다.
int runtime_val = read_register();const int a = runtime_val; // OK — const는 runtime 값 가능constexpr int b = runtime_val; // ERROR — constexpr은 compile-time 필요constexpr int c = 42; // OK#constexpr 함수
constexpr int square(int x) { return x * x;}
constexpr int a = square(5); // 25 — 컴파일 타임int b = square(read_input()); // 런타임 호출 (compile-time 값 아니므로)constexpr 함수는 상황에 따라 컴파일 타임 또는 런타임에 호출됩니다. 컴파일러가 인자를 보고 결정합니다.
C++14 이전에는 constexpr 함수가 한 줄짜리 return만 가능했습니다. C++14부터는 loop, branch, 변수 등 대부분의 statement를 쓸 수 있습니다.
// C++14 — full constexprconstexpr int factorial(int n) { int result = 1; for (int i = 2; i <= n; ++i) { result *= i; } return result;}#임베디드 — 컴파일 타임 LUT
룩업 테이블을 컴파일러가 자동으로 생성합니다. RAM/Flash 공간은 그대로 쓰지만 런타임 계산은 사라집니다.
// Sin 테이블 256 entryconstexpr std::array<float, 256> generate_sin_table() { std::array<float, 256> table{}; for (int i = 0; i < 256; ++i) { // 컴파일 타임에 sin 계산 (C++26 정도까지 std::sin은 안 됨, 직접 Taylor) float angle = (i * 2.0f * 3.14159f) / 256.0f; table[i] = taylor_sin(angle); } return table;}
constexpr auto sin_table = generate_sin_table(); // Flash에 박힘sin_table은 256 × 4 = 1024 바이트가 .rodata 섹션에 들어갑니다. 런타임 초기화 코드가 전혀 없습니다.
#Taylor series로 컴파일 타임 sin
constexpr float taylor_sin(float x) { // x를 [-pi, pi]로 정규화 (생략) float result = x; float term = x; for (int n = 1; n < 7; ++n) { term *= -x * x / ((2 * n) * (2 * n + 1)); result += term; } return result;}C++ 표준 sin/cos는 constexpr이 아닙니다. Taylor 급수나 CORDIC을 직접 구현해야 합니다. 한 번 작성하면 전체 LUT를 자동으로 생성할 수 있습니다.
#임베디드 — 컴파일 타임 CRC
CRC 테이블 생성은 임베디드의 단골 작업입니다. constexpr로 컴파일 타임에 만들어 둘 수 있습니다.
constexpr std::array<uint32_t, 256> generate_crc_table() { std::array<uint32_t, 256> table{}; constexpr uint32_t poly = 0xEDB88320; for (uint32_t i = 0; i < 256; ++i) { uint32_t crc = i; for (int j = 0; j < 8; ++j) { crc = (crc & 1) ? (crc >> 1) ^ poly : crc >> 1; } table[i] = crc; } return table;}
constexpr auto crc_table = generate_crc_table();
uint32_t compute_crc(const uint8_t* data, size_t len) { uint32_t crc = 0xFFFFFFFF; for (size_t i = 0; i < len; ++i) { crc = (crc >> 8) ^ crc_table[(crc ^ data[i]) & 0xFF]; } return ~crc;}테이블이 Flash에 미리 박히고 런타임 초기화 코드가 0이 됩니다. 비교하면 다음과 같습니다.
# C — 첫 호출 시 lazy 초기화 또는 main 시작 시 초기화# 코드 사이즈: +200 바이트 (초기화 함수) + 1024 (테이블)
# C++ constexpr — 컴파일 타임 생성# 코드 사이즈: 1024 (테이블만)#임베디드 — 컴파일 타임 register 주소
여러 peripheral의 register 주소는 컴파일 타임 상수로 알 수 있습니다. 함수로 계산해 둡니다.
constexpr uintptr_t gpio_base(int port) { return 0x40020000 + port * 0x400; // STM32 GPIOA, GPIOB, ...}
constexpr uintptr_t kGpioAOdrAddr = gpio_base(0) + 0x14;constexpr uintptr_t kGpioBOdrAddr = gpio_base(1) + 0x14;
#define GPIOA_ODR (*reinterpret_cast<volatile uint32_t*>(kGpioAOdrAddr))매크로 대신 컴파일 타임 함수와 상수를 씁니다. 타입 안전성과 디버깅 가능성을 함께 얻습니다.
#if constexpr (C++17)
컴파일 타임 분기입니다. 선택되지 않은 분기는 컴파일러가 제거합니다.
template<typename T>void serialize(uint8_t* buf, T value) { if constexpr (sizeof(T) == 1) { buf[0] = value; } else if constexpr (sizeof(T) == 2) { buf[0] = value >> 8; buf[1] = value & 0xFF; } else if constexpr (sizeof(T) == 4) { buf[0] = value >> 24; buf[1] = (value >> 16) & 0xFF; buf[2] = (value >> 8) & 0xFF; buf[3] = value & 0xFF; }}
// 사용uint8_t buf[4];serialize(buf, uint16_t(0x1234)); // 2바이트 분기만 컴파일됨sizeof(T)가 컴파일 타임에 알려지므로 선택되지 않은 분기의 코드가 제거됩니다.
런타임 if는 모든 분기를 컴파일해 코드 크기가 늘어나지만, if constexpr은 필요한 분기만 남깁니다.
#static_assert + constexpr로 컴파일 타임 검증
constexpr int kMaxBufferSize = 4096;constexpr int kSlotCount = 16;
static_assert(kMaxBufferSize % kSlotCount == 0, "Buffer size must be divisible by slot count");
constexpr int kSlotSize = kMaxBufferSize / kSlotCount;잘못된 설정은 컴파일 실패로 이어집니다. 런타임에 발견되지 않습니다.
#런타임 vs 컴파일 타임 — 어셈블리 비교
같은 코드에 constexpr 적용 여부만 다르게 한 결과입니다.
// V1 — 런타임 계산int compute_threshold(int level) { int base = 100; for (int i = 0; i < level; ++i) base *= 2; return base;}
int threshold = compute_threshold(5);
// V2 — constexprconstexpr int compute_threshold(int level) { int base = 100; for (int i = 0; i < level; ++i) base *= 2; return base;}
constexpr int threshold = compute_threshold(5); // = 3200V1의 어셈블리는 루프 실행과 함수 호출이 그대로 남습니다.
compute_threshold: movs r3, #100 cbz r0, .L2 mov r2, r0.L3: lsls r3, r3, #1 subs r2, r2, #1 bne .L3.L2: mov r0, r3 bx lrV2의 어셈블리는 상수만 남습니다.
# threshold 변수 직접 사용 — 함수 호출 없음ldr r0, =3200완전한 zero-cost입니다.
#constexpr의 제약
C++14 기준이며, 대부분 C++17/20에서 완화됐습니다.
- 동적 메모리 할당이 불가합니다(C++20에서
constexpr new허용). - 예외 throw가 불가합니다(C++20에서 부분 완화).
- virtual 함수 호출이 불가합니다(C++20에서 허용).
- try/catch가 불가합니다(C++20에서 부분 허용).
- I/O는 영원히 불가합니다.
reinterpret_cast도 불가합니다.
// 안 됨 (C++17)constexpr int* ptr() { return new int(42); } // dynamic alloc
// 됨 (C++20)constexpr int* ptr() { return new int(42); }C++ 표준이 점진적으로 완화되고 있으며, GCC 13은 C++20 constexpr의 약 90%를 지원합니다.
#consteval — 컴파일 타임 강제 (C++20)
constexpr은 상황에 따라 런타임 호출도 허용하지만, consteval은 컴파일 타임을 강제합니다.
consteval int square(int x) { return x * x;}
constexpr int a = square(5); // OK — 컴파일 타임int b = square(read_input()); // ERROR — 런타임 인자 불가런타임 호출을 원천 차단합니다. 자세한 내용은 Part 2-05에서 다룹니다.
#constinit — 정적 초기화 강제 (C++20)
static 객체의 초기화 시점을 보장합니다.
constinit int counter = compute_initial(); // 컴파일 타임에 초기화
// 컴파일 에러 — runtime 초기화 시도constinit int x = read_register();Part 1-06 — Static Initialization Order Fiasco를 컴파일 타임에 방지하는 기능입니다.
#자주 보는 함정과 안티패턴
#1. const ≠ constexpr
const int n = read(); // OK, but not compile-timeint arr[n]; // C VLA — 표준 C++ 아님 (gnu++만)
constexpr int n = 100;int arr[n]; // OK — 컴파일 타임 크기#2. constexpr 함수에 IO나 std::sin 사용
constexpr float my_sin(float x) { return std::sin(x); // ERROR (C++ 표준 sin이 constexpr 아님)}직접 Taylor 급수로 구현하거나 C++26을 기다려야 합니다.
#3. 큰 constexpr LUT으로 컴파일 시간 폭증
constexpr auto huge = generate_table<1000000>(); // 컴파일 30초+보통 256~4096 entry 정도가 적절합니다.
#4. 컴파일러가 constexpr을 적용하지 못함
constexpr int f(int x) { /* */ }int y = f(some_var); // some_var가 const 아니면 런타임 호출constexpr auto y = f(...);로 받거나 kConst 변수에 대입해 강제합니다.
#5. constexpr이 LTO와 충돌
거의 발생하지 않습니다. constexpr이 먼저 적용돼 LTO 단계에는 이미 결과가 박혀 있습니다.
#6. constexpr 멤버 함수에서 mutable 필드 변경
C++14부터 constexpr 멤버 함수에서 mutable 필드를 변경할 수 있습니다. 다만 const 멤버에서는 여전히 불가능합니다.
#측정 — constexpr 적용 후 코드 크기
CRC 테이블을 STM32F4에서 비교합니다.
# C — runtime 초기화.text : +180 B (init function).bss : +1024 B (table, runtime filled)init time : ~50 us at startup
# C++ constexpr.text : +0 B (no init).rodata : +1024 B (Flash, compile-time)init time : 0 us초기화 코드와 시간을 함께 절약합니다. 데이터가 RAM에서 Flash로 옮겨가는데, 대부분의 임베디드 환경은 Flash 쪽이 더 넉넉합니다.
#정리
constexpr은 컴파일 타임에 값이 정해지는 변수와 함수에 모두 적용됩니다.- LUT, CRC table, lookup 함수를 컴파일 타임에 생성해 RAM을 절약합니다.
if constexpr은 컴파일 타임 분기이며 코드 크기를 줄여 줍니다.- C++14 이후 대부분의 statement가
constexpr가능하며, C++20에서는 new와 virtual까지 완화됐습니다. consteval(C++20)은 런타임 호출을 차단하고,constinit은 static 초기화를 강제합니다.
#관련 항목
- Part 2-04: constexpr 고급 — 복잡한 컴파일 타임 알고리즘
- Part 2-05: consteval과 constinit — C++20 추가 키워드
- Part 1-07: 링커 스크립트 —
.rodata배치 - Part 5-01: Register 추상화 — constexpr 주소
#다음 글
Part 2-04: constexpr 고급 — 컴파일 타임 sort, search, 문자열 처리를 다룹니다. constexpr 알고리즘의 한계와 가능성을 함께 살펴봅니다.
Embedded C++ for Real Systems · 12 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.
constexpr 고급 활용 — 룩업 테이블·CRC·해시 컴파일 타임 생성
컴파일 타임 sort, search, 문자열 — constexpr 알고리즘의 한계와 가능성.
Type Traits 임베디드 활용 — SFINAE·is_pod·컴파일 타임 검사
std::is_*, std::enable_if, SFINAE — 컴파일 타임 type 정보로 코드 분기와 검증.