c++ 20에서는 consteval과 constinit라는 두가지 키워드가 추가되었습니다.

consteval과 constinit의 테스트 환경

컴파일러 지원 버전

VisualStudio 버전별 컴파일러 버전

또한 프로젝트 우 클릭 -> 구성 속성 -> 일반 -> c++ 언어 표준을 ISO C++ 20 표준으로 수정하였습니다.

C++ 언어 표준 변경

consteval

consteval은 immediate function(즉시 함수)를 생성합니다. 

immediate function란 하나의 컴파일 상수로 평가되는 함수로 컴파일 시점에 실행됩니다

즉시 함수는 암묵적으로 인라인 함수가 되며 다음의 조건을 충족 해야 합니다.
  • 소멸자나 메모리를 할당, 재할당하는 함수 적용 불가
  • constexpr 함수의 요구 조건

아래 예제를 보시면 지역 변수 const int a와 리터럴 5의 경우는 컴파일 시점에 인식 할 수 있는 값이라서 

컴파일 시점에 consteval함수인 sqr의 상수식을 만드는데 문제가 없습니다. 

지역 변수 int b의 경우 컴파일 시점에 값을 알 수 없기 때문에 b를 통해서 sqr 상수식을 만드려고 할 때  컴파일 에러가 발생합니다.

#include <iostream>

using namespace std;

// consteval 함수 
// 하나의 컴파일 상수로 평가됩니다.
consteval int sqr(int n) {
    return n * n;
}


int main()
{
    cout << "sqr(5) : " << sqr(5) << '\n';

    const int a = 5;
    cout << "sqr(a) : " << sqr(a) << '\n';

    int b = 5;
    // cout << "sqr(b) : " << sqr(b) << '\n';  // 컴파일 에러 E3133 consteval 함수 "sqr" 호출에서 유효한 상수 식이 생성되지 않았습니다.
}

https://cppinsights.io/ 사이트에서 컴파일러가 어떻게 consteval을 변환하는지 확인해 볼 수 있습니다. 

아래는 컴파일러가 생성한 sqr 함수입니다.

inline 키워드를 붙이는 것을 확인 할 수 있습니다.

// consteval 함수 
// 하나의 컴파일 상수로 평가됩니다.
inline consteval int sqr(int n)
{
  return n * n;
}

consteval 와 constexpr 함수의 차이점 

c++ 11에 추가된 constexpr과의 차이점은 무엇인지 확인 해 보도록 하겠습니다.

함수 타입 함수의 성격
constexpr 문맥 또는 최적화에 따라서 컴파일 시점 또는 런타임 시점에도 실행 가능
consteval 반드시 컴파일 시점에만 실행 가능

해당 내용을 명확하게 인지하고 상황에 따라서 키워드를 선택 하여 사용 할 수 있습니다. 

#include <iostream>

using namespace std;

// 런타임에 평가 됩니다.
const int sqrRuntime(int n) {
    return n * n;
}

// 하나의 컴파일 상수로 평가됩니다.
consteval int sqrCompileTime(int n) {
    return n * n;
}

// 입력 값에 따라서 컴파일 또는 런타임에 평가 됩니다.
constexpr int sqrRunOrCompileTime(int n) {
    return n * n;
}


int main()
{
    // constexpr 변수는 컴파일 타임에 초기화 되어야 합니다.
    // constexpr auto val1 = sqrRuntime(10);  // 에러 E0028 : 식에 상수 값이 있어야 합니다.
    constexpr auto val2 = sqrCompileTime(10);  // 컴파일 시점
    constexpr auto val3 = sqrRunOrCompileTime(10);  // 컴파일 시점

    int x = 100;

    auto val4 = sqrRuntime(x);  // 런타임 시점
    // auto val5 = sqrCompileTime(x);  // 에러 E3133 : consteval 함수 xxx 호출에서 유횽한 상수 식이 생성되지 않앗습니다.
    auto val6 = sqrRunOrCompileTime(x);  // 컴파일 또는 런타임 시점
}

constexpr에 대해서 더 자세히 확인 하시려면 제가 이전에 쓴 글을 참조 부탁드리겠습니다.

https://jungwoong.tistory.com/54?category=1102341 

 

[c++] constexpr 키워드

일반적인 설명  constexpr 키워드는 c++11에서 소개되고 c++14에서 개선되었습니다. (visual studio 2015부터 지원합니다.)  constexpr은 상수식 이며 const처럼 변수에 적용 할 수 있으며 해당 변수에 대한 변.

jungwoong.tistory.com

 

constinit

constinit 키워드는 저장 기간(storage duration)이 정적이거나 스레드인 변수에 적용 할 수 있습니다. 

주의 점은 지역 변수에 적용 할 수 없습니다.

constinit를 변수에 적용하면 컴파일 시점에 초기화 됩니다. 주의 할 점은 상수(const) 형식으로 지정하지 않습니다. 

const char* g() { return "dynamic initialization"; }
constexpr const char* f(bool p) { return p ? "constant initializer" : g(); }

constinit const char* c = f(true);  // OK
// constinit const char* d = f(false);  // error  E0028 : 식에 상수 값이 있어야 합니다.

변수 초기화

변수 초기화시 const, constexpr, constinit의 차이점에 대해서 예제로 알아 봅니다.

변수의 타입 변수 초기화 성격
const 상수, 런타임 시점까지 초기화 지연 가능
constexpr 상수, 컴파일 시점 초기화
constinit 비상수, 컴파일 시점 초기화, 지역 변수 선언 불가
#include <iostream>

using namespace std;

// 상수
constexpr int constexprVal = 1000;
// 비상수
constinit int constinitVal = 1000;


int main()
{    
	const auto constVal = 1000;
	cout << "constVal: " << constVal << '\n';

	//cout << "++constVal: " << ++constVal << '\n';  // 에러
	//cout << "++constexprVal: " << ++constexprVal << '\n';  // 에러
	cout << "++constinitVal: " << ++constinitVal << '\n';  // OK

	constexpr auto localConstexpr = 1000; //  OK
	// constinit auto localConstexpr = 1000; //  에러 : 지멱 변수가 될 수 없습니다.

}

Static Initialization Order Fiasco(정적 변수 초기화 실패)

Static Initialization Order Fiasco란 코드의 서로 다른 번역 단위(다른 cpp 파일)에 있는 정적 저장기간의 변수들이 순서에

의존적인 문제를 말합니다.

이것에 대해서 설명드리기 전에 정적 변수의 초기화 방식에 대해서 먼저 설명을 드린 후에 진행하도록 하겠습니다. 

정적 변수 초기화 방식

정적 단계(컴파일 시점)에서 상수 표현식을 초기화 할 수 있다면 정적 변수는 해당 상수 값으로 초기화 하고

만약 초기화 할 수 없다면 0으로 셋팅합니다.

동적 단계(런타임 시점)에서 초기화 되지 않은 정적 변수를 실행 시점의 값으로 초기화 합니다.

아래의 예제 소스를 보시면 staticA와 staticB가 서로 다른 번역 단위에서 있으면서 의존 관계로 묶여 있습니다,

staticB의 값을 초기화 하려면 staticA 값이 필요합니다.

staticA값은 컴파일 단계에서 초기화 할 수 없기 때문에 기본 값인 0으로 셋팅 후에 런타임에서 초기화 됩니다. 

이런 경우 번역 단위의 초기화 순서에 따라서 staticB의 값이 달라집니다. 

// SIOF1.cpp
int sum(int l, int r){
    return l + r;
}

auto staticA = sum(1, 2);

// main.cpp
#include <iostream>
using namespace std;

extern int staticA;
auto staticB = staticA;

int main() {
 
    cout << "staticB : " << staticB << "\n";
}

이러한 문제점을 명확하게 보여주기 위해서는 cl 컴파일러를 통해서 빌드를 진행하였습니다. 

# cpp 파일들의 obj 생성 작업
cl.exe /c main.cpp SIOF1.cpp

# SIOF1.obj 먼저 초기화되는 링크 순서
cl.exe /Fe:test1.exe SIOF1.obj main.obj # test1.exe 파일 생성
test1.exe # staticB : 3 출력

# main.obj 먼저 초기화되는 링크 순서
cl.exe /Fe:test2.exe main.obj SIOF1.obj # test2.exe 파일 생성
test2.exe # staticB : 0 출력

c++20 이전의 해결 방법

c++20 이전에는 이 문제를 해결하기 위해서 지역 범위 정적 변수의 지연 초기화를 사용 하였습니다. 

지역 범위에 있는 정적 변수는 처음 사용 될 때 생성된 다는 규칙을 통해서 순서의 의존성을 해결 합니다. 

// SIOF1.cpp
int sum(int l, int r){
    return l + r;
}

int & staticA() {
    // 지역 범위 정적 변수는 처음 사용 될 때 생성 됩니다. 
    static auto staticA = sum(1, 2);
    return staticA;
}

// main.cpp
#include <iostream>
using namespace std;

int & staticA();
// 링크 순서와 상관 없이 이곳에서 처음 사용 되기에 staticB은 일정한 값을 가집니다. 
auto staticB = staticA();

// 생략

c++20에서 해결 방법

c++20에서는 constinit 키워드가 추가되었기 때문에 쉽고 깔끔하게 해결 할 수 있습니다. 

constint를 사용하면 반드시 컴파일 시점에 초기화 되는 것을 보장 할 수 있습니다. 

// SIOF1.cpp
// constexpr는 컴파일 시점에 평가 할 수 있다면 컴파일 시점 평가 함수로 평가 됩니다.
constexpr int sum(int l, int r){
    return l + r;
}

// staticA는 컴파일 시점에 초기화 됩니다.
constinit auto staticA = sum(1, 2);

// main.cpp
#include <iostream>
using namespace std;

extern constinit int staticA;
auto staticB = staticA;

// 생략

'c++ > modern c++' 카테고리의 다른 글

[c++17] fold expression(폴드 표현식)  (0) 2022.06.22
[c++20] 3중 비교 연산자 (<=>)  (0) 2022.06.18
[c++20] module(모듈)  (0) 2022.06.13
[c++20] Concept  (0) 2022.06.10
[c++] condition variable(조건 변수)  (0) 2020.07.10

+ Recent posts