C++ 기초부터 마스터까지 — 17화
constexpr와 consteval로 컴파일 시간 계산하기
본 블로그 작성 글들은, 강의를 목표로 하고 제작되었습니다. 또한 작성된 글의 정보도 틀렸을 수 있으니, 주의해주세요.
C++은 일부 계산을 프로그램 실행 전 컴파일 단계에서 수행할 수 있다. constexpr, consteval, constinit은 비슷해 보이지만 각각 함수 평가, 필수 컴파일 시간 실행, 정적 초기화라는 다른 계약을 표현한다.
constexpr는 두 환경에서 사용할 수 있다
constexpr int Square(int value)
{
return value * value;
}
constexpr int tableSize = Square(8); // 컴파일 시간
int input = ReadInput();
int result = Square(input); // 실행 시간
constexpr 함수는 컴파일 시간에 평가할 수 있도록 작성되지만 모든 호출이 반드시 컴파일 시간에 실행되는 것은 아니다. 인자가 컴파일 시간 상수이고 결과가 상수 표현식이 필요한 위치에 쓰이면 컴파일러가 값을 계산해야 한다.
consteval은 컴파일 시간 호출을 강제한다
consteval unsigned int HashLiteral(std::string_view text)
{
unsigned int hash = 2166136261u;
for (char ch : text)
{
hash = (hash ^ static_cast<unsigned char>(ch)) * 16777619u;
}
return hash;
}
constexpr auto playerId = HashLiteral("Player");
consteval 함수의 호출은 반드시 상수 표현식이어야 한다. 런타임 문자열을 전달하면 컴파일되지 않으므로 에셋 키나 프로토콜 태그처럼 빌드 시점에 고정되어야 하는 값의 실수를 일찍 찾을 수 있다.
해시는 충돌할 수 있다. 컴파일 시간에 계산했다는 사실이 고유성을 보장하지 않으므로 충돌 정책과 원문 검증은 별도로 필요하다.
if constexpr로 타입별 코드 제거하기
template <typename T>
void Serialize(const T& value, Writer& writer)
{
if constexpr (std::integral<T>)
{
writer.WriteInteger(value);
}
else
{
value.Serialize(writer);
}
}
일반 if와 달리 선택되지 않은 if constexpr 분기는 해당 템플릿 인스턴스의 코드에서 버려진다. 타입에 존재하지 않는 연산이 반대 분기에 있어도 선택되지 않는다면 컴파일할 필요가 없다.
constinit은 정적 초기화를 확인한다
constinit int globalLimit = 64;
constinit은 정적 저장 기간 객체가 정적 초기화되는지 컴파일러가 확인하게 한다. 객체를 불변으로 만드는 const와는 다르며 초기화 뒤 값을 바꿀 수 있다.
컴파일 시간 계산은 런타임 비용과 초기화 순서 문제를 줄일 수 있지만 공짜는 아니다. 복잡한 메타프로그래밍은 빌드 시간, 오류 메시지, 바이너리 크기를 늘린다. 실제로 고정된 규칙을 검증하거나 반복 계산을 제거할 가치가 있을 때 사용해야 한다.