본문 바로가기
C++/Effective Modern C++

가능하면 항상 constexpr을 사용하라

by COCO1337 2020. 7. 8.

constexpr을 객체에 적용했을 때에는 const의 강화 버전처럼 작용하지만 함수에 적용했을때에는 다른 의미로 작용한다. constexpr은 단지 상수일 뿐만 아니라 컴파일 시점에서 알려진다는 점을 나타낸다.

컴파일 시점에서 알려지는 값들은 읽기 전용 메모리에 배치될 수 있다. 즉 상수이자 컴파일 시점에서 알려진 정수 값을 C++에서 정수 상수 표현식(const expression)이 요구되는 문맥에서 사용할 수 있다는 것이다. 배열 크기나 정수 템플릿 인수, 열거자 값 등이 그런 문맥에 해당한다. 그런 변수를 constexpr로 선언하면 컴파일러는 그것이 컴파일 시점 상수임을 보장해 주게 된다.

int sz;
constexpr auto arrSize1 = sz;		// 오류! sz의 값을 컴파일 시점에서 알 수 없음
std::array<int, arrSize1> data1;	// 오류! 마찬가지로 arrSize1은 컴파일 시점에서 알 수 없음
constexpr auto arrSize2 = 10;		// 컴파일 시점에서 상수
std::array<int, arrSize2> data2;	// arrSize2는 constexpr 객체이므로 사용 가능

constexpr 함수의 경우 컴파일 시점 상수를 인수로 해서 호출된 경우에는 컴파일 시점 상수를 산출한다. 실행시점이 되서야 알려지는 값으로 호출하면 실행시점 값을 산출한다.

즉, constexpr를 사용하면 컴파일 시점상수를 위한 버전과 다른 모든 값을 위한 버전의 함수를 나누어 구현할 필요가 없게 된다.

또한 컴파일 시점 상수를 요구하는 문맥에 constexpr 함수를 사용할 수 있다. 그런 경우에 constexpr 함수에 넘겨주는 인수의 값이 컴파일 시점에서 알려지면 함수의 결과는 컴파일 도중에 계산되며, 인수의 값이 컴파일 시점에서 알려지지 않는다면, 코드의 컴파일이 거부된다.


constexpr 함수는 컴파일 시점 값들로 호출했을 때 반드시 컴파일 시점 결과를 산출할 수 있어야 하므로, 구현에 제약이 따른다. 하지만 C++11과 C++14의 제약들이 조금 다르다

C++11의 constexpr 함수는 실행 가능 문장이 많아야 하나이어야 하고, 보통은 return문이다. 이 제약을 만족하기 위해 if-else문 대신 삼항 연산자를 사용하고 루프 대신 재귀를 사용하기도 한다. 아래는 pow를 구현한 예이다.

constexpr int pow(int base, int exp) noexpt
{
    return (exp == 0 ? 1 : base * pow(base, exp - 1));
}

C++14에서는 제약이 조금 느슨해져 다음과 같은 구문이 허용된다.

constexpr int pow(int base, int exp) noexcept
{
    auto result = 1;
    for (int i = 0; i < exp; ++i) result *= base;
    return result;
}

constexpr 함수는 반드시 리터럴 형식들을 받고 돌려주어야 한다. 리터럴 형식은 컴파일 시점에서 값을 결정할 수 있는 형식이며, C++11에서 void를 제외한 모든 내장 형식이 리터럴 형식에 해당된다. 또한 생성자와 적절한 멤버 함수들이 constexpr인 사용자 형식도 리터럴 형식이 될 수 있다.

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;
};

const Point p1(12.3, 45.6);	// constexpr 생성자가 컴파일 시점에서 실행되기 때문에 가능

// constexpr 객체를 초기화 하는 constexpr 함수
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에서는 constexpr 멤버 함수는 암묵적으로 const로 선언되며, 멤버 함수들의 반환 형식이 void 인데 C++11에서 void는 리터럴 형식이 아니다. C++14에서는 제약이 모두 사라져 사용 가능하다.

constexpr은 객체나 함수의 인터페이스의 일부라는 점을 알아야 한다. constexpr을 지정한다는 것은 함수 또는 객체를 C++이 상수 표현식을 요구하는 문맥에서 사용할 수 있다는 것이다.


Conclusion

- constexpr 객체는 const이며, 컴파일 도중 알려지는 값들로 초기화 된다.

- constexpr 함수는 그 값이 컴파일 도중에 알려지는 인수들로 호출하는 경우에는 컴파일 시점 결과를 산출한다.

- constexpr 객체나 함수는 비 constexpr 객체나 함수보다 광범위한 문맥에서 사용할 수 있다.

- constexpr은 객체 나 함수의 인터페이스의 일부이다.


Reference

Effective Modern C++ 항목 15: 가능하면 항상 constexpr을 선호하라

반응형

댓글