Outdated/Core Language

[C++ Core] 상수 구문 - const vs constexpr

해달 2018. 8. 21. 12:30

[C++ Core] 상수 구문 - const vs constexpr

상수 구문

C++에서는 상수(constant)의 의미를 부여하기 위해 두 가지의 메커니즘을 제공한다.

  • constexpr : 컴파일 시간에 평가한다.
  • const : 해당 유효범위 내에서는 수정할 수 없다. 불변성을 강조한다.

상수 구문은 컴파일러가 평가(evaluate)할 수 있으며, 정수, 부동소수점 수, 열거형으로 시작하여야 하며, 연산자나, 값을 결과로 생산하는 constexpr 함수로 결합할 수 있다.

상수 구문은 다음의 이유 때문에 쓰인다.

  1. 상수는 코드의 가독성을 높인다. 특히, 마법의 숫자(magic number)를 피할 수 있다.
  2. 상수는 유지보수를 쉽게 한다.
  3. 멀티 스레드 시스템에서 개체 간 자원 경쟁을 피할 수 있다.
  4. 가끔 컴파일 시간에 무언가를 평가하는 것이 성능 향상으로 이어질 수 있다.
  5. 시스템 요구사항을 좀 더 직접적으로 표현할 수 있다. (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;
};

이러한 상수 구문을 잘 사용한다면 효율적인 프로그래밍에 도움이 될 것이다.