최근 수정 시각 : 2024-04-07 13:43:55

C++/문법/상수 표현식

파일:관련 문서 아이콘.svg   관련 문서: C++
, C++/문법/클래스
, 컴파일러
, Zig
,
,

파일:상위 문서 아이콘.svg   상위 문서: C++/문법
프로그래밍 언어 문법
{{{#!wiki style="margin: -16px -11px; word-break: keep-all"<colbgcolor=#0095c7><colcolor=#fff,#000> 언어 문법 C(포인터) · C++(자료형 · 특성 · 클래스 · 이름공간 · 상수 표현식) · C# · Java · Python · Kotlin · MATLAB · SQL · PHP · JavaScript
마크업 문법 HTML · CSS
개념과 용어 함수 · 인라인 함수 · 고차 함수 · 람다식 · 리터럴 · size_t · 상속 · 예외 · 조건문 · 참조에 의한 호출 · eval
기타 == · === · NaN · null · undefined · 모나드 · 배커스-나우르 표기법
프로그래밍 언어 예제 · 목록 · 분류 }}}

1. 개요2. 자명성3. 변수
3.1. constexpr 상수3.2. constinit 초기화
4. 컴파일 문맥
4.1. 표준 라이브러리: is_constant_evaluated()4.2. if consteval
5. 함수
5.1. constexpr 함수5.2. consteval 함수
6. 클래스
6.1. 정적 데이터 멤버6.2. 비정적 멤버 함수6.3. 정적 멤버 함수
7. 상수 표현이 가능한 경우
7.1. 암시적 형변환
7.1.1. 참조7.1.2. 포인터
7.2. 상수 진리값
7.2.1. static_assert7.2.2. 조건적 noexcept7.2.3. if constexpr7.2.4. 조건적 explicit
8. 불가능한 경우
8.1. 형변환

1. 개요

Constant Expressions
C++11 부터 도입된 상수 표현식, 또는 상수식은 실제 코드가 실행되는 사용자 시점(런타임)이 아니라 컴파일 시점으로 코드의 평가를 앞당길 수 있는 획기적인 기능이다. C++의 킬러 요소라고 말할 수 있는 핵심 기능이다 [1]. 상수 표현식의 의미는 해당 코드는 결정론적인 동작을 하며, 초기 조건이 주어지면 유한한 절차의 알고리즘을 통해 무조건 결과를 알 수 있는 표현식이란 뜻이다. 그리고 결정론적인 코드란 로컬 머신안에서, 사용자의 코드에서 결정할 수 있는 코드다. 바이너리가 생성되는 컴파일 시점에 실행 결과가 결정되기 때문에 아무런 평가 과정도 컴파일 이후에 남지 않는다. 곧 프로그램 이용자의 실행 시점에는 코드 실행 시간이 0이 되도록 최적화된다. 예를 들어 상수 시간에 실행되는 함수는 실행 시점(런타임)이 아니라 컴파일 시점(컴파일 타임), 심지어는 IDE에서 바로 값을 볼 수도 있다. 이토록 강력한 기능이지만 상수 표현식을 사용할 수 있는 조건은 제한되어 있으며 작성하기 까다로운 부분이 존재한다. 그래도 조건은 계속 완화되고 있다. C++14까지는 일부 생성자와 getter 함수나 지원했지만 C++23에 오면 IO, 동시성 프로그램 말고는 죄다 상수식으로 취급한다고 보면 된다.
Phases of Translation
컴파일 과정의 9번째 단계에서 상수 표현식을 실행한다.

2. 자명성

컴파일 시점에 결과를 결정하는 기능을 추가할 수 있었던 건 클래스 문서에서도 설명한 자명한 자료형의 덕이 컸다. 자명하다는 것은 방정식에 명백한 해가 존재한다는 뜻이다. 자명한 자료형은 어떠한 외부 의존성없이 스스로 존재하고 스스로 값을 결정하고 스스로 파괴될 수 있다. 이것을 결정론적인 동작을 한다고 부른다. 결정론적인 코드는 초기값이 들어오면 반드시 상수 시간에 결과를 알 수 있다. 다시 말하면 프로그램에 필요한 메모리를 컴파일 시간에 알 수 있고, 어떤 동작을 할 지 컴파일 시간에 알 수 있고, 마지막에 어떻게 소멸하는지 컴파일 시간에 알 수 있다. 하지만 자명함을 만족시키려면 조건이 따른다. 무조건 사용자의 코드 안에서 모든 것을 결정할 수 있어야 한다. 만약 자료형에 외부 의존성이 있으면 평가 과정이 유한할지 무한할지, 성공할지 실패할 지 알 수가 없다. 여기서 외부 의존성이란 외부 연결을 통해 동적으로 가져온 함수 또는 운영체제 호출을 말한다. 운영체제 호출 때문에 당장 실패할지 성공할지 모르는 코드는 상수식이 아니다. 혹여나 외부 라이브러리의 경우 같이 제공되는 헤더 파일에 완전히 정의된 클래스나 함수가 제공되면 상수식으로 만들 수 있다. 그렇지만 보통 동적 라이브러리의 경우 라이브러리의 초기화, 정리, 함수 호출에 운영체제의 도움이 필요하다. 그리고 정적 라이브러리는 컴파일러의 도움을 받아도 사용자 단에서는 정적 라이브러리 파일에서 가져온 함수의 정의를 알 수 없다.

C++의 모든 원시 자료형(Primitive) 또는 원시 자료형의 별칭 자료형(Alias)은 자명한 자료형이다. 모든 종류의 열거형은 자명한 자료형이다. 클래스의 인스턴스를 컴파일 시간에 만들 때는 그 클래스가 자명해야(Trivial) 한다. 클래스에서 자명한 클래스의 조건을 다시 보면, 오로지 단일한 default 생성자, default 소멸자가 있어야 하며, 복사 혹은 이동 생성자를 갖고 있으면 default여야 하고, 복사 혹은 이동 대입 연산자를 갖고 있으면 default여야 한다. virtual 소멸자를 갖고 있으면 안되고, 가상 클래스를 상속받아도 안되고, 추상 클래스이면 안되고, 비정적 참조형 데이터 멤버를 가질 수 없다. 또한 모든 비정적 데이터 멤버도 상기한 조건을 만족해야 하며, 마찬가지로 상속 구조에서 모든 부모 클래스도 상기한 조건을 만족해야 한다. 사실 가상 클래스나 운영체제 파생 클래스를 안쓰면 그럭저럭 만족시킬 수 있지만 default 생성자와 default 소멸자가 활용에 제약을 많이 줘서 잘 쓰이진 않는다. 마지막으로 어떤 자료형의 것이든 간에 포인터도 const T*, T *const 상관없이 자명한 자료형이다. 모든 종류의 포인터라는 건 미완성된 클래스 혹은 운영체제 객체의 포인터도 포함된다. 이를 이용해서 자명한 클래스의 조건을 조금 우회할 수 있다. 하지만 포인터에서 객체를 얻어내는 순간 컴파일 문맥에서 탈출하므로 메모리 할당 외에는 용도가 제한된다.

3. 변수

3.1. constexpr 상수

constexpr 자료형 식별자 = ;
inlineconstexpr 자료형 식별자 = ;
constexprC++11
자료형 앞에 평가 지시자 constexpr를 덧붙여 컴파일 시점에 값이 초기화되는 상수임을 나타낼 수 있다. 또한 선택적으로 static, extern, thread_local, inline 등의 연결성 지시자도 붙일 수 있다. constexpr 변수는 무조건 상수(const)이며, 컴파일 시점에 값이 정해진다. 그렇기 때문에 실행 시점에는 변수의 생성 실패, 메모리 오버헤드 등 어떠한 문제도 발생하지 않는다.

inline
inline을 붙이면 아예 컴파일 시점에 모든 변수가 값으로 바뀌게 컴파일러를 유도할 수 있다. 이름공간 안에 있는 inlineconst 상수는 외부 연결이 적용된다. 마찬가지로 이름공간 안에 있는 inline constexpr 상수도 외부 연결이 적용되어 선언만 있고 값을 즉시 할당하지 않아도 괜찮다. 코드 어딘가에 그 inline constexpr 상수에 값을 할당하는 구문이 있으면 반드시 컴파일 시점에 값을 할당한다. 하지만 이름이 같은데 자료형이 다른 inline constexpr 상수가 있으면 컴파일 오류가 발생하므로 유의해야 한다. 정적인 static constexpr 상수는 반드시 inline이며 곧 static inline constexpr을 암시한다.

3.2. constinit 초기화

constinitC++20
자료형 앞에 평가 지시자 constinit을 덧붙여 컴파일 시점에 값이 초기화되는 변수임을 나타낼 수 있다. 또한 선택적으로 static, extern, thread_local, inline 등의 연결성 지시자도 붙일 수 있다. constinit는 오직 변수의 상수 시간 초기화를 위해 추가된 기능이다. constexpr에서 초기화 기능만 이용하는 셈이다. 그래서 constinit 변수는 constexpr 변수와는 다르게 상수가 아니다. 할 이유는 없지만 constinit const 상수는 constexpr과 같은 동작을 한다.

4. 컴파일 문맥

4.1. 표준 라이브러리: is_constant_evaluated()

[[nodiscard]] constexpr bool is_constant_evaluated();
std::is_constant_evaluated()C++11

4.2. if consteval

if constevalC++20

5. 함수

5.1. constexpr 함수

[[특성]]
constexpr 반환 자료형 함수 식별자(매개변수1-자료형 매개변수1, 매개변수2-자료형 매개변수2, ...)
{
...
}
constexprC++11
해당 함수가 상수 표현식임을 나타낸다. constexpr 함수는 무조건 inline이어야 한다. 따라서 함수의 선언과 정의를 같이 해야 한다. 자료형 앞에 constexprC++11 이나 constevalC++20 평가 지시자를 덧붙여 컴파일 시점에 결과가 정해질 수 있는 함수임을 나타낼 수 있다. 또한 선택적으로 static, extern, inline 등의 연결성 지시자도 붙일 수 있다.

상수 표현식 함수 사용에는 커다란 제약이 있다. 평가가 컴파일 시점에 이루어져야 하기 때문에 반환 자료형이 자명해야 한다. 또한 함수의 지역변수도 자명한 자료형이어야 한다. 때문에 사용할 수 있는 자료형의 제한이 매우 크다. 다행히 매개변수의 경우 포인터나 참조형이라도 문제가 없다.

C++11에서는 진짜 O(1)인 함수였는데 C++17에서는 가능하다면 컴파일 시점에 평가해야 하는 함수가 되었다. C++20에선 동적 메모리 할당 조차 컴파일 시점에 실행될 수 있다. 이러면 할당 위치만 힙이고 작동은 스택처럼 구현되어 힙의 내용이 컴파일 시점에 결정된다. 이런 식으로 동적 배열 클래스 std::vector, 문자열 클래스 std::stringconstexpr 생성자, 소멸자와 메서드를 얻었다. 상수 표현식의 내용이 포괄적으로 바뀐 셈인데, 좋을 것만 같지만 이러다 보니 컴파일 시간이 너무 오래 걸리고, 키워드의 의미가 배보다 배꼽이 더 커지는 일이 일어났다. 때문에 매크로도 대체할 겸 진짜 의도를 되살리기 위해 후술할 consteval 키워드가 도입되었다. C++23에 와서는 진짜로 컴파일 시점에 평가될 수도 있는 함수가 되었다. 아예 constexpr 함수 안에서 constexpr가 아닌 함수를 호출할 수 있다. 왜냐하면 함수가 실행되면 그저 컴파일 문맥이 아닐 뿐 constexpr의 의미를 해치는 건 아니기 때문이다.

5.2. consteval 함수

[[특성]]
consteval 반환 자료형 함수 식별자(매개변수1-자료형 매개변수1, 매개변수2-자료형 매개변수2, ...)
{
...
}
constevalC++20
해당 함수가 상수 표현식이고, 문맥 상으로도 반드시 컴파일 시점에 평가 되어야함을 나타낸다. 보면 알겠지만 constexpr의 처음 도입조건보다 더 엄격한 조건을 갖고 있다. 함수 내부와 매개 변수에서도 정적이고 컴파일 시점에 결정되는 자료형 또는 객체가 아니면 참조할 수 없다. 컴파일 시점에 값이 결정된다는 것은, 사용자 입장에서는 언제나 고정된 값으로 보인다는 뜻이다. 때문에 consteval 함수는 즉발 함수(Immediate Function)라고 불리며 어떤 부작용(Side Effect) 없이 독립적으로 실행되는 함수다[2]. 그래서 consteval 함수는 코드 상에서만 함수로 보이는, 사실상 상수라고 봐야 한다.

이 지시자의 의의는 컴파일러 전용 함수를 C++에 구현한다는 것에 있다. constexpr 상수[3]는 C의 전처리기 키워드를 대체할 수 있었다. consteval은 이제 위험한 매크로의 일부를 대체할 수 있다. 예를 들어서 전처리기 분기를 통해[4] 어떤 상수를 선언한다면, 예전에는 전처리기와 매크로 지옥에서 빠져나오지 못했다. 이런 전처리기 구문들은 프로그램 헤더 구조의 저 멀리 최상단에 놓이게 될텐데 때문에 중복되는 헤더 삽입, 전처리기 키워드 중복 문제가 발생한다. 그러나 이젠 consteval에서 C++ 코드를 통해 밖으로 보이지 않고 처리가 가능하다.

6. 클래스

6.1. 정적 데이터 멤버

메타 데이터 (Meta Data)

6.2. 비정적 멤버 함수

6.3. 정적 멤버 함수

7. 상수 표현이 가능한 경우

7.1. 암시적 형변환

  1. 원시 자료형의 static_cast<T>
  2. constexpr 형변환 연산자가 지원되는 클래스의 static_cast<T>
  3. static_cast<T>으로 호환이 가능한 C 방식의 형변환 (T), T()
  4. static_cast<T>으로 호환이 가능한 const_cast<T>
  5. decltype(auto)
  6. decltype(expr)

7.1.1. 참조

  1. 상수 문맥 안의 this
  2. 컴파일 문맥 동안 이루어지는 임시 객체를 만들지 않는 참조 변수 선언 [5]

7.1.2. 포인터

  1. 함수의 포인터로의 형변환
  2. 클래스 멤버 함수의 포인터로의 형변환
  3. 오버로딩된 클래스 멤버 함수를 찾기 위해 정확한 함수 포인터로의 형변환 [6]
  4. nullptrstd::nullptr_t로의 형변환
  5. 배열의 포인터로의 형변환 [7]

7.2. 상수 진리값

7.2.1. static_assert

7.2.2. 조건적 noexcept

noexcept(bool-condition)

7.2.3. if constexpr

Constexpr IfC++17

7.2.4. 조건적 explicit

explicit(bool-condition)C++20

8. 불가능한 경우

  1. 어셈블리
  2. C언어의 가변 인자: vs_arg
  3. 상수 문맥 밖의 this

8.1. 형변환

  1. 형변환 연산자가 constexpr을 지원하지 않는 클래스의 static_cast<T>
  2. static_cast<T>으로 호환이 불가능한 C 방식의 형변환 (T), T()
  3. static_cast<T>으로 호환이 불가능한 const_cast<T>
  4. reinterpret_cast<T>
  5. dynamic_cast<T>


[1] 현재는 Zig 정도가 상수 표현식 기능을 제공한다[2] 함수형 언어에서 함수와 정확히 같다[3] [4] 운영체제 플랫폼 구분 등[5] const T& 는 rvalue를 받으면 임시 객체를 만든다[6] 오버로딩된 멤버 함수는 static_cast<Class::(*)(type param)>(&Class::method)와 같이 얻어내야 한다[7] 반대는 불가능하다


파일:CC-white.svg 이 문서의 내용 중 전체 또는 일부는 문서의 r381에서 가져왔습니다. 이전 역사 보러 가기
파일:CC-white.svg 이 문서의 내용 중 전체 또는 일부는 다른 문서에서 가져왔습니다.
[ 펼치기 · 접기 ]
문서의 r381 (이전 역사)
문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)