constexpr 지정자

constexpr 지정자 (constexpr specifier)

어떤 값을 컴파일 타임에 계산할 수 있다고 컴파일러에게 알려주고 싶을 때가 있어요. constexpr 지정자가 바로 그 역할을 해요. 이 지정자를 붙이면 그 개체(entity)를 컴파일 타임 상수 표현식이 필요한 자리에서도 쓸 수 있게 되죠. 이 페이지에서 constexpr이 어떤 규칙으로 동작하는지 하나씩 살펴볼게요.

출처: cppreference

본문

constexpr 지정자는 해당 개체의 값을 컴파일 타임에 평가할 수 있음을 선언해요. 그런 개체는 (적절한 함수 인자가 주어진다면) 컴파일 타임 상수 표현식만 허용되는 곳에서도 사용할 수 있게 돼요.

객체 선언이나 비정적 멤버 함수에 붙은 constexpr 지정자는 const를 암시해요(until C++14). 함수나 정적 데이터 멤버의 첫 번째 선언에 붙은 constexprinline을 암시해요(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 문들
    • casedefault 외의 레이블이 붙은 문들
    • 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이 되지 않음
  1. 그 이유는 constexpr 지정자를 가진 변수 템플릿의 선언이 둘 이상 있을 수 없기 때문에 중복되기 때문이에요.

더 알아보기