-
[Effective Modern C++] constexprC++/Effective Modern C++ 2022. 4. 29. 15:23
constexpr은 Constant expression이지만, 기존의 const예약어와는 조금 다릅니다.
개념적으로 constexpr은 해당 값이 상수 뿐 아니라, 컴파일 시점에 알려지는 값을 나타냅니다.
그 외에도 함수, 생성자 등에 적용 가능하다는 특성 또한 존재하며, constexpr이 적용된 식 등이 항상 const하지는 않다는 특징이 있습니다.
단점이 아닌 특징으로, 이것은 constexpr의 기능에 속합니다.
이번 글에서는 constexpr에 대하여 알아보도록 하겠습니다.
1. constexpr객체와 상수성
더보기constexpr객체는 const이지만, const객체는 constexpr이 아닙니다.
이것은 상수 표현식이 필요한 구문에서 차이를 살펴볼 수 있습니다.
예제를 살펴보도록 하겠습니다.
int val ... constexpr auto arr_size_1 = val; const auto arr_size_2 = val; const auto arr_size_3 = 10; ... std::array<int, arr_size_1> arr1; std::array<int, arr_size_2> arr2; std::array<int, arr_size_3> arr3;
위의 구문들은 다음과 같이 해석할 수 있습니다.
arr_size_1 Error arr1 Error arr_size_2 const int arr2 Error arr_size_3 constexpr int arr3 std::array<int> constexpr은 컴파일 시점에 알려지는 값을 나타냅니다. 따라서 컴파일 시점에 값이 결정되지 않은 val을 arr_size_1에 대입할 수 없습니다.
또한 배열 크기, std::array의 길이, 열거자의 값 등 상수 표현식이 필요한 구문들은 컴파일 시점에 그 값이 알려져 있어야 합니다. 따라서 arr1, arr2 또한 컴파일이 되지 않습니다.
const객체가 컴파일 시점에 값이 초기화되지 않을 수 있다는 점 만으로도, constexpr과 const의 차이는 뚜렷하다고 생각합니다.
2. constexpr함수와 파라미터
더보기constexpr함수는 파라미터의 종류에 따라 달라집니다.
- 파라미터가 컴파일 시점 상수일 경우, 함수의 결과가 컴파일 도중 계산됩니다. 따라서, 컴파일 시점 상수를 요구하는 문맥에 사용할 수 있게 됩니다. 만약 인수의 값이 컴파일 시점에 알려지지 않을 경우 컴파일이 거부됩니다.
- 파라미터 중 컴파일 시점에 알려지지 않는 값이 존재할 경우, 일반 함수처럼 작동합니다. 실행시점에 값이 계산되고, 컴파일 시점 상수가 필요한 문맥에 사용할 수 없습니다.
이는 같은 함수가 컴파일 시점, 실행 시점에 나뉘어야 할 경우 분리해서 구현할 필요가 없다는 것을 의미합니다.
예제를 살펴보도록 하겠습니다.
#include <iostream> using namespace std; constexpr int function(int val) { return val + 5; } int main() { int size = 5; std::array<int, function(5)> arr_1; std::array<int, function(size)> arr_2; }
C2975 : '_Size': 'std::array'의 템플릿 인수가 잘못되었습니다. 컴파일 타임 상수 식이 필요합니다.
위 constexpr 함수인 function을 이용하여 std::array를 초기화 하는 두 가지 식이 있습니다.
첫 번째 식은 상수를 파라미터로 했기 때문에 컴파일 시점 상수가 되어 문제없이 컴파일 되지만, 두 번째 식은 함수의 결과가 실행 시점에 계산되기 때문에 컴파일 되지 않습니다.
3. constexpr함수의 제약
더보기constexpr함수는 반드시 컴파일 시점에 결과를 산출할 수 있어야 합니다. 따라서 제약이 일부 존재하는데, 그 제약들이 C++11과 C++14에서 조금 차이점을 보입니다.
우선, C++11에서 constexpr함수의 구현부는 최대 한 줄 입니다. 일반적으로 void가 아닌 이상, return 구문이 그 한 줄을 차지하게 될 것입니다.
이러햔 제약 때문에 constexpr함수에서 할 수 있는 일이 많지 않은데, 이것을 그나마 확장시킬 수 있는 방법이 삼항 연산자와 재귀호출 입니다. 두 방법을 이용해서 거듭제곱 함수를 만들 경우 아래와 같이 구현할 수 있습니다.
constexpr int pow(int base, int exp) { return (exp == 0 ? 1 : base * pow(base, exp - 1)); }
이런 구현부의 제한은 C++14에서 사라졌습니다.
constexpr함수는 반드시 리터럴 타입을 파라미터로 받고, 넘겨주어야 합니다.
리터럴 타입은 컴파일 시점에 값을 결정할 수 있는 형식이며, 기본 자료형 (C++11에서는 void 제외) 및 constexpr인 사용자 형식이 리터럴 타입이 될 수 있습니다.
사용자 형식이 constexpr이 되려면 생성자와 적절한 멤버 함수가 constexpr이 되어야 합니다.
#include <iostream> using namespace std; class MyClass { public: constexpr MyClass(int val = 0) noexcept : data(val){} constexpr int get_data() const noexcept { return data; } void set_data(int val) noexcept { data = val; } private: int data; }; int main(){ constexpr MyClass a(10); }
위와 같이 constexpr로 선언된 생성자가 있는 MyClass는 constexpr로 선언이 가능합니다.
위 코드의 set_data가 constexpr이 아닌 이유는 두 가지 이며, 이는 C++11에서만 해당됩니다. C++14에서는 해당 내용들이 사라져 constexpr로 선언할 수 있습니다.
- constexpr멤버 함수는 암묵적으로 const로 선언됩니다. 따라서 값을 수정하는 코드를 사용할 수 없습니다.
- 예제 이전 문단에서 언급되었듯, C++11에서는 void가 리터럴 타입에 속하지 않습니다.
위와 같이 constexpr객체를 생성하는 것은 그러한 객체를 읽기 전용 메모리에 생성할 수 있다는 의미입니다.
또한 이것은 실행 시점에 발생하는 계산을 컴파일 시점으로 옮길 수 있다는 것을 의미하고, 이는 곧 실행 속도의 향상을 의미합니다.
constexpr을 사용할 경우 함수, 객체가 사용되는 범위를 확장할 수 있습니다.
또한 실행 시점의 연산 일부를 컴파일 시점으로 옮기는 것으로, 컴파일 시간이 늘어나게 되지만 실행 시간을 단축 시킬 수 있다는 특징도 있습니다.
하지만 constexpr을 사용할 때 주의해야 할 점도 존재하는데, constexpr이 함수, 객체의 인터페이스라는 점 입니다.
noexcept에 관해 살펴보았을 때 처럼, constexpr을 사용할 때에는 이 설계가 얼마나 유지될 수 있는지 생각해보아야 할 것입니다.
특히 디버깅, 혹은 특정 성능 측정 관련 이슈로 인해 입출력 기능을 추가하게 될 경우 constexpr이 빠지게 될 수 있는데, 이 경우 클라이언트 코드에서 실행이 불가능한 상황이 발생할 수 있습니다.
확장성 및 성능을 위해 constexpr을 최대한 사용하는 것이 좋겠지만, 고려해보아야 할 것은 확실하게 고려하는 것이 좋겠습니다.
감사합니다.
'C++ > Effective Modern C++' 카테고리의 다른 글
[Effective Modern C++] Special member function (0) 2022.05.05 [Effective Modern C++] const와 Thread safety (0) 2022.05.03 [Effective Modern C++] noexcept (0) 2022.04.28 [Effective Modern C++] const_iterator (0) 2022.04.27 [Effective Modern C++] 식별자 : override (0) 2022.04.22