[EMC++] Item 15. 가능하면 항상 constexpr을 사용해라
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};
constexpr
와 const
키워드 모두 한번 정한 값을 변경할 수 없는 상수지만 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
함수가 있지만 두 가지 문제점 때문에 사용할 수 없다.
std::pow
함수는 리턴 타입이int
가 아닌double
이다.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
함수가 두 가지 문제 때문에 불가능했다.
- 해당 함수는 멤버 변수를 수정하는
setter
함수인데 C++11에서는constexpr
객체가 암묵적으로const
로 선언됨 - 해당 함수는 리턴 타입이
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
를 빼버리면 큰 문제가 생길 수 있다.)