C++ constexpr 완벽 가이드 | 컴파일 타임 계산·if constexpr·consteval 실전
이 글의 핵심
C++ constexpr 완벽 가이드에 대한 실전 가이드입니다. 컴파일 타임 계산·if constexpr·consteval 실전 등을 예제와 함께 상세히 설명합니다.
들어가며: “컴파일 타임에 계산하고 싶어요”
구체적인 문제 시나리오
프로젝트를 진행하다 보면 이런 상황을 자주 겪습니다:
- 배열 크기를 런타임 변수로 넘기려 했는데
int arr[n]에서 컴파일 에러가 난다 - CRC32, 해시 테이블 같은 값들을 매번 런타임에 계산하는데, 값이 고정되어 있으면 아깝다
- 설정값(버퍼 크기, 타임아웃 등)을 코드에 하드코딩하지 않고, 컴파일 시점에 한 번만 계산해 두고 싶다
- 템플릿 인자로
std::array<int, 128>처럼 상수를 넘겨야 하는데,getBufferSize()같은 함수 결과를 쓸 수 없다
추가 문제 시나리오
시나리오 1: 프로토콜 버퍼 크기 고정
네트워크 패킷 헤더가 항상 16바이트이고, 페이로드 최대 크기가 4096바이트일 때, std::array<uint8_t, 16 + 4096>처럼 컴파일 타임에 크기를 정해야 합니다. 16 + 4096을 매크로나 매직 넘버로 쓰지 않고 constexpr 함수로 계산하면 유지보수가 쉬워집니다.
시나리오 2: Base64 인코딩 테이블
Base64 인코딩용 64개 문자 룩업 테이블은 실행 시점에 바뀌지 않습니다. constexpr로 컴파일 타임에 테이블을 생성하면 런타임 초기화 비용이 사라지고, .rodata 섹션에 상수로 들어가 메모리 효율도 좋아집니다.
시나리오 3: JSON 스키마 검증 상수
최대 필드 개수, 최대 문자열 길이 등이 JSON 스키마에 정의되어 있을 때, 이 값들을 컴파일 타임 상수로 파싱해 std::array 크기나 static_assert에 사용하면, 스키마 변경 시 컴파일 단계에서 오류를 잡을 수 있습니다.
시나리오 4: 임베디드 메모리 맵
레지스터 오프셋, 메모리 영역 크기, 인터럽트 벡터 개수 등이 보드별로 고정되어 있을 때, constexpr로 보드 타입별 상수를 정의하면 switch 케이스나 템플릿 인자에 바로 쓸 수 있습니다.
시나리오 5: 단위 테스트용 고정 시드
테스트 재현성을 위해 난수 시드를 고정해야 할 때, constexpr로 시드를 계산해 두면 테스트마다 동일한 시퀀스를 보장할 수 있고, 시드 계산이 컴파일 타임에 끝나 런타임 오버헤드가 없습니다.
시나리오 6: 게임 엔진 회전 행렬
매 프레임 수천 개의 스프라이트에 2D/3D 회전 행렬을 적용할 때, 각도가 고정(예: 90°, 180°)이면 sin, cos 값을 런타임에 매번 계산하는 대신 컴파일 타임 룩업 테이블로 미리 계산해 두면 CPU 부하를 크게 줄일 수 있습니다.
C++에서는 배열 크기, 템플릿 비타입 인자, switch 케이스 등이 상수 식(constant expression)이어야 합니다. constexpr을 사용하면 “이 함수/변수는 컴파일 타임에 계산 가능하다”고 선언할 수 있어서, 상수 식이 필요한 곳에서 그 결과를 그대로 사용할 수 있습니다.
constexpr 평가 시점 시각화
flowchart TD
A[constexpr 함수 호출] --> B{인자가 상수 식?}
B -->|예| C[컴파일 타임에 평가]
B -->|아니오| D[런타임에 평가]
C --> E[배열 크기, 템플릿 인자 등에 사용 가능]
D --> F[일반 함수처럼 동작]
constexpr vs 매크로 비교
flowchart LR
subgraph macro["매크로"]
M1[전처리기 치환] --> M2[타입 검사 없음]
M2 --> M3[디버깅 어려움]
end
subgraph constexpr["constexpr"]
C1[컴파일러 평가] --> C2[타입 검사]
C2 --> C3[네임스페이스·스코프]
C3 --> C4[오버로딩·템플릿 조합]
end
이 글을 읽으면
- constexpr로 컴파일 타임 상수를 정의할 수 있습니다.
- constexpr 함수·변수·if constexpr·consteval을 완전히 이해할 수 있습니다.
- 배열 크기·템플릿 인자에 활용할 수 있습니다.
- 자주 발생하는 에러와 해결법을 알 수 있습니다.
- 프로덕션에서 바로 쓸 수 있는 패턴을 적용할 수 있습니다.
개념을 잡는 비유
템플릿 인자 자리는 붕어빵 틀의 칸 수가 정해지듯, 컴파일 시점에 크기·상수가 박혀 있어야 하는 경우가 많습니다. constexpr·컴파일 타임 계산은 그 값을 미리 찍어내어, 배열 크기와 static_assert 같은 곳에 그대로 얹을 수 있게 해 줍니다.
목차
- constexpr 변수
- constexpr 함수 완전 예제
- constexpr 변수와 생성자
- if constexpr 상세
- consteval (C++20)
- constexpr vs const 비교
- C++14/17/20 확장
- 자주 발생하는 에러와 해결법
- 베스트 프랙티스
- 성능 벤치마크
- 프로덕션 패턴
1. constexpr 변수
컴파일 타임 상수
constexpr 변수는 컴파일 시점에 값이 정해져야 합니다. 따라서 int arr[MAX]처럼 배열 크기나 std::array<int, MAX>의 템플릿 인자로 그대로 쓸 수 있습니다. const만 쓰면 런타임에 초기화된 “읽기 전용” 변수일 뿐이라 배열 크기로 쓸 수 없고, constexpr을 쓰면 “이 값은 컴파일 타임에 알려진 상수”임을 컴파일러가 보장합니다.
// 복사해 붙여넣은 뒤: g++ -std=c++17 -o constexpr_var constexpr_var.cpp && ./constexpr_var
#include <array>
#include <iostream>
constexpr int MAX = 100;
constexpr double PI = 3.14159265358979;
int main() {
int arr[MAX]; // OK: 상수 식
std::array<int, MAX> a; // OK
std::cout << "MAX=" << MAX << ", PI=" << PI << "\n";
return 0;
}
실행 결과: MAX=100, PI=3.14159265가 출력됩니다.
constexpr 변수 초기화 규칙
constexpr 변수는 반드시 리터럴이나 다른 constexpr로 초기화해야 합니다. 런타임 함수 호출 결과로는 초기화할 수 없습니다.
constexpr int add(int a, int b) { return a + b; }
constexpr int a = 42; // OK: 리터럴
constexpr int b = a + 1; // OK: 다른 constexpr
constexpr int c = add(2, 3); // OK: add가 constexpr이고 인자가 상수
int runtime_val = 100; // 런타임 변수
// constexpr int d = runtime_val; // ❌ 컴파일 에러!
2. constexpr 함수 완전 예제
2.1 기본 개념
constexpr 함수는 인자가 상수 식이면 컴파일 타임에 호출할 수 있습니다. add(3, 5)는 리터럴이므로 x는 컴파일 시점에 8로 고정되고, int arr[add(2, 3)]처럼 배열 크기로 쓸 수 있습니다. 런타임에 add(i, j)처럼 호출해도 되고, 그때는 일반 함수처럼 동작합니다.
// 복사해 붙여넣은 뒤: g++ -std=c++17 -o add add.cpp && ./add
#include <iostream>
constexpr int add(int a, int b) {
return a + b;
}
int main() {
constexpr int x = add(3, 5); // 컴파일 타임에 8
std::cout << x << "\n";
int i = 2, j = 3;
int y = add(i, j); // 런타임 호출, OK
return 0;
}
실행 결과: 8이 한 줄 출력됩니다.
2.2 팩토리얼 (C++14)
C++14부터 constexpr 함수 안에 if, 여러 return, 재귀 등이 허용됩니다.
#include <array>
constexpr unsigned long long factorial(int n) {
if (n <= 1) return 1;
return n * factorial(n - 1);
}
int main() {
constexpr auto f5 = factorial(5); // 120, 컴파일 타임
std::array<int, factorial(5)> arr; // 크기 120
return 0;
}
2.3 피보나치 (반복문으로 재귀 한계 회피)
과거에는 template <int N> struct Fib로 재귀 템플릿을 썼지만, constexpr 함수로 같은 계산을 하면 읽기와 유지보수가 쉽습니다. 재귀 대신 반복문을 쓰면 컴파일러 한계를 피할 수 있습니다.
#include <array>
constexpr int fib(int n) {
if (n <= 1) return n;
int a = 0, b = 1;
for (int i = 2; i <= n; ++i) {
int next = a + b;
a = b;
b = next;
}
return b;
}
int main() {
constexpr int f10 = fib(10); // 55
constexpr int f20 = fib(20); // 6765
std::array<int, fib(10)> buf; // 크기 55
return 0;
}
2.4 2의 거듭제곱 (nextPowerOfTwo)
버퍼 크기를 2의 거듭제곱으로 맞출 때 유용합니다.
constexpr size_t nextPowerOfTwo(size_t n) {
size_t p = 1;
while (p < n) p *= 2;
return p;
}
// 사용 예
std::array<int, nextPowerOfTwo(100)> buf; // 크기 128
2.5 문자열 길이 (C++14)
컴파일 타임에 문자열 리터럴의 길이를 구할 수 있습니다.
constexpr size_t strLen(const char* s) {
size_t len = 0;
while (s[len] != '\0') ++len;
return len;
}
constexpr size_t LEN = strLen("hello"); // 5
std::array<char, strLen("hello") + 1> buf;
2.6 clamp (조건부 계산)
constexpr int clamp(int value, int min_val, int max_val) {
if (value < min_val) return min_val;
if (value > max_val) return max_val;
return value;
}
constexpr int c = clamp(150, 0, 100); // 100
2.7 CRC32 룩업 테이블 (완전한 예제)
CRC32는 0–255 각 바이트에 대한 256개 테이블 항목이 필요합니다. constexpr로 컴파일 타임에 전체 테이블을 생성할 수 있습니다.
#include <array>
#include <cstdint>
#include <utility>
constexpr uint32_t crc32_table_entry(uint32_t idx) {
uint32_t crc = idx;
for (int i = 0; i < 8; ++i) {
crc = (crc >> 1) ^ (0xEDB88320u & -(crc & 1));
}
return crc;
}
template <size_t... Is>
constexpr auto make_crc32_table(std::index_sequence<Is...>) {
return std::array<uint32_t, sizeof...(Is)>{{crc32_table_entry(static_cast<uint32_t>(Is))...}};
}
constexpr auto CRC32_TABLE = make_crc32_table(std::make_index_sequence<256>{});
uint32_t crc32(const uint8_t* data, size_t len) {
uint32_t crc = 0xFFFFFFFFu;
for (size_t i = 0; i < len; ++i) {
crc = CRC32_TABLE[(crc ^ data[i]) & 0xFF] ^ (crc >> 8);
}
return crc ^ 0xFFFFFFFFu;
}
장점: CRC32_TABLE은 컴파일 타임에 한 번만 계산되고, 바이너리의 .rodata에 상수로 저장됩니다.
2.8 컴파일 타임 문자열 해시 (FNV-1a)
문자열 리터럴을 컴파일 타임에 해시해 switch 분기에 사용할 수 있습니다.
constexpr unsigned long long hash_fnv1a(const char* str) {
unsigned long long hash = 14695981039346656037ULL;
while (*str) {
hash ^= static_cast<unsigned long long>(*str++);
hash *= 1099511628211ULL;
}
return hash;
}
constexpr auto CMD_START_HASH = hash_fnv1a("start");
constexpr auto CMD_STOP_HASH = hash_fnv1a("stop");
void handle_command(const char* cmd) {
switch (hash_fnv1a(cmd)) {
case CMD_START_HASH:
// start 처리
break;
case CMD_STOP_HASH:
// stop 처리
break;
default:
break;
}
}
3. constexpr 변수와 생성자
리터럴 타입
constexpr 변수로 쓰려면 타입이 리터럴 타입(literal type)이어야 합니다. 기본 타입(int, double 등), 배열, constexpr 생성자를 가진 사용자 정의 타입 등이 리터럴 타입입니다.
constexpr 생성자 예시
struct Point {
int x, y;
constexpr Point(int x, int y) : x(x), y(y) {}
constexpr int sum() const { return x + y; }
};
constexpr Point p(1, 2);
constexpr int s = p.sum(); // 3
constexpr 생성자 제약
- 생성자 본문은 비어 있거나, 다른 constexpr 생성자/함수만 호출해야 함
- 모든 멤버를 constexpr로 초기화 가능해야 함
- 가상 상속 불가, 가상 함수 호출 불가 (C++20 이전)
struct Vec3 {
float x, y, z;
constexpr Vec3(float x, float y, float z) : x(x), y(y), z(z) {}
constexpr float lengthSq() const {
return x * x + y * y + z * z;
}
};
constexpr Vec3 v(1.0f, 0.0f, 0.0f);
constexpr float lenSq = v.lengthSq(); // 1.0
4. if constexpr 상세
기본 개념
if constexpr(조건)은 조건이 컴파일 타임 상수일 때, 참인 갈래만 인스턴스화됩니다. 선택되지 않은 갈래는 인스턴스화되지 않으므로, 그 갈래에서만 쓰는 타입/연산이 없어도 컴파일 에러가 나지 않습니다.
예제 1: 포인터 언래핑
#include <type_traits>
template <typename T>
auto unwrap(T x) {
if constexpr (std::is_pointer_v<T>)
return *x;
else
return x;
}
int main() {
int a = 42;
int* p = &a;
auto v1 = unwrap(p); // int (역참조)
auto v2 = unwrap(a); // int (그대로)
return 0;
}
예제 2: 타입별 직렬화
#include <string>
#include <type_traits>
template <typename T>
std::string to_string_impl(const T& x) {
if constexpr (std::is_same_v<T, std::string>)
return x;
else if constexpr (std::is_arithmetic_v<T>)
return std::to_string(x);
else
return "?";
}
예제 3: 포인터 vs 값 출력
#include <iostream>
#include <type_traits>
template <typename T>
void print(const T& x) {
if constexpr (std::is_pointer_v<T>)
std::cout << "ptr: " << *x << "\n";
else
std::cout << "val: " << x << "\n";
}
int main() {
int a = 10;
print(a); // val: 10
print(&a); // ptr: 10
return 0;
}
예제 4: enable_if 대체
std::enable_if 대신 if constexpr로 같은 의도를 더 읽기 쉽게 표현할 수 있습니다.
template <typename T>
auto safe_add(T a, T b) {
if constexpr (std::is_integral_v<T>)
return static_cast<long long>(a) + b; // 오버플로우 방지
else if constexpr (std::is_floating_point_v<T>)
return a + b;
else
return a + b; // 기타 타입
}
if constexpr vs 일반 if
| 구분 | if constexpr | 일반 if |
|---|---|---|
| 평가 시점 | 컴파일 타임 | 런타임 |
| 선택되지 않은 분기 | 인스턴스화 안 됨 | 인스턴스화됨 (문법 검사됨) |
| 사용처 | 템플릿, 타입 분기 | 런타임 조건 |
5. consteval (C++20)
constexpr vs consteval
constexpr 함수는 컴파일 타임과 런타임 모두에서 호출할 수 있습니다. consteval 함수는 반드시 컴파일 타임에만 평가되며, 런타임에 호출하면 컴파일 에러가 납니다.
// constexpr: 양쪽 모두 가능
constexpr int add(int a, int b) { return a + b; }
// consteval: 컴파일 타임 전용 (C++20)
consteval int mul(int a, int b) { return a * b; }
int main() {
constexpr int x = add(1, 2); // OK: 컴파일 타임
int i = 1, j = 2;
int y = add(i, j); // OK: 런타임
constexpr int z = mul(2, 3); // OK: 컴파일 타임
// int w = mul(i, j); // ❌ 컴파일 에러: mul은 런타임 호출 불가
return 0;
}
consteval 사용처
- 암호화 키 파생, 보안 상수 등 “절대 런타임에 노출되면 안 되는” 값
- 컴파일 타임 해시 등 반드시 상수로만 쓰여야 하는 값
consteval unsigned long long compile_time_hash(const char* str) {
unsigned long long hash = 14695981039346656037ULL;
while (*str) {
hash ^= static_cast<unsigned long long>(*str++);
hash *= 1099511628211ULL;
}
return hash;
}
constexpr auto HASH = compile_time_hash("secret_key"); // OK
// int x = rand();
// auto h = compile_time_hash("x"); // ❌ 런타임 문자열은 불가
constexpr vs consteval 비교 다이어그램
flowchart LR
subgraph constexpr["constexpr"]
C1[상수 인자] --> C2[컴파일 타임]
C3[변수 인자] --> C4[런타임]
end
subgraph consteval["consteval"]
E1[항상] --> E2[컴파일 타임만]
end
| 항목 | constexpr | consteval |
|---|---|---|
| 컴파일 타임 호출 | ✅ | ✅ |
| 런타임 호출 | ✅ | ❌ |
| 사용 목적 | 양쪽 모두 지원 | 컴파일 타임 강제 |
6. constexpr vs const 비교
핵심 차이
| 구분 | const | constexpr |
|---|---|---|
| 의미 | ”변경하지 않음" | "컴파일 타임에 계산 가능” |
| 초기화 시점 | 런타임 가능 | 반드시 컴파일 타임 |
| 상수 식 사용 | 불가 (런타임 초기화 시) | 가능 |
| 배열 크기 | 불가 (런타임 초기화 시) | 가능 |
구체적 예시
int getValue() { return 42; }
void example() {
const int a = 10; // OK: 리터럴로 초기화
const int b = getValue(); // OK: 런타임 초기화
int arr1[a]; // OK: a는 컴파일 타임 상수
// int arr2[b]; // ❌ 컴파일 에러! b는 런타임 값
constexpr int c = 10; // OK
// constexpr int d = getValue(); // ❌ 컴파일 에러! getValue()는 constexpr 아님
std::array<int, c> arr3; // OK
}
언제 무엇을 쓸까?
- 배열 크기, 템플릿 인자, switch 케이스 →
constexpr - 런타임에 한 번 설정 후 변경하지 않을 값 →
const - 컴파일 타임에 계산된 값을 여러 곳에서 쓸 때 →
constexpr
7. C++14/17/20 확장
C++11
- constexpr 함수에 단일 return만 허용
- 재귀만 가능, 루프 불가
C++14
- 여러 return, 루프, 지역 변수 허용
- constexpr 멤버 함수에서 멤버 수정 가능
void반환 타입 가능
C++17
if constexpr추가- 람다가 constexpr 가능 (조건 충족 시)
std::array,std::string_view등 constexpr 확장
C++20
- consteval 추가
- constexpr 안에서 동적 할당, try, 가상 호출 등 더 많은 기능 허용
std::vector,std::string등 많은 표준 라이브러리가 constexpr 지원
C++ 표준별 constexpr 함수 예시
// C++11 호환: 단일 return + 재귀만
constexpr int factorial_cpp11(int n) {
return n <= 1 ? 1 : n * factorial_cpp11(n - 1);
}
// C++14: 루프 가능
constexpr int factorial_cpp14(int n) {
int result = 1;
for (int i = 2; i <= n; ++i) result *= i;
return result;
}
8. 자주 발생하는 에러와 해결법
에러 1: 비리터럴 타입을 constexpr로 사용
원인: constexpr 변수/함수는 리터럴 타입만 다룰 수 있습니다. std::string, std::vector(C++20 이전) 등은 리터럴 타입이 아니었습니다.
// ❌ C++17 이하에서 컴파일 에러
// constexpr std::string msg = "hello";
// ✅ C++20에서는 std::string이 constexpr 지원
// ✅ C++17 이하: 고정 크기 배열 사용
constexpr char msg[] = "hello";
에러 2: 런타임 전용 연산 사용
원인: C++14 이하에서 constexpr 함수 안에 동적 할당, 예외, 가상 함수 호출 등을 쓸 수 없습니다.
// ❌ C++14에서 에러
// constexpr int bad() {
// int* p = new int(42); // 동적 할당 불가
// return *p;
// }
// ✅ 컴파일 타임에 허용된 연산만 사용
constexpr int good(int x) {
return x * 2;
}
에러 3: constexpr 함수에 비상수 인자 전달 후 상수 식에 사용
원인: 인자가 상수 식이 아니면 constexpr 함수도 런타임에 실행됩니다. 상수 식이 필요한 곳에 런타임 결과를 넣으면 에러가 납니다.
constexpr int add(int a, int b) { return a + b; }
int main() {
int x = 3, y = 5;
// constexpr int z = add(x, y); // ❌ 에러: x, y는 상수 식이 아님
int w = add(x, y); // ✅ OK: 런타임 호출
constexpr int v = add(3, 5); // ✅ OK: 상수 인자
return 0;
}
에러 4: 재귀 깊이 초과
원인: 컴파일 타임 재귀가 너무 깊으면 컴파일러 한계에 걸릴 수 있습니다.
// ❌ fib(40) 이상에서 일부 컴파일러 한계
// constexpr int fib_recursive(int n) {
// if (n <= 1) return n;
// return fib_recursive(n - 1) + fib_recursive(n - 2); // 지수적 재귀
// }
// ✅ 반복문: 깊이 제한 없음
constexpr int fib_safe(int n) {
if (n <= 1) return n;
int a = 0, b = 1;
for (int i = 2; i <= n; ++i) {
int next = a + b;
a = b;
b = next;
}
return b;
}
에러 5: static_assert 조건에 런타임 값 사용
원인: static_assert의 조건은 반드시 컴파일 타임 상수여야 합니다.
// ❌ 컴파일 에러
// void process(int max_size) {
// static_assert(max_size > 0, "max_size must be positive");
// }
// ✅ constexpr 또는 템플릿 사용
template <int MaxSize>
void process() {
static_assert(MaxSize > 0, "max_size must be positive");
}
constexpr bool validate_size(int n) { return n > 0; }
static_assert(validate_size(100), "");
에러 6: std::array 크기에 constexpr 아닌 식 사용
원인: std::array<T, N>의 N은 반드시 컴파일 타임 상수여야 합니다.
// ❌ 컴파일 에러
// int get_size() { return 128; }
// std::array<int, get_size()> arr;
// ✅ constexpr 함수 사용
constexpr int get_size() { return 128; }
std::array<int, get_size()> arr; // OK
에러 7: constexpr 함수에서 I/O 또는 전역 상태 접근
원인: constexpr 평가 중에는 std::cout, 파일 입출력, 전역 변수 수정 등이 불가능합니다.
// ❌ 컴파일 에러
// constexpr int bad() {
// std::cout << "hello"; // I/O 불가
// return 42;
// }
// ❌ 전역 상태 수정 불가
// int global = 0;
// constexpr int also_bad() {
// global = 1; // 부수 효과 불가
// return 42;
// }
// ✅ 순수 계산만
constexpr int good(int x) {
return x * 2;
}
에러 8: consteval에 런타임 인자 전달
원인: consteval 함수는 리터럴 또는 constexpr 변수만 인자로 받을 수 있습니다.
consteval int mul(int a, int b) { return a * b; }
int main() {
constexpr int m = 4;
constexpr int z = mul(m, 5); // OK
// int x = rand();
// int y = mul(x, 2); // ❌ 컴파일 에러
return 0;
}
에러 9: if constexpr에서 선택되지 않은 분기의 문법 오류
원인: if constexpr의 false 분기에서도 문법 검사는 됩니다. 타입에 의존하는 표현식이 잘못되면 컴파일 에러가 납니다.
template <typename T>
auto value_or_zero(const T& x) {
if constexpr (std::is_pointer_v<T>)
return x ? *x : 0;
else
return x;
}
// T가 int일 때: *x는 문법 오류이지만, false 분기이므로 인스턴스화되지 않음
// 단, 선택되지 않은 분기에서도 문법적으로 유효해야 함 (C++17)
에러 10: 컴파일러별 constexpr 한계 차이
원인: GCC, Clang, MSVC 간에 constexpr 평가 한계, 재귀 깊이, std::sin 등 수학 함수 constexpr 지원이 다릅니다.
| 항목 | GCC | Clang | MSVC |
|---|---|---|---|
| constexpr 재귀 기본 한계 | ~512 | ~512 | ~500 |
| std::sin constexpr | 확장 | 확장 | 제한적 |
| constexpr new (C++20) | 지원 | 지원 | 부분 |
해결: -fconstexpr-depth=N으로 조정 가능하지만, 반복문으로 전환하는 것이 이식성이 좋습니다.
9. 베스트 프랙티스
1. 상수 식이 필요한 곳에서는 constexpr 사용
배열 크기, 템플릿 인자, static_assert, switch 케이스 등에는 constexpr 변수 또는 함수 결과를 사용합니다.
constexpr size_t BUF_SIZE = 4096;
std::array<char, BUF_SIZE> buffer;
constexpr int MAX = 100;
static_assert(MAX > 0, "MAX must be positive");
2. 재귀 대신 반복문 선호
컴파일러 재귀 한계를 피하고 이식성을 높이려면 반복문을 사용합니다.
// ✅ 권장: 반복문
constexpr int factorial(int n) {
int result = 1;
for (int i = 2; i <= n; ++i) result *= i;
return result;
}
3. static_assert로 constexpr 검증
설정값이 유효한 범위인지 컴파일 타임에 검증합니다.
constexpr int MAX_CONNECTIONS = 1024;
constexpr bool valid_connections(int n) {
return n >= 1 && n <= MAX_CONNECTIONS;
}
static_assert(valid_connections(MAX_CONNECTIONS), "MAX_CONNECTIONS must be valid");
4. 룩업 테이블은 std::make_index_sequence 활용
256개, 1024개 같은 고정 크기 테이블은 std::make_index_sequence와 함께 constexpr로 생성합니다.
template <size_t... Is>
constexpr auto make_table(std::index_sequence<Is...>) {
return std::array<int, sizeof...(Is)>{{compute(Is)...}};
}
constexpr auto TABLE = make_table(std::make_index_sequence<256>{});
5. enable_if 대신 if constexpr
C++17 이상에서는 if constexpr가 더 읽기 쉽습니다.
template <typename T>
auto process(T x) {
if constexpr (std::is_pointer_v<T>)
return *x;
else
return x;
}
6. 컴파일 타임 전용이면 consteval
“반드시 컴파일 타임에만” 평가되어야 하는 값은 consteval로 강제합니다.
consteval int secret_hash(const char* s) {
// ... 해시 계산
return hash;
}
7. 매크로 대신 constexpr
타입 검사, 네임스페이스, 디버깅이 가능하므로 매크로 대신 constexpr를 사용합니다.
// ❌ 매크로
// #define MAX_SIZE 1024
// ✅ constexpr
constexpr size_t MAX_SIZE = 1024;
10. 성능 벤치마크
컴파일 타임 vs 런타임 계산
constexpr로 컴파일 타임에 계산하면 런타임 비용이 0입니다.
#include <chrono>
#include <iostream>
constexpr unsigned long long factorialConstexpr(int n) {
return n <= 1 ? 1 : n * factorialConstexpr(n - 1);
}
unsigned long long factorialRuntime(int n) {
return n <= 1 ? 1 : n * factorialRuntime(n - 1);
}
int main() {
constexpr int N = 15;
constexpr auto precomputed = factorialConstexpr(N);
// 런타임 계산 100만 번
auto start = std::chrono::high_resolution_clock::now();
volatile unsigned long long sum = 0;
for (int i = 0; i < 1'000'000; ++i)
sum += factorialRuntime(N);
auto end = std::chrono::high_resolution_clock::now();
auto runtime_ms = std::chrono::duration<double, std::milli>(end - start).count();
// 컴파일 타임 결과 사용 (100만 번 접근)
start = std::chrono::high_resolution_clock::now();
sum = 0;
for (int i = 0; i < 1'000'000; ++i)
sum += precomputed;
end = std::chrono::high_resolution_clock::now();
auto constexpr_ms = std::chrono::duration<double, std::milli>(end - start).count();
std::cout << "Runtime factorial(15) x 1M: " << runtime_ms << " ms\n";
std::cout << "Constexpr result x 1M: " << constexpr_ms << " ms\n";
return 0;
}
예상 결과 (환경에 따라 다름):
| 방식 | 시간 (대략) |
|---|---|
| 런타임 factorial(15) x 1,000,000 | ~50–200 ms |
| constexpr 결과 사용 x 1,000,000 | ~1–5 ms |
CRC32 테이블: 런타임 초기화 vs constexpr
| 방식 | 초기화 비용 | 런타임 CRC 계산 |
|---|---|---|
| 런타임 테이블 | 부팅 시 μs 단위 | 동일 |
| constexpr 테이블 | 0 (컴파일 타임) | 동일 |
constexpr 테이블은 .rodata에 상수로 들어가므로 초기화 비용이 없습니다.
11. 프로덕션 패턴
패턴 1: 컴파일 타임 룩업 테이블
CRC32, 해시, 인코딩 테이블 등을 컴파일 타임에 생성합니다.
constexpr int parity(uint8_t n) {
int p = 0;
while (n) {
p ^= (n & 1);
n >>= 1;
}
return p;
}
template <size_t... Is>
constexpr auto makeParityTable(std::index_sequence<Is...>) {
return std::array<int, sizeof...(Is)>{{parity(static_cast<uint8_t>(Is))...}};
}
constexpr auto PARITY_TABLE = makeParityTable(std::make_index_sequence<256>{});
int p = PARITY_TABLE[42]; // 런타임 비용: 배열 접근 1번
패턴 2: 설정값 컴파일 타임 파싱
버퍼 크기, 타임아웃 등을 문자열 리터럴에서 컴파일 타임에 파싱할 수 있습니다.
constexpr int parseSize(const char* s) {
int result = 0;
while (*s >= '0' && *s <= '9') {
result = result * 10 + (*s - '0');
++s;
}
return result;
}
constexpr int BUF_SIZE = parseSize("4096"); // 4096
std::array<char, BUF_SIZE> buffer;
패턴 3: 타입별 상수
템플릿과 constexpr를 조합해 타입에 따라 다른 상수를 사용합니다.
template <typename T>
struct TypeTraits;
template <>
struct TypeTraits<int> {
static constexpr size_t max_digits = 10;
};
template <>
struct TypeTraits<long long> {
static constexpr size_t max_digits = 19;
};
template <typename T>
std::array<char, TypeTraits<T>::max_digits + 1> to_string_buffer;
패턴 4: 프로토콜 버퍼 크기 조합
헤더 + 페이로드 크기를 컴파일 타임에 조합해 패킷 버퍼를 정의합니다.
constexpr size_t PACKET_HEADER_SIZE = 16;
constexpr size_t MAX_PAYLOAD_SIZE = 4096;
constexpr size_t PACKET_BUFFER_SIZE = PACKET_HEADER_SIZE + MAX_PAYLOAD_SIZE;
std::array<uint8_t, PACKET_BUFFER_SIZE> packet_buffer;
패턴 5: consteval로 강제 컴파일 타임 평가 (C++20)
consteval int must_be_compile_time(int x) {
return x * 2;
}
constexpr int a = must_be_compile_time(21); // OK: 42
패턴 6: static_assert와 constexpr 검증
constexpr int MAX_CONNECTIONS = 1024;
constexpr int MIN_CONNECTIONS = 1;
constexpr bool valid_connections(int n) {
return n >= MIN_CONNECTIONS && n <= MAX_CONNECTIONS;
}
static_assert(valid_connections(MAX_CONNECTIONS), "MAX_CONNECTIONS must be valid");
static_assert(valid_connections(512), "512 should be valid");
패턴 7: 컴파일 타임 타입 ID
template <typename T>
struct type_id {
static constexpr const char* name = __PRETTY_FUNCTION__; // GCC/Clang
};
// type_id<int>::name != type_id<double>::name
constexpr 적용 체크리스트
실무에서 constexpr를 도입할 때 확인할 항목입니다.
- 배열/버퍼 크기가 컴파일 시점에 고정인가? →
constexpr변수 또는 함수 사용 - 룩업 테이블(CRC, 해시, 인코딩)이 고정인가? → constexpr +
std::make_index_sequence패턴 - 설정값이 문자열 리터럴에서 파싱 가능한가? →
parseSize("4096")같은 constexpr 파서 - C++ 표준 버전이 C++14 이상인가? (루프·여러 문 사용 시)
- 재귀 깊이가 컴파일러 한계를 넘지 않는가? → 반복문으로 전환 검토
- static_assert로 상수 검증이 필요한가? → constexpr 조건 함수 활용
- 모든 코드 블록에 언어 태그(
cpp)가 있는가?
정리
| 항목 | 내용 |
|---|---|
| constexpr 변수 | 컴파일 타임 상수 |
| constexpr 함수 | 상수 식에서 호출 가능 (인자가 상수면 컴파일 타임) |
| constexpr 생성자 | 사용자 정의 타입을 리터럴 타입으로 만듦 |
| if constexpr | 컴파일 타임 분기, 선택되지 않은 갈래는 인스턴스화 안 됨 |
| consteval | 반드시 컴파일 타임에만 평가 (C++20) |
| const vs constexpr | const는 “불변”, constexpr은 “컴파일 타임 계산 가능” |
| C++14 | 루프·여러 문장 허용 |
| C++20 | 동적 할당, consteval 등 더 많은 것 허용 |
| 프로덕션 패턴 | 룩업 테이블, 설정 파싱, 타입별 상수 |
같이 보면 좋은 글 (내부 링크)
이 주제와 연결되는 다른 글입니다.
- C++ 컴파일 타임 프로그래밍 기법 | 런타임 오버헤드 제거와 constexpr·consteval 실전
- C++ Type Traits 완벽 가이드 | std::is_integral·std::enable_if
- C++ 컴파일 타임 최적화 | constexpr·PCH·모듈·ccache·Unity 빌드 [#15-3]
이 글에서 다루는 키워드 (관련 검색어)
C++ constexpr, 컴파일 타임 상수, constexpr 함수, constexpr vs const, if constexpr, consteval, 리터럴 타입 등으로 검색하시면 이 글이 도움이 됩니다.
한 줄 요약: constexpr로 상수·배열 크기·템플릿 인자를 컴파일 타임에 계산할 수 있습니다. if constexpr로 타입 분기를, consteval로 컴파일 타임 전용 평가를 강제할 수 있습니다.
자주 묻는 질문 (FAQ)
Q. 이 내용을 실무에서 언제 쓰나요?
A. C++ constexpr 함수·변수·if constexpr·consteval 기초부터 실전까지. 문제 시나리오, 완전한 예제, 자주 발생하는 에러, 베스트 프랙티스, 프로덕션 패턴. 실무에서는 위 본문의 예제와 선택 가이드를 참고해 적용하면 됩니다.
Q. 선행으로 읽으면 좋은 글은?
A. 각 글 하단의 이전 글 링크를 따라가면 순서대로 배울 수 있습니다. C++ 시리즈 목차에서 전체 흐름을 확인할 수 있습니다.
Q. 더 깊이 공부하려면?
A. cppreference와 해당 라이브러리 공식 문서를 참고하세요. 글 말미의 참고 자료 링크도 활용하면 좋습니다.
다음 글: C++ 컴파일 타임 프로그래밍 기법 (#26-2)
관련 글
- C++ constexpr 고급 가이드 | constexpr 컨테이너·알고리즘·문자열·new/delete 실전
- C++ 고성능 RPC 시스템: gRPC와 Protocol Buffers를 이용한 마이크로서비스 구축
- C++ 보안 코딩 가이드: 오버플로우 방지와 암호화 라이브러리(OpenSSL) 실전 연동 [#43-2]
- C++ Observability: Prometheus와 Grafana로 C++ 서버 모니터링 구축하기
- C++ constexpr 함수와 변수 | 컴파일 타임에 계산하기 [#26-1]