Item 15. 가능하면 항상 constexpr을 사용해라


상수 표현식(constant expression)

컴파일러가 컴파일 시점에 어떠한 식의 값을 결정할 수 있다면 해당 식을 상수 표현식(constant expression)이라고 한다.

또한, 이러한 상수 표현식들 중 값이 정수인 것들을 정수 상수 표현식(integral constant expression)이라 부른다.

정수 상수 표현식은 배열 크기, 정수 템플릿 인수, 열거자 값 등을 지정할 때 사용할 수 있다.

// 아래 코드는 number가 정수 상수 표현식이어야만 컴파일이 가능하다.

// 배열 크기로 정수 상수 표현식을 사용
int arr[number];

// 정수 템플릿의 인자로 정수 상수 표현식을 사용
template <int N>
class A {
  int operator()() { return N; }
}
A<number> a;

// 열거자 값으로 정수 상수 표현식을 사용
enum A { a = number, b, c};


constexpr

C++11부터 도입된 constexpr 키워드는 해당 식이 상수 표현식임을 명시해주는 키워드이다.

constexpr 키워드로 객체를 선언하면 해당 객체는 컴파일 시점의 상수 표현식으로 정의된다. (선언과 함께 초기화되어야 한다.)

constexpr int number = 10;

// 배열 크기로 정수 상수 표현식으로 사용
int arr[number];

// 정수 템플릿의 인자로 정수 상수 표현식을 사용
template <int N>
class A {
  int operator()() { return N; }
}
A<number> a;

// 열거자 값으로 정수 상수 표현식을 사용
enum A { a = number, b, c};

constexprconst 키워드 모두 한번 정한 값을 변경할 수 없는 상수지만 constexpr은 반드시 컴파일 시점에 값이 정해져야 한다.

int sz; // 비 constexpr 변수

constexpr auto arraySize1 = sz;    // 에러, sz의 값이 컴파일 시점에 알려지지 않음
std::array<int, sz> data1;         // 에러, sz의 값이 컴파일 시점에 알려지지 않음

const auto arraySize2 = sz;        // 가능, arraySize2는 sz의 const 복사본
std::array<int, arraySize2> data2; // 에러, arraySize2의 값이 컴파일 시점에 알려지지 않음

constexpr auto arraySize3 = 10;    // 가능
std::array<int, arraySize3> data2; // 가능, arraySize3는 constexpr 객체


constexpr 함수

constexpr 함수는 컴파일 시점에 알려지는 값을 인자로 호출하는 경우, 컴파일 시점의 결과를 산출한다.

인자의 값이 컴파일 시점에 알려지지 않는다면 constexpr 함수는 컴파일 시점에 실행할 수 없고 일반 함수처럼 동작한다. (이 경우 constness도 보장할 수 없다.)

컴파일 도중에 $3^n$의 크기를 갖는 배열을 구현한다고 가정해보자.

C++ 표준 라이브러리 함수로 밑(base)을 지수(exponent)로 제곱한 값을 구하는 std::pow 함수가 있지만 두 가지 문제점 때문에 사용할 수 없다.

  1. std::pow 함수는 리턴 타입이 int가 아닌 double이다.
  2. std::pow 함수는 컴파일 시점에 값이 결정되는 constexpr이 아니다.

이러한 문제로 pow 함수를 직접 작성해야 한다.

// 밑(base)을 지수(exponent)로 제곱한 값을 구하는 pow 함수
constexpr int pow(int base, int exp) noexcept {
  // 구현은 아래서 하겠다.
}

constexpr auto num = 5;               // 지수의 값
std::array<int, pow(3, num)> results; // 3^n의 크기를 갖는 배열

// 만약, pow 함수의 인자가 컴파일 시점에 알려지지 않으면 일반 함수처럼 호출
auto base = readFromDB("base");
auto exp = readFromDB("exponent");

auto baseToExp = pow(base, exp);

constexpr 함수를 작성할 때는 제약이 존재하는데 C++11과 C++14의 차이가 존재한다.

// C++11에서는 constexpr 함수는 실행 가능한 문장이 최대 1개
constexpr int pow(int base, int exp) noexcept {
  return (exp == 0 ? 1 : base * pow(base, exp - 1));
}

// C++14에서는 constexpr 함수는 실행 가능한 문장의 제약이 없음
constexpr int pow(int base, int exp) noexcept {
  auto result = 1;
  for (int i = 0; i < exp; ++i) {
    result *= base;
  }

  return result;
}


또한 constexpr 함수는 반드시 리터럴 타입(literal type)들을 받고 돌려줘야 한다.

리터럴 타입(literal type)은 컴파일 도중에 값을 결정할 수 있는 타입을 말한다.

C++11에서 void를 제외한 모든 내장 타입은 리터럴 타입에 해당한다.

class Point {
public:
  // 컴파일 시점에 값을 알 수 있다면 constexpr 생성자도 가능
  constexpr Point(double xVal = 0, double yVal = 0) noexcept : x(xVal), y(yVal) {}

  constexpr double xValue(void) const noexcept { return x; }
  constexpr double yValue(void) const noexcept { return y; }

  // C++11에서 void는 리터럴 타입이 아님
  void setX(double newX) noexcept { x = newX; }
  void setY(double newY) noexcept { y = newY; }

private:
  double x, y;  
};

// constexpr 생성자가 컴파일 타입에 실행
constexpr Point p1(9.4, 27.7);
constexpr Point p2(28.8, 5.3);

constexpr Point midpoint(const Point& p1, const Point& p2) noexcept {
  return { (p1.xValue() + p2.xValue()) / 2, (p1.yValue() + p2.yValue()) / 2 };
}

// constexpr 함수의 결과를 이용해 constexpr 객체 초기화
constexpr auto mid = midpoint(p1, p2);

앞서 C++11에서는 setX, setY 함수가 두 가지 문제 때문에 불가능했다.

  1. 해당 함수는 멤버 변수를 수정하는 setter 함수인데 C++11에서는 constexpr 객체가 암묵적으로 const로 선언됨
  2. 해당 함수는 리턴 타입이 void인데 C++11에서는 void는 리터럴 타입이 아님

하지만 C++14에서는 두 가지 제약이 모두 사라졌다.

class Point {
public:
  // C++14부터는 가능
  constexpr void setX(double newX) noexcept { x = newX; }
  constexpr void setY(double newY) noexcept { y = newY; }

  // 나머지 멤버 함수, 멤버 변수는 위와 동일
};

// 원점을 기준으로 p와 대칭인 Point 객체를 반환
constexpr Point reflection(const Point& p) noexcept {
  Point result;

  result.setX(-p.xValue());
  result.setY(-p.yValue());

  return result;
}

// 컴파일 시점에 전부 가능
constexpr Point p1(9.4, 27.7);
constexpr Point p2(28.8, 5.3);
constexpr auto mid = midpoint(p1, p2);
constexpr auto reflectedMid = reflection(mid);

constexpr 키워드는 객체나 함수 인터페이스의 일부이므로 설계 단계에서부터 정확하게 결정해야 클라이언트 측에서 문제없이 사용할 수 있다. (중간에 constexpr를 빼버리면 큰 문제가 생길 수 있다.)


카테고리:

업데이트: