C++20 consteval 완벽 가이드 | 컴파일 타임 전용 함수

C++20 consteval 완벽 가이드 | 컴파일 타임 전용 함수

이 글의 핵심

C++20 consteval 완벽 가이드에 대한 실전 가이드입니다. 컴파일 타임 전용 함수 등을 예제와 함께 상세히 설명합니다.

consteval이란? 왜 필요한가

문제 시나리오: constexpr의 모호함

문제: constexpr 함수는 컴파일 타임에도, 런타임에도 실행될 수 있습니다. 컴파일 타임 계산을 강제하고 싶을 때 모호합니다.

constexpr int factorial(int n) {
    return n <= 1 ? 1 : n * factorial(n - 1);
}

int main() {
    constexpr int x = factorial(5);  // 컴파일 타임: 120
    
    int n = 10;
    int y = factorial(n);  // 런타임 (의도하지 않았을 수도)
}

해결: consteval컴파일 타임 전용 함수입니다. 런타임 값을 넘기면 컴파일 에러가 나므로, 컴파일 타임 계산을 보장할 수 있습니다.

consteval int factorial(int n) {
    return n <= 1 ? 1 : n * factorial(n - 1);
}

int main() {
    constexpr int x = factorial(5);  // OK: 120
    
    int n = 10;
    // int y = factorial(n);  // Error: n은 런타임 값
}
flowchart TD
    subgraph constexpr["constexpr"]
        ce1["컴파일 타임 OK"]
        ce2["런타임 OK"]
    end
    subgraph consteval["consteval"]
        cv1["컴파일 타임 OK"]
        cv2["런타임 Error"]
    end

목차

  1. constexpr vs consteval
  2. 즉시 함수 (Immediate Function)
  3. 실전 활용: 컴파일 타임 검증
  4. 메타프로그래밍
  5. 자주 발생하는 문제와 해결법
  6. 프로덕션 패턴
  7. 완전한 예제: 컴파일 타임 설정 시스템

1. constexpr vs consteval

비교

항목constexprconsteval
컴파일 타임 실행가능필수
런타임 실행가능불가
용도유연한 계산컴파일 타임 보장
에러런타임 값 OK런타임 값 Error

예시

// constexpr: 둘 다 가능
constexpr int add(int a, int b) {
    return a + b;
}

int main() {
    constexpr int x = add(1, 2);  // 컴파일 타임
    
    int a = 5;
    int y = add(a, 10);  // 런타임 (OK)
}

// consteval: 컴파일 타임만
consteval int multiply(int a, int b) {
    return a * b;
}

int main() {
    constexpr int x = multiply(2, 3);  // OK
    
    int a = 5;
    // int y = multiply(a, 10);  // Error: a는 런타임 값
}

2. 즉시 함수

즉시 함수란

consteval 함수는 즉시 함수라고 하며, 호출 시점에 즉시 평가되어야 합니다.

consteval int square(int x) {
    return x * x;
}

// consteval 함수는 consteval 내에서만 호출 가능
consteval int sum_of_squares(int a, int b) {
    return square(a) + square(b);  // OK
}

// constexpr에서는 호출 불가
constexpr int wrapper(int x) {
    // return square(x);  // Error: consteval in constexpr
    return x * 2;
}

int main() {
    constexpr int result = sum_of_squares(3, 4);  // 25
}

3. 실전 활용: 컴파일 타임 검증

범위 검증

consteval int check_range(int value, int min, int max) {
    if (value < min || value > max) {
        throw "Value out of range";
    }
    return value;
}

int main() {
    constexpr int size = check_range(100, 1, 1024);  // OK
    int buffer[size];
    
    // constexpr int bad = check_range(2000, 1, 1024);  // Compile error
}

문자열 해시

#include <string_view>

consteval unsigned int hash(std::string_view str) {
    unsigned int hash = 5381;
    for (char c : str) {
        hash = ((hash << 5) + hash) + static_cast<unsigned char>(c);
    }
    return hash;
}

enum class MessageType : unsigned int {
    Login = hash("login"),
    Logout = hash("logout"),
    Data = hash("data")
};

void handle_message(const std::string& type) {
    switch (hash(type.c_str())) {  // 컴파일 타임 해시
    case hash("login"):
        std::cout << "Login\n";
        break;
    case hash("logout"):
        std::cout << "Logout\n";
        break;
    case hash("data"):
        std::cout << "Data\n";
        break;
    }
}

타입 크기 검증

template<typename T>
consteval bool is_small_type() {
    return sizeof(T) <= 16;
}

template<typename T>
    requires is_small_type<T>()
void process(T value) {
    // T는 16바이트 이하 보장
}

int main() {
    process(42);        // OK: sizeof(int) = 4
    process(3.14);      // OK: sizeof(double) = 8
    // process(std::string{"hello"});  // Error: sizeof(string) > 16
}

4. 메타프로그래밍

컴파일 타임 팩토리얼

consteval int factorial(int n) {
    if (n < 0) throw "Negative factorial";
    return n <= 1 ? 1 : n * factorial(n - 1);
}

int main() {
    constexpr int f5 = factorial(5);   // 120
    constexpr int f10 = factorial(10); // 3628800
    
    // 배열 크기로 사용
    int buffer[factorial(4)];  // 24 elements
}

컴파일 타임 문자열 처리

#include <array>
#include <string_view>

consteval std::size_t count_chars(std::string_view str, char c) {
    std::size_t count = 0;
    for (char ch : str) {
        if (ch == c) ++count;
    }
    return count;
}

int main() {
    constexpr auto count = count_chars("hello world", 'l');
    static_assert(count == 3);
    
    std::array<char, count> buffer;  // 3 elements
}

컴파일 타임 설정

consteval int get_buffer_size() {
    #ifdef LARGE_BUFFER
        return 1024 * 1024;  // 1MB
    #else
        return 4096;         // 4KB
    #endif
}

consteval int get_thread_count() {
    return 8;  // 컴파일 타임 상수
}

int main() {
    constexpr int buffer_size = get_buffer_size();
    constexpr int threads = get_thread_count();
    
    char buffer[buffer_size];
    std::array<std::thread, threads> thread_pool;
}

5. 자주 발생하는 문제와 해결법

문제 1: 런타임 값 전달

증상: error: call to consteval function is not a constant expression.

consteval int square(int x) {
    return x * x;
}

int main() {
    int a = 5;
    // int b = square(a);  // Error: a는 런타임 값
    
    // ✅ 해결법 1: constexpr 변수
    constexpr int c = 5;
    int d = square(c);  // OK
    
    // ✅ 해결법 2: 리터럴
    int e = square(5);  // OK
}

문제 2: 부작용

증상: error: call to non-constexpr function.

원인: consteval 함수 안에서 I/O, 동적 할당 등 부작용이 있는 함수 호출.

// ❌ 잘못된 사용
consteval int bad() {
    std::cout << "Hello\n";  // Error: I/O
    return 42;
}

// ✅ 올바른 사용: 순수 계산만
consteval int good(int x) {
    return x * 2;
}

문제 3: constexpr에서 consteval 호출

원인: constexpr 함수는 런타임에도 실행될 수 있으므로, consteval 함수를 호출할 수 없습니다.

consteval int immediate() {
    return 42;
}

// ❌ 잘못된 사용
constexpr int wrapper() {
    return immediate();  // Error
}

// ✅ 올바른 사용: consteval에서 호출
consteval int wrapper2() {
    return immediate();  // OK
}

// ✅ 또는 constexpr if로 분기
constexpr int wrapper3(int x) {
    if (std::is_constant_evaluated()) {
        return 42;  // 컴파일 타임
    } else {
        return x * 2;  // 런타임
    }
}

6. 프로덕션 패턴

패턴 1: 컴파일 타임 문자열 검증

consteval bool is_valid_identifier(std::string_view str) {
    if (str.empty()) return false;
    if (!std::isalpha(str[0]) && str[0] != '_') return false;
    
    for (char c : str) {
        if (!std::isalnum(c) && c != '_') return false;
    }
    return true;
}

template<std::size_t N>
consteval auto make_identifier(const char (&str)[N]) {
    if (!is_valid_identifier(str)) {
        throw "Invalid identifier";
    }
    return std::string_view(str, N - 1);
}

int main() {
    constexpr auto id1 = make_identifier("valid_name");  // OK
    // constexpr auto id2 = make_identifier("123invalid");  // Compile error
}

패턴 2: 컴파일 타임 룩업 테이블

#include <array>

consteval std::array<int, 256> generate_crc_table() {
    std::array<int, 256> table{};
    for (int i = 0; i < 256; ++i) {
        int crc = i;
        for (int j = 0; j < 8; ++j) {
            crc = (crc >> 1) ^ ((crc & 1) ? 0xEDB88320 : 0);
        }
        table[i] = crc;
    }
    return table;
}

constexpr auto CRC_TABLE = generate_crc_table();

unsigned int crc32(const char* data, std::size_t length) {
    unsigned int crc = 0xFFFFFFFF;
    for (std::size_t i = 0; i < length; ++i) {
        crc = (crc >> 8) ^ CRC_TABLE[(crc ^ data[i]) & 0xFF];
    }
    return ~crc;
}

패턴 3: 설정 계산

consteval int calculate_pool_size() {
    #ifdef PRODUCTION
        return 128;
    #elif defined(STAGING)
        return 64;
    #else
        return 16;
    #endif
}

consteval int calculate_timeout_ms() {
    return 30 * 1000;  // 30초
}

int main() {
    constexpr int pool_size = calculate_pool_size();
    constexpr int timeout = calculate_timeout_ms();
    
    static_assert(pool_size > 0);
    static_assert(timeout > 0);
}

7. 완전한 예제: 컴파일 타임 설정 시스템

#include <string_view>
#include <array>

// 설정 키-값 쌍
struct ConfigEntry {
    std::string_view key;
    int value;
};

// 컴파일 타임 설정 생성
consteval auto generate_config() {
    std::array<ConfigEntry, 3> config{{
        {"max_connections", 1000},
        {"timeout_ms", 30000},
        {"buffer_size", 4096}
    }};
    return config;
}

// 컴파일 타임 설정 조회
consteval int get_config(std::string_view key) {
    constexpr auto config = generate_config();
    for (const auto& entry : config) {
        if (entry.key == key) {
            return entry.value;
        }
    }
    throw "Config key not found";
}

int main() {
    constexpr int max_conn = get_config("max_connections");  // 1000
    constexpr int timeout = get_config("timeout_ms");        // 30000
    
    std::array<int, max_conn> connection_pool;
    
    static_assert(max_conn == 1000);
    static_assert(timeout == 30000);
}

정리

개념설명
consteval컴파일 타임 전용 함수
즉시 함수호출 시점에 즉시 평가
constexpr 차이consteval은 런타임 실행 불가
용도컴파일 타임 검증, 메타프로그래밍

consteval컴파일 타임 계산을 강제해, 런타임 오버헤드 없이 복잡한 계산을 수행할 수 있습니다.


FAQ

Q1: consteval vs constexpr 언제 쓰나요?

A: 컴파일 타임 계산을 강제하고 싶으면 consteval, 유연하게 둘 다 허용하려면 constexpr을 사용하세요.

Q2: consteval 함수를 constexpr에서 호출할 수 있나요?

A: 아니요. consteval 함수는 consteval 함수 안에서만 호출할 수 있습니다. constexpr 함수는 런타임에도 실행될 수 있으므로, consteval을 호출하면 에러입니다.

Q3: 런타임 값을 전달하면?

A: 컴파일 에러가 납니다. consteval 함수는 모든 인자가 컴파일 타임 상수여야 합니다.

Q4: 부작용이 있으면?

A: I/O, 동적 할당, 전역 변수 수정 등 부작용이 있는 코드는 consteval 함수에서 사용할 수 없습니다. 순수 계산만 가능합니다.

Q5: 컴파일러 지원은?

A:

  • GCC 10+: 완전 지원
  • Clang 10+: 완전 지원
  • MSVC 2019 (16.10+): 완전 지원

Q6: consteval 학습 리소스는?

A:

한 줄 요약: consteval로 컴파일 타임 계산을 강제할 수 있습니다. 다음으로 constexpr를 읽어보면 좋습니다.


같이 보면 좋은 글 (내부 링크)

이 주제와 연결되는 다른 글입니다.

  • C++ static_assert | “정적 단언” 가이드
  • C++20 Concepts 완벽 가이드 | 템플릿 제약의 새 시대

관련 글

  • C++ constexpr 함수 |