Effective C++/Effective Modern C++
[Effective Modern C++] 15. constexpr
김디트
2025. 2. 25. 11:23
728x90
항목 15. 가능하면 항상 constexpr을 사용하라
constexpr 개요
- 객체에 적용하면 const의 강화된 버전처럼 작용한다.
- 어떠한 값이 상수인데다 컴파일 시점에 알려지게 한다.
- 허나 함수에 적용하면 상당히 다른 의미로 작용한다.
- 결과값이 반드시 const가 아니다.
- 심지어 반드시 컴파일 시점에 알려진다는 보장도 없다.
- 결함이 아닌 의도된 기능이다.(심지어 위 두 성질은 constexpr의 장점이다.)
constexpr 객체
- constexpr 객체는 실제로 const이며, 그 값은 실제로 컴파일 시점에 알려진다.
- 컴파일 시점에 알려지는 값에는 특별한 권한이 있다.
- 읽기 전용 메모리에 배치될 수 있다.
- C++에서 정수 상수 표현식(integral constant expression)이 요구되는 문맥에서 사용할 수 있다.
- 배열 크기
- 정수 템플릿 인수
- 열거자 값
- allignment 지정자
int sz;
...
constexpr auto arraySize1 = sz; // 에러! sz의 값은 컴파일에 확정적이지 않다.
std::array<int, sz> data1; // 여전히 에러!
constexpr auto arraySize2 = 10; // 성공
std::array<int, arraySize2> data2; // constexpr 객체를 사용하므로 성공
// const를 사용한다면?
const auto arraySize = sz; // 성공
std::array<int, arraySize> data; // 에러! const 객체는 컴파일 시점에 확정되는 값이 아니다.
constexpr 함수
- 컴파일 시점 상수를 매개변수로 호출하면 컴파일 시점 상수를 리턴한다.
- 컴파일 시점 상수를 요구하는 문맥에 constexpr 함수를 사용할 수 있다.
- 다만 매개변수의 값이 컴파일 시점에 확정적이지 않으면 컴파일 에러.
- 런타임 시점의 수를 매개변수로 호출하면 런타임 시점 값을 리턴한다.
- 컴파일 시점에 확정되지 않는 수를 하나라도 사용하면 보통의 함수처럼 동작한다.
- 즉, 컴파일 시점용, 런타임 시점용 함수를 두 가지 나눠서 구현하지 않아도 된다!
constexpr
int pow(int base, int exp) noexcept
{
...
}
constexpr auto numConds = 5;
std::array<int, pow(3, numConds)> results; // 컴파일 시점용으로 사용
// constexpr 객체를 인자로 pow 를 호출하였으므로
// 리턴되는 값은 컴파일 시점에 확정적이다.
auto base = readFromDB("base");
auto exp = readFromDB("exponent");
auto baseToExp = pow(base, exp); // 런타임 시점용으로 사용
- constexpr 함수 구현의 제약
- C++11과 C++14의 제약들이 조금 다르다.
- C++11
- 실행 가능 문장이 하나를 넘으면 안된다.
- 즉, return 문 하나만 사용할 수 있다.
- 조건부 연산자(삼항 연산자)와 재귀를 사용하여 이 제약을 해소할 수 있을 것이다.
-
constexpr int pow(int base, int exp) noexcept { 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; }
- 반드시 리터럴 타입(literal type)들을 받고 돌려줘야 한다.
- 리터럴 타입
- 컴파일 도중에 값을 결정할 수 있는 타입.
- 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;
};
// 생성자가 constexpr이 될 수 있는 이유
// 전달되는 값들이 컴파일 시점에 알 수 있기만 하면
// 멤버 변수들 역시 컴파일 시점에 알 수 있기 때문이다.
constexpr Point p1(9.4, 27.2); // 성공
constexpr Point p2(28.8, 5.3); // 역시 성공
// getter 함수 역시 constexpr이므로
// 이 함수를 호출하는 또 다른 constexpr 함수를 선언하는 것도 가능하다.
constexpr
Point minpoint(const Point& p1, const Point& p2) noexcept
{
return { (p1.xValue() + p2.xValue()) / 2,
(p1.yValue() + p2.yValue()) / 2 };
}
constexpr auto mid = midpoint(p1, p2);
- C++11에서는 두 가지 제약 때문에 Point의 멤버 함수 setX, setY를 constexpr로 선언할 수 없다.
- 멤버 함수들은 작동 대상 객체를 수정하는데, C++11에서는 constexpr 멤버 함수는 암묵적으로 const이다.
- 이 멤버 함수들은 리턴 타입이 void인데, C++11에서는 void가 리터럴 타입이 아니다.
- 하지만 C++14에서는 위 두 제약이 사라졌으므로 setter도 constexpr로 선언할 수 있다.
class Point
{
public:
...
constexpr void setX(double newX) noexcept { x = newX; } // C++14
constexpr void setY(double newY) noexcept { y = newY; } // C++14
...
};
// 그럼 이제 아래와 같은 함수를 작성할 수도 있다.
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); // 성공
728x90