[include(틀:관련 문서, top1=C++, top2=C++/문법/클래스, top3=컴파일러, top4=Zig)] [include(틀:상위 문서, top1=C++/문법)] [include(틀:프로그래밍 언어 문법)] [목차] == 개요 == [[https://en.cppreference.com/w/cpp/language/constant_expression|Constant Expressions]] C++11 부터 도입된 상수 표현식, 또는 상수식은 실제 코드가 실행되는 사용자 시점(런타임)이 아니라 컴파일 시점으로 코드의 평가를 앞당길 수 있는 획기적인 기능이다. C++의 킬러 요소라고 말할 수 있는 핵심 기능이다 [* 현재는 [[Zig]] 정도가 상수 표현식 기능을 제공한다]. 상수 표현식의 의미는 해당 코드는 [[결정론]]적인 동작을 하며, 초기 조건이 주어지면 유한한 절차의 알고리즘을 통해 무조건 결과를 알 수 있는 표현식이란 뜻이다. 그리고 결정론적인 코드란 로컬 머신안에서, 사용자의 코드에서 결정할 수 있는 코드다. 바이너리가 생성되는 컴파일 시점에 실행 결과가 결정되기 때문에 아무런 평가 과정도 컴파일 이후에 남지 않는다. 곧 프로그램 이용자의 실행 시점에는 코드 실행 시간이 0이 되도록 최적화된다. 예를 들어 상수 시간에 실행되는 함수는 실행 시점(런타임)이 아니라 컴파일 시점(컴파일 타임), 심지어는 [[IDE]]에서 바로 값을 볼 수도 있다. 이토록 강력한 기능이지만 상수 표현식을 사용할 수 있는 조건은 제한되어 있으며 작성하기 까다로운 부분이 존재한다. 그래도 조건은 계속 완화되고 있다. C++14까지는 일부 생성자와 getter 함수나 지원했지만 C++23에 오면 IO, 동시성 프로그램 말고는 죄다 상수식으로 취급한다고 보면 된다. ---- [[https://en.cppreference.com/w/cpp/language/translation_phases|Phases of Translation]] 컴파일 과정의 9번째 단계에서 상수 표현식을 실행한다. == 자명성 == 컴파일 시점에 결과를 결정하는 기능을 추가할 수 있었던 건 [[C++/문법/클래스|클래스]] 문서에서도 설명한 자명한 자료형의 덕이 컸다. 자명하다는 것은 방정식에 명백한 해가 존재한다는 뜻이다. 자명한 자료형은 어떠한 외부 의존성없이 스스로 존재하고 스스로 파괴되고 스스로 값을 결정할 수 있다. 다시 말하면 프로그램의 필요한 메모리를 컴파일 시간에 알 수 있고, 어떤 동작을 할 지 컴파일 시간에 알 수 있고, 마지막에 어떻게 소멸하는지 컴파일 시간에 알 수 있다. 이것을 [[결정론]]적이다 라고 한다. 하지만 자명함에는 조건이 따른다. 결정론적인 코드는 초기값이 들어오면 반드시 상수 시간에 결과를 알 수 있어야 하는데 외부 의존성이 있으면 이 과정이 유한할지 무한할지, 성공할지 실패할 지 알 수가 없다. 여기서 외부 의존성이란 외부 연결을 통해 가져온 함수, 그리고 [[운영체제]] 호출을 말한다. 오직 사용자의 코드 안에서, 모든 것을 결정할 수 있어야 한다. 시스템 호출 때문에 당장 실패할지 성공할지 모르는 코드는 상수식이 아닌 것이다. 외부 라이브러리의 경우 같이 제공되는 헤더 파일에 완전히 정의된 클래스나 함수가 제공되면 상수식으로 만들 수 있다. 하지만 보통 동적 라이브러리의 경우 라이브러리의 초기화, 정리, 함수 호출에 운영체제의 도움이 필요하다. 정적 라이브러리는 컴파일러가 맡지만 역시 사용자 단에서는 정적 라이브러리 파일에서 가져온 함수의 정의를 알 수 없다. 모든 원시자료형은 자명한 자료형이다. 모든 종류의 열거형은 자명한 자료형이다. 클래스의 인스턴스를 컴파일 시간에 만들 때는 그 클래스가 자명해야(Trivial) 한다. [[C++/문법/클래스|클래스]]에서 자명한 클래스의 조건을 다시 보면은, 오직 단일한 {{{default}}} 생성자, {{{default}}} 소멸자, 복사 혹은 이동 생성자를 갖고 있으면 {{{default}}}여야 하고, 복사 혹은 이동 대입 연산자를 갖고 있으면 {{{default}}}여야 한다. {{{virtual}}} 소멸자를 갖고 있으면 안되고, 가상 클래스를 상속받아도 안되고, 추상 클래스이면 안되고, 참조 필드를 가질 수 없다. 또한 모든 비정적 데이터 멤버도 상기한 조건을 만족해야 하며, 마찬가지로 상속 구조에서 모든 부모 클래스도 상기한 조건을 만족해야 한다. 사실 가상 클래스나 운영체제 파생 클래스를 안쓰면 그럭저럭 만족시킬 수 있지만 {{{default}}} 생성자, {{{default}}} 소멸자가 C++ 클래스 응용에 제약을 많이 줘서 많이 쓰이진 않는다. 마지막으로 어떤 자료형의 것이든 간에 모든 종류의 포인터도 자명한 자료형이다. {{{const T*}}}, {{{T *const}}} 상관없이. 모든 종류의 포인터라는 건 미완성된 클래스 혹은 운영체제 객체의 포인터도 포함된다. 이를 이용해서 자명한 클래스의 조건을 조금 우회할 수 있다. 하지만 포인터에서 객체를 얻어내는 순간 컴파일 문맥에서 탈출하므로 메모리 할당 외에는 용도가 제한된다. == 변수 == === constexpr 상수 === ||{{{#!wiki style="font-size:1.06em" {{{#DodgerBlue,#CornFlowerBlue '''{{{constexpr}}}'''}}}{{{#LightSeaGreen,#DarkTurquoise {{{ 자료형}}}}}}{{{#DarkGray,#SlateGray ''{{{ 식별자}}}''}}}{{{ = }}}''{{{값}}}''{{{;}}} {{{#DodgerBlue,#CornFlowerBlue '''{{{inline}}}'''}}}{{{#DodgerBlue,#CornFlowerBlue '''{{{constexpr}}}'''}}}{{{#LightSeaGreen,#DarkTurquoise {{{ 자료형}}}}}}{{{#DarkGray,#SlateGray ''{{{ 식별자}}}''}}}{{{ = }}}''{{{값}}}''{{{;}}} }}}|| '''{{{constexpr}}}'''^^{{{-1 {{{#008000,#a3ff84 C++11}}} }}}^^ 자료형 앞에 평가 지시자 {{{constexpr}}}를 덧붙여 컴파일 시점에 값이 초기화되는 상수임을 나타낼 수 있다. 또한 선택적으로 {{{static}}}, {{{extern}}}, {{{thread_local}}}, {{{inline}}} 등의 연결성 지시자도 붙일 수 있다. {{{constexpr}}} 변수는 무조건 상수({{{const}}})이며, 컴파일 시점에 값이 정해진다. 그렇기 때문에 실행 시점에는 변수의 생성 실패, 메모리 오버헤드 등 어떠한 문제도 발생하지 않는다. [[https://en.cppreference.com/w/cpp/language/inline|{{{inline}}}]] {{{inline}}}을 붙이면 아예 컴파일 시점에 모든 변수가 값으로 바뀌게 컴파일러를 유도할 수 있다. 이름공간 안에 있는 {{{inline}}}인 {{{const}}} 상수는 외부 연결이 적용된다. 마찬가지로 이름공간 안에 있는 {{{inline constexpr}}} 상수도 외부 연결이 적용되어 선언만 있고 값을 즉시 할당하지 않아도 괜찮다. 코드 어딘가에 그 {{{inline constexpr}}} 상수에 값을 할당하는 구문이 있으면 반드시 컴파일 시점에 값을 할당한다. 하지만 이름이 같은데 자료형이 다른 {{{inline constexpr}}} 상수가 있으면 컴파일 오류가 발생하므로 유의해야 한다. 정적인 {{{static constexpr}}} 상수는 반드시 {{{inline}}}이며 곧 {{{static inline constexpr}}}을 암시한다. === constinit 초기화 === '''{{{constinit}}}'''^^{{{-1 {{{#008000,#a3ff84 C++20}}} }}}^^ 자료형 앞에 평가 지시자 {{{constinit}}}을 덧붙여 컴파일 시점에 값이 초기화되는 변수임을 나타낼 수 있다. 또한 선택적으로 {{{static}}}, {{{extern}}}, {{{thread_local}}}, {{{inline}}} 등의 연결성 지시자도 붙일 수 있다. {{{constinit}}}는 오직 변수의 상수 시간 초기화를 위해 추가된 기능이다. {{{constexpr}}}에서 초기화 기능만 이용하는 셈이다. 그래서 {{{constinit}}} 변수는 {{{constexpr}}} 변수와는 다르게 상수가 아니다. 할 이유는 없지만 {{{constinit}}} {{{const}}} 상수는 {{{constexpr}}}과 같은 동작을 한다. == 컴파일 문맥 == === 표준 라이브러리: is_constant_evaluated() === ||{{{#!wiki style="font-size:1.06em" {{{[[nodiscard]]}}}{{{#DodgerBlue,#CornFlowerBlue '''{{{ constexpr}}}'''}}}{{{#DodgerBlue,#CornFlowerBlue '''{{{ bool}}}'''}}}{{{#F97575,#IndianRed {{{ is_constant_evaluated}}}}}}{{{();}}} }}}|| {{{std::is_constant_evaluated()}}}^^{{{-1 {{{#008000,#a3ff84 C++11}}} }}}^^ === if consteval === {{{if consteval}}}^^{{{-1 {{{#008000,#a3ff84 C++20}}} }}}^^ == 함수 == === constexpr 함수 === ||{{{#!wiki style="font-size:1.06em" ''{{{#DarkGray,#SlateGray {{{[[특성]]}}}}}}'' {{{#DodgerBlue,#CornFlowerBlue '''{{{constexpr}}}'''}}}{{{#LightSeaGreen,#DarkTurquoise {{{ 반환 자료형}}}}}}{{{#F97575,#IndianRed {{{ 함수 식별자}}}}}}{{{(}}}''{{{#LightSeaGreen,#DarkTurquoise {{{매개변수1-자료형}}}}}}'' ''{{{#DarkGray,#SlateGray {{{ 매개변수1}}}}}}''{{{, }}}''{{{#LightSeaGreen,#DarkTurquoise {{{매개변수2-자료형}}}}}}'' ''{{{#DarkGray,#SlateGray {{{ 매개변수2}}}}}}''{{{, ...)}}} {{{{}}} ... {{{}}}} }}}|| '''{{{constexpr}}}'''^^{{{-1 {{{#008000,#a3ff84 C++11}}} }}}^^ 해당 함수가 상수 표현식임을 나타낸다. {{{constexpr}}} 함수는 무조건 {{{inline}}}이어야 한다. 따라서 함수의 선언과 정의를 같이 해야 한다. 자료형 앞에 {{{constexpr}}}^^{{{-1 {{{#008000,#a3ff84 C++11}}} }}}^^ 이나 {{{consteval}}}^^{{{-1 {{{#008000,#a3ff84 C++20}}} }}}^^ 평가 지시자를 덧붙여 컴파일 시점에 결과가 정해질 수 있는 함수임을 나타낼 수 있다. 또한 선택적으로 {{{static}}}, {{{extern}}}, {{{inline}}} 등의 연결성 지시자도 붙일 수 있다. 상수 표현식 함수 사용에는 커다란 제약이 있다. 평가가 컴파일 시점에 이루어져야 하기 때문에 반환 자료형이 자명해야 한다. 또한 함수의 지역변수도 자명한 자료형이어야 한다. 때문에 사용할 수 있는 자료형의 제한이 매우 크다. 다행히 매개변수의 경우 포인터나 참조형이라도 문제가 없다. C++11에서는 진짜 [[시간 복잡도|O(1)]]인 함수였는데 C++17에서는 ''가능하다면 컴파일 시점에 평가해야 하는 함수''가 되었다. C++20에선 동적 메모리 할당 조차 컴파일 시점에 실행될 수 있다. 이러면 할당 위치만 힙이고 작동은 스택처럼 구현되어 힙의 내용이 컴파일 시점에 결정된다. 이런 식으로 동적 배열 클래스 {{{std::vector}}}, 문자열 클래스 {{{std::string}}}가 {{{constexpr}}} 생성자, 소멸자와 메서드를 얻었다. 상수 표현식의 내용이 포괄적으로 바뀐 셈인데, 좋을 것만 같지만 이러다 보니 컴파일 시간이 너무 오래 걸리고, 키워드의 의미가 배보다 배꼽이 더 커지는 일이 일어났다. 때문에 매크로도 대체할 겸 진짜 의도를 되살리기 위해 후술할 {{{consteval}}} 키워드가 도입되었다. C++23에 와서는 진짜로 ''컴파일 시점에 평가될 수도 있는 함수''가 되었다. 아예 {{{constexpr}}} 함수 안에서 {{{constexpr}}}가 아닌 함수를 호출할 수 있다. 왜냐하면 함수가 실행되면 그저 컴파일 문맥이 아닐 뿐 {{{constexpr}}}의 의미를 해치는 건 아니기 때문이다. === consteval 함수 === ||{{{#!wiki style="font-size:1.06em" ''{{{#DarkGray,#SlateGray {{{[[특성]]}}}}}}'' {{{#DodgerBlue,#CornFlowerBlue '''{{{consteval}}}'''}}}{{{#LightSeaGreen,#DarkTurquoise {{{ 반환 자료형}}}}}}{{{#F97575,#IndianRed {{{ 함수 식별자}}}}}}{{{(}}}''{{{#LightSeaGreen,#DarkTurquoise {{{매개변수1-자료형}}}}}}'' ''{{{#DarkGray,#SlateGray {{{ 매개변수1}}}}}}''{{{, }}}''{{{#LightSeaGreen,#DarkTurquoise {{{매개변수2-자료형}}}}}}'' ''{{{#DarkGray,#SlateGray {{{ 매개변수2}}}}}}''{{{, ...)}}} {{{{}}} ... {{{}}}} }}}|| '''{{{consteval}}}'''^^{{{-1 {{{#008000,#a3ff84 C++20}}} }}}^^ 해당 함수가 상수 표현식이고, 문맥 상으로도 반드시 컴파일 시점에 평가 되어야함을 나타낸다. 보면 알겠지만 {{{constexpr}}}의 처음 도입조건보다 더 엄격한 조건을 갖고 있다. 함수 내부와 매개 변수에서도 정적이고 컴파일 시점에 결정되는 자료형 또는 객체가 아니면 참조할 수 없다. 컴파일 시점에 값이 결정된다는 것은, 사용자 입장에서는 언제나 고정된 값으로 보인다는 뜻이다. 때문에 {{{consteval}}} 함수는 즉발 함수(Immediate Function)라고 불리며 어떤 [[Haskell|부작용(Side Effect)]] 없이 독립적으로 실행되는 함수다[* [[함수형 언어]]에서 함수와 정확히 같다]. 그래서 {{{consteval}}} 함수는 코드 상에서만 함수로 보이는, 사실상 상수라고 봐야 한다. 이 지시자의 의의는 컴파일러 전용 함수를 C++에 구현한다는 것에 있다. {{{constexpr}}} 상수[* ]는 C의 전처리기 키워드를 대체할 수 있었다. {{{consteval}}}은 이제 위험한 매크로의 일부를 대체할 수 있다. 예를 들어서 전처리기 분기를 통해[* 운영체제 플랫폼 구분 등] 어떤 상수를 선언한다면, 예전에는 전처리기와 매크로 지옥에서 빠져나오지 못했다. 이런 전처리기 구문들은 프로그램 헤더 구조의 저 멀리 최상단에 놓이게 될텐데 때문에 중복되는 헤더 삽입, 전처리기 키워드 중복 문제가 발생한다. 그러나 이젠 {{{consteval}}}에서 C++ 코드를 통해 밖으로 보이지 않고 처리가 가능하다. == 클래스 == === 정적 데이터 멤버 === '''[[메타 데이터]] (Meta Data)''' === 비정적 멤버 함수 === === 정적 멤버 함수 === == 상수 표현이 가능한 경우 == === 암시적 형변환 === 1. 원시 자료형의 {{{static_cast}}} 1. {{{constexpr}}} 형변환 연산자가 지원되는 클래스의 {{{static_cast}}} 1. {{{static_cast}}}으로 호환이 가능한 [[C언어|C]] 방식의 형변환 {{{(T)}}}, {{{T()}}} 1. {{{static_cast}}}으로 호환이 가능한 {{{const_cast}}} 1. {{{decltype(auto)}}} 1. {{{decltype(expr)}}} ==== 참조 ==== 1. 상수 문맥 안의 {{{this}}} 1. 컴파일 문맥 동안 이루어지는 임시 객체를 만들지 않는 참조 변수 선언 [* const T& 는 rvalue를 받으면 임시 객체를 만든다] ==== 포인터 ==== 1. 함수의 포인터로의 형변환 1. 클래스 멤버 함수의 포인터로의 형변환 1. 오버로딩된 클래스 멤버 함수를 찾기 위해 정확한 함수 포인터로의 형변환 [* 오버로딩된 멤버 함수는 static_cast(&Class::method)와 같이 얻어내야 한다] 1. {{{nullptr}}}의 {{{std::nullptr_t}}}로의 형변환 1. 배열의 포인터로의 형변환 [* 반대는 불가능하다] === 상수 진리값 === ==== static_assert ==== ==== 조건적 noexcept ==== {{{noexcept(bool-condition)}}} ==== if constexpr ==== '''{{{Constexpr If}}}'''^^{{{-1 {{{#008000,#a3ff84 C++17}}} }}}^^ ==== 조건적 explicit ==== {{{explicit(bool-condition)}}}^^{{{-1 {{{#008000,#a3ff84 C++20}}} }}}^^ == 불가능한 경우 == 1. 어셈블리 1. [[C언어]]의 가변 인자: {{{vs_arg}}} 1. 상수 문맥 밖의 {{{this}}} === 형변환 === 1. 형변환 연산자가 {{{constexpr}}}을 지원하지 않는 클래스의 {{{static_cast}}} 1. {{{static_cast}}}으로 호환이 불가능한 [[C언어|C]] 방식의 형변환 {{{(T)}}}, {{{T()}}} 1. {{{static_cast}}}으로 호환이 불가능한 {{{const_cast}}} 1. {{{reinterpret_cast}}} 1. {{{dynamic_cast}}} [각주][include(틀:문서 가져옴, title=C++/문법, version=381, paragraph=8)] [[분류:프로그래밍 언어 문법]]