C++20의 새로운 기능들을 소개하는 시리즈의 일곱 번째 글에 오신 것을 환영합니다. 이번 글에서는 컴파일 타임 상수 표현식을 더욱 엄격하게 제어할 수 있는 consteval과 constinit 키워드에 대해 자세히 알아보겠습니다.
consteval과 constinit이란 무엇인가요?
C++20에서는 컴파일 타임 상수 계산을 더욱 엄격하게 관리하기 위해 consteval과 constinit 키워드가 도입되었습니다.
- 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 키워드를 사용하면 컴파일 타임 상수 계산과 초기화를 더욱 엄격하게 관리할 수 있습니다. 이를 통해 코드의 안정성과 명확성을 높이고, 잠재적인 버그를 예방할 수 있습니다. 특히, 복잡한 상수 계산이나 전역 변수 초기화 순서로 인한 문제를 해결하는 데 큰 도움이 됩니다.
참고 자료:
'개발 이야기 > C++' 카테고리의 다른 글
[C++20 새기능 소개] 개선된 람다 캡처 (Lambda Capture) (0) | 2024.12.03 |
---|---|
[C++20 새기능 소개] std::format 라이브러리 (32) | 2024.12.02 |
[C++20 새기능 소개] 지정 초기화자(Designated Initializers) (19) | 2024.11.30 |
[C++20 새기능 소개] 모듈 (Modules) (0) | 2024.11.29 |
[C++20 새기능 소개] 코루틴 (Coroutines) (0) | 2024.11.28 |