[C++ Core] 상수 구문 - const
vs constexpr
상수 구문
C++에서는 상수(constant)의 의미를 부여하기 위해 두 가지의 메커니즘을 제공한다.
constexpr
: 컴파일 시간에 평가한다.const
: 해당 유효범위 내에서는 수정할 수 없다. 불변성을 강조한다.
상수 구문은 컴파일러가 평가(evaluate)할 수 있으며, 정수, 부동소수점 수, 열거형으로 시작하여야 하며, 연산자나, 값을 결과로 생산하는 constexpr
함수로 결합할 수 있다.
상수 구문은 다음의 이유 때문에 쓰인다.
- 상수는 코드의 가독성을 높인다. 특히, 마법의 숫자(magic number)를 피할 수 있다.
- 상수는 유지보수를 쉽게 한다.
- 멀티 스레드 시스템에서 개체 간 자원 경쟁을 피할 수 있다.
- 가끔 컴파일 시간에 무언가를 평가하는 것이 성능 향상으로 이어질 수 있다.
- 시스템 요구사항을 좀 더 직접적으로 표현할 수 있다. (e.g. 임베디드 시스템에서 불변 데이터를 읽기 전용 메모리에 넣는 경우 등)
const
는 C언어 때부터 쓰여온 것이라 익숙할 것이다. 그래서 본 글에서는 constexpr
에 집중하여 기술해보도록 하겠다.
constexpr
앞서 말했듯, cosnt
와 constexpr
의 가장 큰 차이점은 컴파일 시간에 평가가 되는 것이다. '언제 이걸 사용할 수 있지?'라고 고민할 수 있는데, 항상 컴파일 시간에 평가 가능한가? 를 고민해보면 되겠다. 구체적인 예시들을 살펴보자.
간단한 상수 선언
예전 C++에서는 이름 있는 상수를 만들 때, const
를 이용하였다. 이제는 대신에 간단한 상수를 선언할 때는 constexpr
를 사용할 수 있다. 그리고 좀 더 효율적이다.
constexpr int ARRAY_SIZE = 10; constexpr double PI = 3.14159265359;
주소 한정
포인터와 레퍼런스 타입에 대해서는 주소에 대해서 한정지을 수 있다.
constexpr const char* p1 = "asdf"; // 구버전 : const char* const p1 = "asdf"; constexpr const char& p2 = p1[1]; // 구버전 : const char& const p2 = p1[1];
물론 레퍼런스의 주소를 한정하는 건 아무 의미 없겠지만 말이다.
constexpr
함수
const
는 값에 대해서만 한정지울 수 있어, 충분히 컴파일 시간에 할 수 있는 연산도 실행 시간에 해야한다는 단점이 있었다. 구체적인 예시를 통해 보자. 다음은 팩토리얼을 구하는 함수이다.
int factorial(int n) { if (n <= 1) return 1; return n * factorial(n - 1); }
재귀적으로 구하는 팩토리얼 함수이다. 재귀적으로 구현한 함수는 콜 스택이 길어지면 길어질 수록 재귀 트리가 깊어져 실행 시간이 무지막지하게 늘어난다. 이러한 연산을 어떻게하면 줄일 수 있을까 고민한 끝에 사람들은 컴파일 시간에 평가하고 코드를 산출하는 template
의 특성을 이용하여 템플릿 메타프로그래밍(template metaprogramming)을 이용했다.
template <int N> struct Factorial { enum { value = N * Factorial<N - 1>::value }; }; template <> struct Factorial<0> { enum { value = 1 }; };
똑같이 재귀적인 모습이지만 실행 시간 연산을 컴파일 시간 연산으로 옮겨 놓았다. 하지만 템플릿 메타프로그래밍은 가독성이 떨어진다는 단점이 있었다. C++11부터는 constexpr
을 이용하여 가독성을 해치지 않으면서도 같은 성능을 보장하는 함수를 작성할 수 있다.
constexpr int factorial(int n) { if (n <= 1) return 1; return n * factorial(n - 1); }
constexpr
함수를 작성할 때의 정확한 규칙은 여기에서 참고하라. 정리하자면 해당 연산이 상태를 변화시키는가? 를 고려하면 되겠다. 일반 함수뿐만 아니라 메서드 및 연산자 오버로딩에도 당연히 적용된다. 또, 생성자에도 적용할 수 있는데, 이런 생성자를 갖고 있는 타입을 특별히, 리터럴 타입(literal type)이라고 한다.
class Complex { public: constexpr Complex(int inReal, int inImaginary = 0); constexpr Complex(const Complex& other); // 복사 생성자도 만들 수 있음에 주목하자. constexpr int GetReal() { return mReal; } // 구버전 : int GetReal() const { return mReal; } constexpr int GetImaginary() { return mImaginary; } // 구버전 : int GetImaginary() const { return mImaginary; } // 연산자에도 쓸 수 있다. constexpr Complex operator+(const Complex& rhs); constexpr Complex operator-(const Complex& rhs); // 이하 생략.. private: int mReal = 0; int mImaginary = 0; };
이러한 상수 구문을 잘 사용한다면 효율적인 프로그래밍에 도움이 될 것이다.
'Outdated > Core Language' 카테고리의 다른 글
[C++ Core] 함수 개체(function object)와 람다 표현식(lambda expression) (0) | 2018.09.07 |
---|---|
[C++ Core] 상수 구문 - new 연산자 (0) | 2018.08.29 |
[C++ Core] POD(plain old data), 표준 레이아웃 타입(standard layout type), 간단한 타입(trivial type) (0) | 2018.08.17 |
[C++ Core] 우측 값 레퍼런스(rvalue reference) (4) | 2018.08.15 |
[C++ Core] 타입 추론(type deduction) - auto와 decltype (0) | 2018.08.13 |