Post List

2015년 4월 3일 금요일

item 15: constexpr을 잘 활용하자.

작가
Meyers, Scott
출판
O'ReillyMedia
발매
2014.12.12
평점








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

constexpr auto MID = Mid(P1, P2);

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의 일부분입니다.