C++ constexpr if | "컴파일 타임 분기" 가이드

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

비교표

구분일반 ifconstexpr 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

정리

핵심 요약

  1. constexpr if: 컴파일 타임 조건문
  2. 코드 제거: 선택된 분기만 생성
  3. 템플릿 특수화 대체: 더 간결
  4. type_traits: 타입별 분기
  5. 성능: 런타임 오버헤드 없음

일반 if vs constexpr if

특징일반 ifconstexpr 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]