C++ constexpr if | "컴파일 타임 분기" 가이드
이 글의 핵심
C++17 if constexpr은 템플릿 안에서 컴파일 타임에만 평가되는 조건문입니다. constexpr 함수·상수 초기화와 함께 쓰이고, type_traits로 분기할 때 템플릿 특수화 대신 한 함수에서 처리할 수 있습니다.
들어가며
C++17 if constexpr은 템플릿 안에서 컴파일 타임에만 평가되는 조건문입니다. type_traits로 분기할 때 템플릿 특수화 대신 한 함수에서 처리할 수 있습니다.
#include <iostream>
#include <type_traits>
// 템플릿 함수: 모든 타입 T를 받을 수 있음
template<typename T>
void process(T value) {
// if constexpr: 컴파일 타임에 조건 평가
// 선택된 분기만 코드로 생성됨 (나머지는 제거)
// std::is_integral_v<T>: T가 정수 타입인지 확인
// (int, long, short, char 등)
if constexpr (std::is_integral_v<T>) {
std::cout << "정수: " << value << std::endl;
}
// std::is_floating_point_v<T>: T가 실수 타입인지 확인
// (float, double, long double)
else if constexpr (std::is_floating_point_v<T>) {
std::cout << "실수: " << value << std::endl;
}
// 그 외 타입 (string, 포인터 등)
else {
std::cout << "기타: " << value << std::endl;
}
}
int main() {
process(42); // T=int → 첫 번째 분기만 컴파일
process(3.14); // T=double → 두 번째 분기만 컴파일
process("hello"); // T=const char* → 세 번째 분기만 컴파일
}
1. 일반 if vs constexpr if
비교표
| 구분 | 일반 if | constexpr if |
|---|---|---|
| 평가 시점 | 런타임 | 컴파일 타임 |
| 조건 | 런타임 값 | 컴파일 타임 상수 |
| 코드 생성 | 모든 분기 생성 | 선택된 분기만 생성 |
| 타입 검사 | 모든 분기 검사 | 선택된 분기만 검사 |
| 최적화 | 컴파일러 의존 | 보장됨 |
| 사용 위치 | 어디서나 | 주로 템플릿 |
코드 생성 차이
#include <iostream>
#include <type_traits>
// 일반 if: 런타임 평가
template<typename T>
void func1(T value) {
// 일반 if: 런타임에 조건 평가
// 문제: 모든 분기가 컴파일되어야 함
if (std::is_integral_v<T>) { // 런타임
// value++; // ❌ 컴파일 에러!
// T가 string이면 value++는 유효하지 않은 코드
// 실행 안되더라도 컴파일은 되어야 함
std::cout << "정수" << std::endl;
}
}
// constexpr if: 컴파일 타임 평가
template<typename T>
void func2(T value) {
// if constexpr: 컴파일 타임에 조건 평가
// 선택된 분기만 컴파일됨
if constexpr (std::is_integral_v<T>) { // 컴파일 타임
// T가 정수면 이 코드만 컴파일
value++; // ✅ OK (T가 string이면 이 코드 자체가 제거됨)
std::cout << "정수: " << value << std::endl;
} else {
// T가 정수가 아니면 이 코드만 컴파일
std::cout << "정수 아님" << std::endl;
}
}
int main() {
func1(42); // 런타임 분기
func1(std::string("test")); // 런타임 분기
func2(42); // 컴파일 타임: 첫 번째 분기만 생성
func2(std::string("test")); // 컴파일 타임: 두 번째 분기만 생성
return 0;
}
출력:
정수
정수: 43
정수 아님
2. 템플릿 특수화 대체
구현 방식 비교
| 항목 | 템플릿 특수화 | constexpr if |
|---|---|---|
| 코드 줄 수 | 많음 (각 타입별 함수) | 적음 (한 함수) |
| 유지보수 | 어려움 | 쉬움 |
| 가독성 | 분산됨 | 집중됨 |
| 컴파일 시간 | 느림 | 빠름 |
| 디버깅 | 어려움 | 쉬움 |
#include <iostream>
#include <type_traits>
// ❌ 템플릿 특수화 (복잡)
template<typename T>
void print(T value);
template<>
void print<int>(int value) {
std::cout << "int: " << value << std::endl;
}
template<>
void print<double>(double value) {
std::cout << "double: " << value << std::endl;
}
template<>
void print<const char*>(const char* value) {
std::cout << "string: " << value << std::endl;
}
// ✅ constexpr if (간단)
template<typename T>
void printModern(T value) {
if constexpr (std::is_same_v<T, int>) {
std::cout << "int: " << value << std::endl;
} else if constexpr (std::is_same_v<T, double>) {
std::cout << "double: " << value << std::endl;
} else if constexpr (std::is_same_v<T, const char*>) {
std::cout << "string: " << value << std::endl;
} else {
std::cout << "other: " << value << std::endl;
}
}
int main() {
std::cout << "=== 템플릿 특수화 ===" << std::endl;
print(42);
print(3.14);
print("hello");
std::cout << "\n=== constexpr if ===" << std::endl;
printModern(42);
printModern(3.14);
printModern("world");
return 0;
}
출력:
=== 템플릿 특수화 ===
int: 42
double: 3.14
string: hello
=== constexpr if ===
int: 42
double: 3.14
string: world
3. 실전 예제
예제 1: 타입별 처리
#include <type_traits>
#include <vector>
#include <iostream>
#include <string>
template<typename T>
size_t getSize(const T& container) {
if constexpr (std::is_array_v<T>) {
return std::extent_v<T>; // 배열 크기
} else if constexpr (requires { container.size(); }) {
return container.size(); // 컨테이너 크기
} else {
return 1; // 단일 값
}
}
int main() {
int arr[10];
std::vector<int> vec = {1, 2, 3};
int x = 42;
std::cout << "배열 크기: " << getSize(arr) << std::endl; // 10
std::cout << "벡터 크기: " << getSize(vec) << std::endl; // 3
std::cout << "단일 값: " << getSize(x) << std::endl; // 1
return 0;
}
출력:
배열 크기: 10
벡터 크기: 3
단일 값: 1
예제 2: 직렬화
#include <sstream>
#include <iostream>
#include <string>
#include <vector>
#include <type_traits>
template<typename T>
std::string serialize(const T& value) {
std::ostringstream oss;
if constexpr (std::is_arithmetic_v<T>) {
oss << value;
} else if constexpr (std::is_same_v<T, std::string>) {
oss << "\"" << value << "\"";
} else if constexpr (requires { value.begin(); value.end(); }) {
oss << "[";
bool first = true;
for (const auto& item : value) {
if (!first) oss << ", ";
oss << serialize(item);
first = false;
}
oss << "]";
} else {
oss << "unknown";
}
return oss.str();
}
int main() {
std::cout << serialize(42) << std::endl; // 42
std::cout << serialize(3.14) << std::endl; // 3.14
std::cout << serialize(std::string("hello")) << std::endl; // "hello"
std::vector<int> vec = {1, 2, 3};
std::cout << serialize(vec) << std::endl; // [1, 2, 3]
std::vector<std::string> strs = {"a", "b", "c"};
std::cout << serialize(strs) << std::endl; // ["a", "b", "c"]
return 0;
}
출력:
42
3.14
"hello"
[1, 2, 3]
["a", "b", "c"]
예제 3: 최적화된 복사
#include <iostream>
#include <type_traits>
#include <cstring>
#include <vector>
#include <string>
template<typename T>
void copy(T* dest, const T* src, size_t n) {
if constexpr (std::is_trivially_copyable_v<T>) {
// POD 타입: memcpy 사용 (빠름)
std::memcpy(dest, src, n * sizeof(T));
std::cout << "memcpy 사용 (빠름)" << std::endl;
} else {
// 복잡한 타입: 개별 복사
for (size_t i = 0; i < n; i++) {
dest[i] = src[i];
}
std::cout << "개별 복사 (안전)" << std::endl;
}
}
struct Simple {
int x, y;
};
struct Complex {
std::string name;
std::vector<int> data;
};
int main() {
Simple s1[10], s2[10];
for (int i = 0; i < 10; ++i) {
s1[i] = {i, i * 2};
}
copy(s2, s1, 10); // memcpy 사용
std::cout << "s2[5]: {" << s2[5].x << ", " << s2[5].y << "}" << std::endl;
Complex c1[10], c2[10];
for (int i = 0; i < 10; ++i) {
c1[i] = {"name" + std::to_string(i), {i, i+1, i+2}};
}
copy(c2, c1, 10); // 개별 복사
std::cout << "c2[5]: " << c2[5].name << std::endl;
return 0;
}
출력:
memcpy 사용 (빠름)
s2[5]: {5, 10}
개별 복사 (안전)
c2[5]: name5
4. 자주 발생하는 문제
문제 1: 잘못된 조건
#include <iostream>
int main() {
// ❌ 런타임 변수 사용
bool flag = true;
// if constexpr (flag) { // 컴파일 에러
// std::cout << "true" << std::endl;
// }
// ✅ constexpr 변수 사용
constexpr bool flag2 = true;
if constexpr (flag2) { // OK
std::cout << "true" << std::endl;
}
return 0;
}
문제 2: 타입 체크 누락
#include <iostream>
#include <vector>
// ❌ 타입 체크 없이 멤버 접근
template<typename T>
void bad(T value) {
// if constexpr (true) {
// value.size(); // T가 size()가 없으면 에러
// }
}
// ✅ 타입 체크 후 접근
template<typename T>
void good(T value) {
if constexpr (requires { value.size(); }) {
std::cout << "크기: " << value.size() << std::endl;
} else {
std::cout << "크기 없음" << std::endl;
}
}
int main() {
std::vector<int> v = {1, 2, 3};
good(v); // 크기: 3
good(42); // 크기 없음
return 0;
}
출력:
크기: 3
크기 없음
문제 3: else 분기 누락
#include <iostream>
#include <type_traits>
#include <string>
// ❌ else 없음
template<typename T>
void bad(T value) {
if constexpr (std::is_integral_v<T>) {
value++;
std::cout << "정수: " << value << std::endl;
}
// T가 정수가 아니면?
}
// ✅ else 분기 추가
template<typename T>
void good(T value) {
if constexpr (std::is_integral_v<T>) {
value++;
std::cout << "정수: " << value << std::endl;
} else {
std::cout << "정수 아님" << std::endl;
}
}
int main() {
good(42);
good(std::string("test"));
return 0;
}
출력:
정수: 43
정수 아님
5. constexpr if vs SFINAE
#include <iostream>
#include <type_traits>
// SFINAE (복잡)
template<typename T>
std::enable_if_t<std::is_integral_v<T>, void>
funcSFINAE(T value) {
value++;
std::cout << "SFINAE 정수: " << value << std::endl;
}
template<typename T>
std::enable_if_t<!std::is_integral_v<T>, void>
funcSFINAE(T value) {
std::cout << "SFINAE 정수 아님" << std::endl;
}
// constexpr if (간단)
template<typename T>
void funcConstexpr(T value) {
if constexpr (std::is_integral_v<T>) {
value++;
std::cout << "constexpr if 정수: " << value << std::endl;
} else {
std::cout << "constexpr if 정수 아님" << std::endl;
}
}
int main() {
std::cout << "=== SFINAE ===" << std::endl;
funcSFINAE(42);
funcSFINAE(3.14);
std::cout << "\n=== constexpr if ===" << std::endl;
funcConstexpr(42);
funcConstexpr(3.14);
return 0;
}
출력:
=== SFINAE ===
SFINAE 정수: 43
SFINAE 정수 아님
=== constexpr if ===
constexpr if 정수: 43
constexpr if 정수 아님
6. 실전 예제: 디버그 로깅
#include <iostream>
#include <string>
constexpr bool DEBUG = true;
template<typename... Args>
void log(Args&&... args) {
if constexpr (DEBUG) {
(std::cout << ... << args) << std::endl;
}
// DEBUG가 false면 코드 완전히 제거
}
void processData(int id, const std::string& data) {
log("처리 시작: id=", id, ", data=", data);
// 데이터 처리 로직
log("처리 완료: id=", id);
}
int main() {
processData(1, "test data");
processData(2, "another data");
return 0;
}
출력 (DEBUG = true):
처리 시작: id=1, data=test data
처리 완료: id=1
처리 시작: id=2, data=another data
처리 완료: id=2
출력 (DEBUG = false):
(출력 없음, 로그 코드 완전히 제거)
7. 중첩 constexpr if
#include <iostream>
#include <type_traits>
template<typename T>
void process(T value) {
if constexpr (std::is_pointer_v<T>) {
using ElementType = std::remove_pointer_t<T>;
if constexpr (std::is_const_v<ElementType>) {
std::cout << "const 포인터" << std::endl;
} else {
std::cout << "일반 포인터" << std::endl;
}
if (value) {
std::cout << "값: " << *value << std::endl;
}
} else {
std::cout << "포인터 아님: " << value << std::endl;
}
}
int main() {
int x = 42;
const int y = 100;
process(&x); // 일반 포인터, 값: 42
process(&y); // const 포인터, 값: 100
process(x); // 포인터 아님: 42
return 0;
}
출력:
일반 포인터
값: 42
const 포인터
값: 100
포인터 아님: 42
정리
핵심 요약
- constexpr if: 컴파일 타임 조건문
- 코드 제거: 선택된 분기만 생성
- 템플릿 특수화 대체: 더 간결
- type_traits: 타입별 분기
- 성능: 런타임 오버헤드 없음
일반 if vs constexpr if
| 특징 | 일반 if | constexpr if |
|---|---|---|
| 평가 | 런타임 | 컴파일 타임 |
| 조건 | 변수 | constexpr |
| 코드 | 모든 분기 | 선택 분기만 |
| 최적화 | 컴파일러 의존 | 보장 |
실전 팁
사용 원칙:
- 템플릿 타입별 처리
- 컴파일 타임 최적화
- 템플릿 특수화 대체
- 디버그 로깅
성능:
- 불필요한 코드 제거
- 바이너리 크기 감소
- 런타임 오버헤드 없음
- 컴파일 타임 증가 (미미)
주의사항:
- constexpr 조건만 가능
- 타입 체크 필수
- else 분기 고려
- 중첩 사용 주의
다음 단계
- C++ constexpr Function
- C++ Type Traits
- C++ Template Basics
같이 보면 좋은 글 (내부 링크)
이 주제와 연결되는 다른 글입니다.
- C++ constexpr 함수 | “컴파일 타임 함수” 가이드
- C++ 템플릿 | “제네릭 프로그래밍” 초보자 가이드
- C++ Constant Initialization | “상수 초기화” 가이드
- C++ Type Traits | “타입 특성” 완벽 가이드
관련 글
- C++ 컴파일 타임 프로그래밍 기법 | 런타임 오버헤드 제거와 constexpr·consteval 실전
- C++ constexpr 완벽 가이드 | 컴파일 타임 계산·if constexpr·consteval 실전
- C++ 컴파일 타임 프로그래밍 |
- C++ constexpr 함수 |
- C++ 컴파일 타임 최적화 | constexpr·PCH·모듈·ccache·Unity 빌드 [#15-3]