Effective Modern C++ - Scott Meyers
Item 15: Use constexpr whenever possible.
item 15. constexpr을 잘 활용하자.
constexpr은 개체나 함수를 const로 만들어 주는 역할을 합니다.
즉, 컴파일 타임에 미리 계산결과를 만들어서 텍스트 영역이라고 알려진 read-only memory에 저장을 합니다.
결과적으로 우리가 얻을 수 있는 장점으로는,
1. 당연히 성능상의 이익을 얻을 수 있습니다.
컴파일 타임에 미리예측이 가능한 값으로만 이루어진 연산이나 함수의 결과를 계산해두어서,
런타임 성능을 향상 시킬 수 있습니다.
2. 코드 상의 편리함도 얻을 수 있습니다.
상수값만을 넣어야 하는 경우 (대표적인 예로 배열선언시 요소들의 개수)
과거에는 const 나 enum 등을 활용했지만, 이제는 함수도 사용이 가능합니다.
(물론 해당 함수의 인자들은 모두 컴파일 타임에 알 수 있는 값이어야 합니다.)
너무 constexpr을 예찬했나요 ?
충분히 예찬받을만한 키워드라고 생각합니다.
이제부터 constexpr에 대해서 좀 더 자세히 살펴보겠습니다.
1. constexpr object vs const object
constexpr 개체는 반드시 컴파일 타임에 계산이 되어야 합니다.
하지만, C++98부터 사용하던 const에는 반드시 그러해야 한다는 규칙은 없습니다.
const는 해당 값을 한번 설정했으면 변경 할 수 없다는 뜻이지,
컴파일 타임에 정해져야 한다는 규칙은 없습니다.
즉, 모든 constexpr 개체는 const지만, 모든 const 개체는 constexpr이 아닙니다.
int I; // non-const
constexpr auto ARRAY_SIZE1 = I; // error : expression must have a constant variable
std::array<int, I> A1; // error : expression must have a constant variable
constexpr auto ARRAY_SIZE2 = 10; // OK
std::array<int, ARRAY_SIZE2> A2; // OK
const auto CONST_SIZE = I; // OK
std::array<int,
CONST_SIZE> A3; // error : expression must have a constant variable
|
위 예제에서는 const 와 constexpr 에 대해서 3가지를 알 수 있습니다.
1. 첫번째 std::array 예제의 경우 constexpr 개체에 컴파일 타임에 정해지지 않은 값 (또는 표현식) 을 대입할 경우 에러가 납니다.
std::array의 개수 인자값에 컴파일타임에 정해지지 않은 값을 넣을시 에러가 발생합니다.
2. 두번째 std::array의 경우 constexpr 개체에 컴파일 타임에 알려진 값을 사용한 경우 정상적으로 사용이 가능합니다.
std::array의 개수 인자로도 사용이 가능합니다.
3. 세번째 std::array의 경우 컴파일 타임에 정해지지 않은 값을 const에 대입해도 오류는 없습니다만,
해당 값을 std::array의 개수 인자로 사용은 불가능 합니다.
2. constexpr function
constexpr 함수는 전달되는 인자(argument)가 모두 constexpr 인 경우 컴파일 타임에 미리 계산되어집니다.
인자 중 하나라도 컴파일 타임에 알 수 없는 값일 경우는 그냥 런타임에 실행되는 일반 함수랑 똑같이 동작합니다.
(그렇다고 컴파일시 오류가 나지는 않습니다.)
단, 해당 결과값을 std::array의 인자로 사용한다던지 반드시 상수값을 넣어야 하는 곳에 적용하면 오류가 발생합니다.
한마디로 같은 동작을 하는 함수를 constexpr 과 일반 런타임용을 따로 작성할 필요가 없습니다.
constexpr 함수 하나만 있으면 둘 다 사용이 가능합니다.
std::pow(a, b)를 실행하면 a의 b 승한 결과를 얻을 수 있습니다.
해당 함수는 double 값을 반환하기 때문에,
std::array인자로 넣을 수 있는 int버전의 pow(a, b)를 만들어 보겠습니다.
constexpr int pow(int BASE, int EXP) noexcept
{
return (EXP == 0 ? 1 : BASE * pow(BASE, EXP - 1));
}
|
어라... 재귀 함수 (recursive function)로 만들어 졌습니다.
C++11에서는 constexpr함수는 단 하나의 구문(statement)로 이루어져야 합니다.
그래서 if-else 대신 3항연산 ( ? : )를 사용하고,
for-loop 대신에 재귀 함수를 활용하는 식으로 생성을 합니다.
C++14에서는 일반 함수와 같이 생성이 가능합니다.
(하지만, Visual Studio 2015 CTP에서는 안됩니다.)
constexpr int pow(int BASE, int EXP) noexcept
{
int RESULT = 1;
for (int i = 0; i < EXP; i++)
RESULT *= BASE;
return RESULT;
}
|
위 함수의 인자에 컴파일 타임에 알 수 있는 값만을 넣을 경우는 constexpr로 값을 반환하고,
런타임에 알 수 있는 값을 하나라도 넣을 경우는 일반 함수로 동작합니다.
constexpr auto EXP = 5;
std::array<int, pow(2, EXP)> ARRAY; // pow () in constexpr
auto ER = FuncInRuntime();
auto PR =
pow(3, ER); //
pow() in run-time |
constexpr 함수의 리턴값은 반드시 리터럴 타입(literal type)이어야 합니다.
(컴파일 타임에 결정되어 있어야 한다는 의미입니다.)
그런데 built-in 타입중 void 빼고는 모두 리터럴 타입이며, 사용자가 만든 class도 대부분 리터럴 타입으로 만들수 있습니다.
생성자와 멤버변수 모두 constexpr속성을 가질 수 있습니다.
class Point
{
private:
double X, Y;
public:
constexpr Point(double x = 0, double y = 0) noexcept : X(x), Y(y) {}
constexpr double GetX() const noexcept { return X; }
constexpr double GetY() const noexcept { return Y; }
void SetX(double x) noexcept { X = x; }
void SetY(double y) noexcept { Y = y; }
};
|
Point의 생성자가 constexpr로 선언되어 있기 때문에 생성자의 인자를 컴파일 타임에 알 수 있는 값으로 전달 할 경우 를 constexpr로 생성할 수 있습니다.
constexpr Point P1(9.4, 27.7); constexpr Point P2(28.8, 5.3); |
Point의 Getter 또한 constexpr로 선언되어 있기 때문에 constexpr로 생성된 Point로 부터 다른 constexpr Point생성이 가능합니다.
constexpr Point Mid(const Point& P1, const Point& P2) noexcept
{
return{ (P1.GetX() + P2.GetX()) / 2, (P1.GetY() + P2.GetY()) / 2 };
}
|
MID 개체 및 MID.GetX() * 10 같은 값은 컴파일 타임에 확정된 값입니다.
그러므로, template의 인자나 enum의 속성 등으로 사용이 가능합니다.
위 Point class를 보면 Setter들은 constexpr로 선언하지 않았습니다.
왜 그랬을까요 ?
이미 그 이유는 위에 다 나와 있습니다.
첫째, 모든 constexpr은 const입니다. const Point의 값을 바꾸는 것은 말이 안되죠 ?
둘째, constexpr 함수의 리턴값은 반드시 리터럴 타입(literal type)이어야 합니다.
그런데 built-in 타입중 void 빼고는 모두 리터럴 타입입니다.
Setter 함수들의 리턴타입은 void 이므로 리터럴 타입이 아닙니다.
C++11에서는 이러한 제약때문이 있었지만, C++14에서는 이런게 모두 사라졌습니다.
그러므로 C++14에서는 Setter 도 constexpr로 선언이 가능해 졌습니다.
(하지만 아직 Visual Studio 2015 CTP에서는 안됩니다. ㅠㅠ)
class Point
{
...
constexpr void SetX(double x) noexcept { X = x; }
constexpr void SetY(double y) noexcept { Y = y; }
};
|
Setter의 constexpr이 가능하게되면 다음과 같이 사용 할 수도 있습니다.
constexpr Point Reflect(const Point& P) noexcept
{
Point RET;
RET.SetX(-P.GetX());
RET.SetY(-P.GetY());
return RET;
}
constexpr Point P1(9.4, 27.7);
constexpr Point P2(28.8, 5.3);
constexpr auto MID = Mid(P1, P2);
constexpr auto RefMID = Reflect(MID);
|
RefMID 개체를 컴파일타임에 생성이 가능하게 됩니다.
(물론 아직 Visual Studio 2015 CTP에서는 여러개의 statement를 가진 constexpr함수도,
constexpr Setter도 지원해주지 않아서, 위와 같은 Code가 안됩니다.)
Things to Remember * constexpr object는 컴파일-타임에 알려진 값으로 초기화된 const임을 뜻합니다. * constexpr function은 컴파일-타임에 알려진 값이 인자로 온 경우 그 결과를 컴파일-타임에 미리 만들어 놓는 함수를 뜻합니다. * constexpr object, function은 일반 개체, 함수로도 사용이 가능합니다. (다양한 용도로 활용이 가능합니다.) * constexpr 은 개체와 함수 interface의 일부분입니다. |
댓글 없음:
댓글 쓰기