KoreanFoodie's Study

Effective Modern C++ | 항목 15 : 가능하면 항상 constexpr 를 사용하라 본문

Tutorials/C++ : Advanced

Effective Modern C++ | 항목 15 : 가능하면 항상 constexpr 를 사용하라

GoldGiver 2022. 10. 26. 09:54

C++ 프로그래머의 필독서이자 바이블인, 스콧 마이어스의 Modern Effective C++ 를 읽고 기억할 내용을 요약하고 있습니다. 꼭 읽어보시길 추천드립니다!

항목 15 : 가능하면 항상 constexpr 를 사용하라

핵심 :

1. constexpr 객체는 const 이며, 컴파일 도중에 알려지는 값들로 초기화된다.
2. constexpr 함수는 그 값이 컴파일 도중에 알려지는 인수들로 호출하는 경우에는 컴파일 시점 결과를 산출한다.
3. constexpr 객체나 함수는 비constexpr 객체나 함수보다 광범위한 문맥에서 사용할 수 있다.
4. constexpr 은 객체나 함수의 인터페이스의 일부이다.


개념적으로, constexpr 는 어떠한 값이 단지 상수일 뿐만 아니라 컴파일 시점에서 알려진다는 점을 나타낸다. 하지만 constexpr 의 결과가 반드시 const 는 아니며, 그 값이 컴파일 시점에서 알려진다는 보장은 없다. 이번 항목에서는 constexpr 의 실제 예시를 보며 이런 점들을 파헤칠 것이다.
우선 constexpr 이 객체에 적용된 경우부터 보자. constexpr 이 적용된 객체는 const 이며, 값이 컴파일 시점에서 알려진다. 그래서 읽기 전용 메모리에 배치될 수도 있으며, 정수 상수 표현식(integral constant expression) 이 요구되는 문맥에서 사용될 수 있다(배열 크기, 열거자 값, alignment 지정자 등).

int sz;

// 오류! sz 값이 컴파일 도중에 알려지지 않음
constexpr auto arraySize1 = sz;

// 오류! sz 값이 컴파일 도중에 알려지지 않음
std::array<int, sz> data1;

// OK
constexpr auto arraySize2 = 10;

// OK
std::array<int, arraySize2> data2;

...

// OK. 그냥 const 라는 뜻
const auto arraySize = sz;

// 오류! sz 값이 컴파일 도중에 알려지지 않음
std::array<int, arraySize> data;

위의 코드블락에서 마지막 예시에서 알 수 있는 것은, 모든 constexpr 객체는 const 이지만 모든 const 객체가 constexpr 인 것은 아니라는 것이다!

그렇다면 함수는 어떨까? constexpr 함수의 동작을 정리하면 다음과 같다.

  • 컴파일 시점 상수를 요구하는 문맥에 constexpr 를 사용할 수 있다. 이때 만일 인자의 값이 컴파일 시점에서 알려지면, 함수의 결과가 컴파일 시점에 계산된다. 인자의 값이 컴파일 시점에 알려지지 않는다면, 코드의 컴파일이 거부된다.
  • 컴파일 시점에서 알려지지 않는 하나 이상의 값들로 constexpr 를 호출하면 함수는 보통의 함수처럼 작동한다(런타임에 계산). 이는 같은 연산을 수행하는 함수를 두 버전(constexpr / 비constexpr) 로 나눌 필요 없이 하나의 constexpr 함수를 두 가지 용도로 사용하면 된다는 의미이다.


C++11 에서는 constexpr 함수는 실행 가능 문장이 많아야 하나(보통 return 문)이어야 하는데, "?:" 문법과 재귀를 이용하면 이런 제약을 어느 정도 극복할 수 있다. C++14 에서는 제약이 느슨해져서 조금 여유롭게 구현이 가능하다.
base^exp 길이의 array 를 만드는 constexpr 함수를 구현해야 한다고 가정해 보자.

// C++11 버전, 함수형
constexpr int pow(int base, int exp) noexcept
{
  return base * (exp == 0 ? 1 : pow(base, exp-1));
}


// C++14 버전
constexpr int pow(int base, int exp) noexcept
{
  int result = 1;
  for (int i = 0; i < exp; ++i) result *= base;
  return result;
}

...

constexpr auto numConds = 5;

std::array<pow(3, numConds)> results;

위의 코드를 통해, C++11 과 C++14 버전을 비교할 수 있을 것이다.

또 다른 예시를 보면서 이 항목을 마무리 하자.

class Point {
public:
  constexpr Point(double xVal = 0, double yVal = 0) noexcept
  : x(xVal), y(yVal)
  {}

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

  void setX(double newX) noexcept { x = newX; }
  void setY(double newY) noexcept { y = newY; }

private:
  double x, y;
};

...

// OK, constexpr 생성자가 컴파일 시점에서 "실행"됨
constexpr Point p1(9.4, 27.7);
constexpr Point p2(28.6, 19.1);

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

// constexpr 함수를 이용해서 constexprt 객체 초기화
const auto mid = midpoint(p1, p2);

C++11 에서는, constexpr 함수는 반드시 리터럴 형식(literal type) 이어야 한다(void 를 제외한 모든 내장 형식). 위의 코드에서는 void 로 선언된 setter 함수 두개를 제외하면 모든 함수가 constexpr 로 설정되어 있어, mid 객체를 읽기 전용 메모리 안에 생성할 수 있다. 또한 mid.xValue() * 10 같은 표현식을 템필릿 인수나 열거자에도 사용할 수 있다!

C++14 에서는 constexpr 함수가 리터럴 형식이 아닌 형식도 돌려줄 수 있고, void 가 리터럴 형식에 포함되었다. 그리고, return 문이 많아야 하나이어야 한다는 제약이 사라져 if-else 문을 사용할 수 있게 되었다! C++14 에서 수정할 수 있는 부분을 조금 살펴보자.

// 기존 setter 함수 수정
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;
}

...

// reflectMid 값은 컴파일 도중에 알려진다
constexpr auto reflectMid = reflection(mid);

마지막으로, constexpr 이 객체나 함수의 인터페이스의 일부라는 뜻은, 한 번 constexpr 로 정한 녀석을 웬만해서는 변경될 여지가 없어야 한다. 가능한 한 constexpr 를 붙이되, 설계에 대해 신중히 고민해 보자!

Comments