[C++20 새기능 소개] consteval과 constinit 키워드

C++20의 새로운 기능들을 소개하는 시리즈의 일곱 번째 글에 오신 것을 환영합니다. 이번 글에서는 컴파일 타임 상수 표현식을 더욱 엄격하게 제어할 수 있는 constevalconstinit 키워드에 대해 자세히 알아보겠습니다.

consteval과 constinit이란 무엇인가요?

C++20에서는 컴파일 타임 상수 계산을 더욱 엄격하게 관리하기 위해 constevalconstinit 키워드가 도입되었습니다.

  • consteval: 함수를 컴파일 타임 상수 표현식으로만 평가되도록 강제합니다.
  • constinit: 변수가 컴파일 타임 초기화되도록 보장합니다.

이를 통해 상수 표현식과 관련된 버그를 방지하고, 코드의 안전성과 명확성을 높일 수 있습니다.

왜 consteval과 constinit을 사용해야 할까요?

기존의 constexpr는 함수나 변수가 컴파일 타임과 런타임 모두에서 사용될 수 있도록 허용합니다. 하지만 때로는 함수가 반드시 컴파일 타임에 평가되어야 하거나, 정적 변수가 초기화 순서와 관계없이 초기화되어야 하는 경우가 있습니다. 이러한 요구 사항을 consteval과 constinit을 통해 명확하게 표현할 수 있습니다.

consteval의 사용 방법

1. consteval 함수 정의

consteval 키워드를 사용하면 해당 함수가 항상 컴파일 타임에만 평가되어야 함을 명시할 수 있습니다.

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

사용 예제

int main() {
    constexpr int val = square(5); // 올바름
    int result = square(5);        // 올바름: 인자가 리터럴이므로 컴파일 타임 평가 가능

    int runtime_val = 5;
    // int error_result = square(runtime_val); // 오류: 런타임 값으로 호출 불가

    return 0;
}
  • square(5)는 컴파일 타임에 평가되므로 올바르게 동작합니다.
  • square(runtime_val)은 runtime_val이 런타임에만 알려지는 값이므로 컴파일 타임에 평가될 수 없어 오류가 발생합니다.

constexpr 함수와의 차이점

  • constexpr 함수: 컴파일 타임에 평가될 수 있지만, 런타임에도 호출 가능합니다.
  • consteval 함수: 항상 컴파일 타임에만 평가되어야 합니다.
constexpr int multiply(int x, int y) {
    return x * y;
}

consteval int add(int x, int y) {
    return x + y;
}

int main() {
    int a = 2, b = 3;

    int runtime_result = multiply(a, b); // 올바름: 런타임 호출 가능
    // int error_result = add(a, b);     // 오류: consteval 함수는 런타임에 호출 불가

    constexpr int compile_time_result = multiply(2, 3); // 올바름
    constexpr int consteval_result = add(2, 3);         // 올바름

    return 0;
}

consteval을 활용한 컴파일 타임 계산

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

int main() {
    constexpr int fact5 = factorial(5); // 올바름: 5! = 120
    // int runtime_fact = factorial(runtime_val); // 오류: 런타임 값으로 호출 불가

    return 0;
}

consteval 함수를 사용하여 컴파일 타임에 팩토리얼을 계산할 수 있습니다.

constinit의 사용 방법

1. 정적 변수의 초기화 보장

constinit 키워드는 정적 변수의 초기화 순서 문제를 해결하기 위해 사용됩니다.

#include <iostream>

constinit int global_value = 42;

struct S {
    S() {
        std::cout << "S 생성자 호출\n";
        std::cout << "global_value: " << global_value << '\n';
    }
};

S s_instance;

int main() {
    return 0;
}

위 코드에서 global_value는 constinit으로 선언되어 있으므로, s_instance가 생성되기 전에 항상 초기화됩니다. 따라서 S의 생성자에서 global_value를 안전하게 사용할 수 있습니다.

2. 변경 가능한 상수 초기화

constinit은 변수의 초기화를 컴파일 타임에 보장하지만, 값의 변경은 허용합니다.

constinit int config_value = 100;

int main() {
    config_value = 200; // 올바름
    std::cout << "config_value: " << config_value << '\n'; // 출력: 200
    return 0;
}

constinit과 constexpr의 차이점

  • constexpr: 값이 변경 불가능한 컴파일 타임 상수입니다.
  • constinit: 초기화는 컴파일 타임에 이루어지지만, 값은 변경 가능합니다.
constexpr int max_value = 100; // 변경 불가
constinit int threshold = 50;  // 변경 가능

int main() {
    // max_value = 150; // 오류: constexpr 변수는 변경 불가
    threshold = 75;      // 올바름
    return 0;
}

더 다양한 예제

consteval을 활용한 컴파일 타임 문자열 연결

#include <string_view>

consteval std::string_view concat(std::string_view a, std::string_view b) {
    static char buffer[256] = {};
    std::size_t len = a.size();
    a.copy(buffer, len);
    b.copy(buffer + len, b.size());
    return {buffer, len + b.size()};
}

int main() {
    constexpr auto message = concat("Hello, ", "World!");
    // 출력: Hello, World!
    return 0;
}

constinit으로 전역 객체의 초기화 순서 제어

#include <iostream>

struct Logger {
    Logger() {
        std::cout << "Logger 생성자 호출\n";
    }
    void log(const std::string& message) {
        std::cout << message << '\n';
    }
};

constinit Logger logger; // 초기화 순서 보장

struct Application {
    Application() {
        logger.log("Application 생성자 호출");
    }
};

Application app;

int main() {
    return 0;
}

위 코드에서 logger는 constinit으로 선언되어 있으므로 app 객체보다 먼저 초기화되어, Application의 생성자에서 안전하게 logger를 사용할 수 있습니다.

주의 사항

1. consteval 함수는 런타임에 호출될 수 없음

consteval int get_compile_time_value() {
    return 42;
}

int main() {
    constexpr int value = get_compile_time_value(); // 올바름
    // int runtime_value = get_compile_time_value(); // 오류 발생
    return 0;
}

2. 지역 변수에는 constinit을 사용할 수 없음

int main() {
    // constinit int local_var = 10; // 오류: 지역 변수에는 constinit 사용 불가
    return 0;
}

constinit은 전역 변수 또는 정적 변수에만 사용할 수 있습니다.

3. constinit과 정적 지역 변수

void func() {
    constinit static int counter = 0;
    counter++;
    std::cout << "호출 횟수: " << counter << '\n';
}

int main() {
    func();
    func();
    return 0;
}

위 코드에서 counter는 constinit으로 선언된 정적 지역 변수이며, 초기화가 컴파일 타임에 보장됩니다.

consteval과 constinit의 장점

  • 안정성 강화: 컴파일 타임에 값이 결정되므로 런타임 오류를 예방할 수 있습니다.
  • 초기화 순서 문제 해결: 전역 변수의 초기화 순서로 인한 버그를 방지합니다.
  • 최적화 기회 제공: 컴파일러가 컴파일 타임 값을 알고 있으므로 최적화를 수행할 수 있습니다.
  • 코드 명확성 증가: 코드에서 상수와 변수의 용도를 명확하게 구분할 수 있습니다.

결론

C++20의 consteval과 constinit 키워드를 사용하면 컴파일 타임 상수 계산과 초기화를 더욱 엄격하게 관리할 수 있습니다. 이를 통해 코드의 안정성과 명확성을 높이고, 잠재적인 버그를 예방할 수 있습니다. 특히, 복잡한 상수 계산이나 전역 변수 초기화 순서로 인한 문제를 해결하는 데 큰 도움이 됩니다.

 

참고 자료:

 

반응형