constexpr 지정자
constexpr 지정자 (constexpr specifier)
어떤 값을 컴파일 타임에 계산할 수 있다고 컴파일러에게 알려주고 싶을 때가 있어요. constexpr 지정자가 바로 그 역할을 해요. 이 지정자를 붙이면 그 개체(entity)를 컴파일 타임 상수 표현식이 필요한 자리에서도 쓸 수 있게 되죠. 이 페이지에서 constexpr이 어떤 규칙으로 동작하는지 하나씩 살펴볼게요.
출처: cppreference
본문
constexpr 지정자는 해당 개체의 값을 컴파일 타임에 평가할 수 있음을 선언해요. 그런 개체는 (적절한 함수 인자가 주어진다면) 컴파일 타임 상수 표현식만 허용되는 곳에서도 사용할 수 있게 돼요.
객체 선언이나 비정적 멤버 함수에 붙은 constexpr 지정자는 const를 암시해요(until C++14). 함수나 정적 데이터 멤버의 첫 번째 선언에 붙은 constexpr은 inline을 암시해요(since C++17). 함수나 함수 템플릿의 어떤 선언이라도 constexpr 지정자가 있으면 모든 선언이 그 지정자를 담아야 해요.
constexpr 변수
변수나 변수 템플릿(since C++14)은 다음 조건을 모두 만족하면 constexpr로 선언할 수 있어요.
- 선언이 정의(definition)여야 해요.
- 리터럴 타입(literal type)이어야 해요.
- (그 선언에 의해) 초기화되어야 해요.
- 초기화의 전체 표현식(full-expression)이 상수 표현식이어야 해요. (until C++26)
- 상수 초기화가 가능해야 해요(constant-initializable). (since C++26)
- 상수 파괴(constant destruction)가 가능해야 해요. 이는 다음 조건 중 하나를 만족함을 뜻해요.
- 클래스 타입도, 클래스 타입의 (다차원 포함) 배열도 아니어야 해요.
constexpr소멸자를 가진 클래스 타입이거나 그 (다차원 포함) 배열이면서, 객체를 파괴하는 것만이 유일한 효과인 가상 표현식e에 대해e가 핵심 상수 표현식이어야 해요. 단, 객체와 그 비-mutable 하위 객체(하지만 mutable 하위 객체는 제외)의 수명이e안에서 시작한다고 가정할 때요.
constexpr 변수가 번역 단위 내부 전용(translation-unit-local)이 아니라면, 상수 표현식에서 사용할 수 있는 번역 단위 내부 전용 개체를 가리키도록 초기화되거나 그런 개체를 가리키는 하위 객체를 가져서는 안 돼요. 이런 초기화는 모듈 인터페이스 단위(개인 모듈 조각 밖)나 모듈 파티션에서는 허용되지 않고, 다른 문맥에서는 더 이상 쓰지 않는(deprecated) 것으로 취급돼요. (since C++20)
constexpr 함수
함수나 함수 템플릿은 constexpr로 선언할 수 있어요. 함수가 다음 조건을 모두 만족하면 constexpr 적합(constexpr-suitable) 하다고 말해요.
- 가상 함수가 아니어야 해요. (until C++20)
- 반환 타입(있다면)이 리터럴 타입이어야 해요.
- 각 매개변수 타입이 리터럴 타입이어야 해요. (until C++23)
- 생성자나 소멸자(since C++20)라면 추가 조건을 만족해야 해요(아래 참고). (until C++26)
- 코루틴이 아니어야 해요. (since C++20)
- 함수 본문이
= default,= delete, 또는 다음만 담는 복합문이어야 해요.- null 문들
static_assert선언들- 클래스나 열거형을 정의하지 않는
typedef선언과 별칭 선언 using선언들using지시문들- 생성자가 아니라면 정확히 하나의
return문 (until C++14)
- 함수 본문이
= default,= delete, 또는 다음을 감싸지(until C++20) 않는 복합문이어야 해요.goto문들case와default외의 레이블이 붙은 문들try블록들- 인라인 어셈블리 선언들
- 초기화가 수행되지 않는 변수 정의들 (until C++20)
- 비-리터럴 타입 변수의 정의들
- 정적 또는 스레드 저장 기간을 가진 변수의 정의들 (since C++14) (until C++23)
인스턴스화된 constexpr 함수를 제외하면, 비-템플릿 constexpr 함수는 반드시 constexpr 적합해야 해요.
기본(defaulted)도 템플릿도 아닌 비-생성자 constexpr 함수에 대해, 함수의 호출이 핵심 상수 표현식의 평가된 부분 표현식이 될 수 있는 인자 값 집합이 전혀 없다면 프로그램은 진단 없이 ill-formed예요(no diagnostic required). 템플릿 constexpr 함수에 대해 비-템플릿 함수로 간주했을 때 함수/클래스 템플릿을 constexpr 적합하게 만들 전문화가 없다면 역시 진단 없이 ill-formed예요. (until C++23)
주어진 문맥에서 constexpr 함수의 호출은 모든 면에서 같은 문맥에서 동등한 비-constexpr 함수의 호출과 같은 결과를 내요. 다만 다음 예외가 있어요.
constexpr함수의 호출은 상수 표현식에 나타날 수 있어요.- 상수 표현식에서는 복사 생략(copy elision)이 수행되지 않아요.
constexpr 생성자
constexpr 함수의 요구사항에 더해, 생성자도 constexpr 적합하려면 다음 조건을 모두 만족해야 해요.
- 함수 본문이
= delete이거나 다음 추가 요구사항을 만족해야 해요.- 클래스가 variant 멤버를 가진 공용체(union)라면 그중 정확히 하나만 초기화돼요.
- 클래스가 공용체와 유사(union-like)하지만 공용체가 아니고, variant 멤버를 가진 각 익명 공용체 멤버에 대해 그중 정확히 하나만 초기화돼요.
- 모든 비-variant 비정적 데이터 멤버와 베이스 클래스 하위 객체가 초기화돼요. (until C++20)
- 생성자가 위임 생성자(delegating constructor)라면 대상 생성자가
constexpr생성자여야 해요. - 생성자가 비-위임 생성자라면 비정적 데이터 멤버와 베이스 클래스 하위 객체를 초기화하기 위해 선택된 모든 생성자가
constexpr생성자여야 해요. (until C++23) - 클래스에 가상 베이스 클래스가 없어야 해요. (until C++26)
생성자는 constexpr 적합하기 위해 추가 조건을 만족할 필요가 없어요. (since C++26)
기본도 템플릿도 아닌 constexpr 생성자에 대해, 상수 표현식에 따라야 하는 어떤 객체의 초기화 전체 표현식의 평가된 부분 표현식으로 호출이 될 수 있는 인자 값 집합이 전혀 없다면 프로그램은 진단 없이 ill-formed예요. (until C++23)
constexpr 소멸자
소멸자는 constexpr일 수 없지만, 사소한(trivial) 소멸자는 상수 표현식에서 암시적으로 호출될 수 있어요. (until C++20)
constexpr 함수의 요구사항에 더해, 소멸자도 constexpr 적합하려면 다음 조건을 모두 만족해야 해요.
- 클래스 타입이거나 그 (다차원 포함) 배열인 모든 하위 객체에 대해, 그 클래스 타입이
constexpr소멸자를 가져야 해요. (until C++23) - 클래스에 가상 베이스 클래스가 없어야 해요. (since C++20) (until C++26)
소멸자는 constexpr 적합하기 위해 추가 조건을 만족할 필요가 없어요. (since C++26)
참고 (Notes)
noexcept 연산자는 상수 표현식에 대해 항상 true를 반환하므로, 특정 constexpr 함수 호출이 상수 표현식 분기를 타는지 확인하는 데 쓸 수 있어요.
constexpr int f();
constexpr bool b1 = noexcept(f()); // false, undefined constexpr function
constexpr int f() { return 0; }
constexpr bool b2 = noexcept(f()); // true, f() is a constant expression
그 호출이 결코 핵심 상수 표현식의 요구사항을 만족하지 못하는 constexpr 함수를 작성하는 것도 가능해요.
void f(int& i) // not a constexpr function
{
i = 0;
}
constexpr void g(int& i) // well-formed since C++23
{
f(i); // unconditionally calls f, cannot be a constant expression
}
리터럴 타입이 아닌 클래스에도 constexpr 생성자가 허용돼요. 예를 들어 std::shared_ptr의 기본 생성자가 constexpr이라서 상수 초기화가 가능해요.
참조 변수는 constexpr로 선언할 수 있어요(그 초기화식은 참조 상수 표현식이어야 해요).
static constexpr int const& x = 42; // constexpr reference to a const int object
// (the object has static storage duration
// due to life extension by a static reference)
try 블록과 인라인 어셈블리가 constexpr 함수에 허용되긴 하지만, 잡히지 않은 예외를 던지거나(since C++26) 어셈블리를 실행하는 것은 여전히 상수 표현식에서는 허용되지 않아요.
변수에 상수 파괴가 있다면 소멸자가 사소하지 않더라도 그 소멸자를 호출하기 위해 기계 코드를 생성할 필요가 없어요.
람다도, 특별 멤버도, 템플릿도 아닌 constexpr 함수는 암시적으로 immediate 함수가 될 수 없어요. 그런 의도의 함수 정의를 올바르게 만들려면 사용자가 명시적으로 consteval로 표시해야 해요. (since C++20)
피처 테스트 매크로 (Feature-test macro)
| 매크로 | 값 | 표준 | 피처 |
|---|---|---|---|
__cpp_constexpr |
200704L | (C++11) | constexpr |
__cpp_constexpr |
201304L | (C++14) | 완화된 constexpr, 비-const constexpr 멤버 함수 |
__cpp_constexpr |
201603L | (C++17) | constexpr 람다 |
__cpp_constexpr |
201907L | (C++20) | constexpr 함수에서 사소한 기본 초기화와 asm-선언 |
__cpp_constexpr |
202002L | (C++20) | 상수 평가에서 공용체의 활성 멤버 변경 |
__cpp_constexpr |
202110L | (C++23) | constexpr 함수에서 비-리터럴 변수, 레이블, goto 문 |
__cpp_constexpr |
202207L | (C++23) | 일부 constexpr 제한 완화 |
__cpp_constexpr |
202211L | (C++23) | constexpr 함수에서 정적 constexpr 변수 허용 |
__cpp_constexpr |
202306L | (C++26) | void*에서 constexpr 캐스트: constexpr 타입 소거로 향해 |
__cpp_constexpr_in_decltype |
201711L | (C++11) (DR) | 상수 평가에 필요할 때 함수·변수 정의 생성 |
__cpp_constexpr_dynamic_alloc |
201907L | (C++20) | constexpr 함수에서 동적 저장 기간 연산 |
__cpp_constexpr_virtual_inheritance |
202506L | (C++26) | constexpr 가상 상속 |
키워드 (Keywords)
constexpr
예제 (Example)
C++11/14의 constexpr 함수로 팩토리얼을 계산하고, 문자열 리터럴을 확장하는 리터럴 타입을 정의해요.
#include <iostream>
#include <stdexcept>
// C++11 constexpr functions use recursion rather than iteration
constexpr int factorial(int n)
{
return n <= 1 ? 1 : (n * factorial(n - 1));
}
// C++14 constexpr functions may use local variables and loops
#if __cplusplus >= 201402L
constexpr int factorial_cxx14(int n)
{
int res = 1;
while (n > 1)
res *= n--;
return res;
}
#endif // C++14
// A literal class
class conststr
{
const char* p;
std::size_t sz;
public:
template<std::size_t N>
constexpr conststr(const char(&a)[N]): p(a), sz(N - 1) {}
// constexpr functions signal errors by throwing exceptions
// in C++11, they must do so from the conditional operator ?:
constexpr char operator[](std::size_t n) const
{
return n < sz ? p[n] : throw std::out_of_range("");
}
constexpr std::size_t size() const { return sz; }
};
// C++11 constexpr functions had to put everything in a single return statement
// (C++14 does not have that requirement)
constexpr std::size_t countlower(conststr s, std::size_t n = 0,
std::size_t c = 0)
{
return n == s.size() ? c :
'a' <= s[n] && s[n] <= 'z' ? countlower(s, n + 1, c + 1)
: countlower(s, n + 1, c);
}
// An output function that requires a compile-time constant, for testing
template<int n>
struct constN
{
constN() { std::cout << n << '\n'; }
};
int main()
{
std::cout << "4! = ";
constN<factorial(4)> out1; // computed at compile time
volatile int k = 8; // disallow optimization using volatile
std::cout << k << "! = " << factorial(k) << '\n'; // computed at run time
std::cout << "The number of lowercase letters in \"Hello, world!\" is ";
constN<countlower("Hello, world!")> out2; // implicitly converted to conststr
constexpr int a[12] = {0, 1, 2, 3, 4, 5, 6, 7, 8};
constexpr int length_a = sizeof a / sizeof(int); // std::size(a) in C++17,
// std::ssize(a) in C++20
std::cout << "Array of length " << length_a << " has elements: ";
for (int i = 0; i < length_a; ++i)
std::cout << a[i] << ' ';
std::cout << '\n';
}
출력:
4! = 24
8! = 40320
The number of lowercase letters in "Hello, world!" is 9
Array of length 12 has elements: 0 1 2 3 4 5 6 7 8 0 0 0
결함 보고 (Defect reports)
다음 동작 변경 결함 보고는 이전에 발표된 C++ 표준에 소급 적용됐어요.
| DR | 적용 대상 | 발표된 동작 | 올바른 동작 |
|---|---|---|---|
| CWG 1358 | C++11 | 템플릿 constexpr 함수도 최소한 하나의 유효한 인자 값을 가져야 했음 |
필요 없음 |
| CWG 1359 | C++11 | constexpr 공용체 생성자가 모든 데이터 멤버를 초기화해야 함 |
비-빈 공용체에 대해 정확히 하나의 데이터 멤버를 초기화함 |
| CWG 1366 | C++11 | = default나 = delete 본문을 가진 constexpr 생성자의 클래스가 가상 베이스 클래스를 가질 수 있었음 |
그런 클래스는 가상 베이스 클래스를 가질 수 없음 |
| CWG 1595 | C++11 | constexpr 위임 생성자가 관련된 모든 생성자가 constexpr일 것을 요구했음 |
대상 생성자만 constexpr이면 됨 |
| CWG 1712 | C++14 | constexpr 변수 템플릿은 모든 선언이 constexpr 지정자를 담아야 했음[1] |
더 이상 요구되지 않음 |
| CWG 1911 | C++11 | 비-리터럴 타입의 constexpr 생성자가 허용되지 않았음 |
상수 초기화에서 허용됨 |
| CWG 2004 | C++11 | mutable 멤버를 가진 공용체의 복사/이동이 상수 표현식에서 허용됨 | mutable variant가 암시적 복사/이동을 불가능하게 함 |
| CWG 2022 | C++98 | 동등한 constexpr 함수와 비-constexpr 함수가 같은 결과를 내는지가 복사 생략 수행 여부에 달라질 수 있었음 |
상수 표현식에서는 복사 생략이 항상 수행된다고 가정 |
| CWG 2163 | C++14 | goto 문이 금지됐는데도 레이블은 constexpr 함수에 허용됨 |
레이블도 금지 |
| CWG 2268 | C++11 | mutable 멤버를 가진 공용체의 복사/이동이 CWG 2004 해결로 금지됨 | 객체가 상수 표현식 안에서 생성되면 허용 |
| CWG 2278 | C++98 | CWG 2022의 해결을 구현할 수 없었음 | 상수 표현식에서는 복사 생략이 절대 수행되지 않는다고 가정 |
| CWG 2531 | C++11 | 비-inline 변수가 constexpr로 재선언되면 inline이 됨 |
변수는 inline이 되지 않음 |
- 그 이유는
constexpr지정자를 가진 변수 템플릿의 선언이 둘 이상 있을 수 없기 때문에 중복되기 때문이에요.
더 알아보기
- constant expression — 컴파일 타임에 평가할 수 있는 표현식을 정의해요.
- consteval 지정자 (C++20) — 함수가 immediate 함수, 즉 함수에 대한 모든 호출이 반드시 상수 평가여야 함을 지정해요.
- constinit 지정자 (C++20) — 변수가 정적 초기화, 즉 zero 초기화와 상수 초기화를 가짐을 단언해요.
- C documentation for constexpr