[Modern C++] 항목 15: 가능하면 항상 constexpr을 사용하라

게시:     수정

카테고리:

태그: ,

이 글은 아래의 책을 정리하였습니다. 이펙티브 모던 C++, 스콧 마이어스 저자, 류광 번역

가독성이 떨어지는 직역들을 수정하며 정리하였습니다. e.g. 연역 → 추론, 중복적재 → 오버로딩

📦 3. 현대적 C++에 적응하기

👉🏻 항목 15: 가능하면 항상 constexpr을 사용하라

🔍 constexpr이란?

  • constexpr을 객체에 적용했을 때는 본질적으로 const의 강화된 버전처럼 작용하지만, 함수에 적용했을 때는 상당히 다른 의미로 작용한다.
  • constexpr 객체: 실제로 const이며, 그 값은 실제로 컴파일 시점에서 알려진다.
    • 컴파일 시점에서 알려지는 값들은 읽기 전용 메모리에 배치될 수 있다.
  • constexpr 함수의 결과가 반드시 const인 것은 아니며, 그 값이 반드시 컴파일 시점에서 알려진다는 보장도 없다. 이는 표준의 결함이 아니라 의도된 기능이다.

📦 constexpr 객체

int sz;                              // 비constexpr 변수

constexpr auto arraySize1 = sz;      // ❌ 오류! sz의 값이 컴파일 도중에 알려지지 않음
std::array<int, sz> data1;           // ❌ 오류! 같은 문제

constexpr auto arraySize2 = 10;      // ✅ OK, 10은 확실히 컴파일 시점 상수
std::array<int, arraySize2> data2;   // ✅ OK, arraySize2는 constexpr 객체
  • constconstexpr과 동일한 보장을 제공하지는 않는다. const 객체가 반드시 컴파일 시점에서 알려지는 값으로 초기화되지는 않기 때문이다.
const auto arraySize = sz;           // OK, arraySize는 sz의 const 복사본
std::array<int, arraySize> data;     // ❌ 오류! arraySize의 값은 컴파일 시점에서 알려지지 않음

모든 constexpr 객체는 const이지만, 모든 const 객체가 constexpr인 것은 아니다. 어떤 변수의 값을 반드시 컴파일 시점 상수를 요구하는 문맥에서 사용할 수 있어야 한다면, 선택할 도구는 const가 아니라 constexpr이다.


⚙️ constexpr 함수

constexpr 함수는 컴파일 시점 상수를 인수로 호출하는 경우에는 컴파일 시점 결과를 산출하고, 실행시점 값으로 호출하면 일반 함수처럼 작동한다.

constexpr                              // pow는 결코 예외를 던지지
int pow(int base, int exp) noexcept    // 않는 constexpr 함수
{
                                      // 구현은 나중에
}

constexpr auto numConds = 5;                        // 조건들의 개수
std::array<int, pow(3, numConds)> results;          // results는 3^numConds 개의 요소들을 담는다
  • pow 앞에 constexpr이 있다고 해서 pow가 반드시 const 값을 돌려주는 것은 아니다. baseexp 중 하나라도 컴파일 시점 상수가 아니면 pow의 결과가 실행시점에서 계산될 수 있다.
auto base = readFromDB("base");        // 실행시점에서 값들을 구한다
auto exp  = readFromDB("exponent");

auto baseToExp = pow(base, exp);       // 실행시점에서 pow 함수를 호출한다

C++11 vs C++14 제약:

  • C++11: constexpr 함수는 실행 가능 문장이 많아야 하나(return 문)이어야 한다. 조건부 연산자(?:)나 재귀를 활용해 표현력을 확장할 수 있다.
// C++11
constexpr int pow(int base, int exp) noexcept
{
    return (exp == 0 ? 1 : base * pow(base, exp - 1));
}
  • C++14: constexpr 함수에 대한 제약이 상당히 느슨해져서 일반적인 제어 흐름도 허용된다.
// 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 함수는 반드시 리터럴 타입(literal type)들을 받고 돌려주어야 한다.
    • 리터럴 타입: 컴파일 도중에 값을 결정할 수 있는 타입. C++11에서 void를 제외한 모든 내장 타입이 해당된다.
    • 생성자와 적절한 멤버 함수들이 constexpr인 사용자 타입도 리터럴 타입이 될 수 있다.

🏗️ 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;
};
  • Point의 생성자를 constexpr로 선언할 수 있는 이유는, 주어진 인수들이 컴파일 시점에서 알려진다면 생성된 Point 객체의 멤버 값들도 컴파일 시점에서 알려질 수 있기 때문이다.
constexpr Point p1(9.4, 27.7);  // ✅ constexpr 생성자가 컴파일 시점에서 "실행됨"
constexpr Point p2(28.8, 5.3);  // ✅ 역시 OK
  • constexpr Point 객체에 대해 xValue()yValue() 조회 함수를 호출하면, 그 결과를 이용해서 또 다른 constexpr 객체를 초기화하는 constexpr 함수를 작성하는 것이 가능하다.
constexpr
Point midpoint(const Point& p1, const Point& p2) noexcept
{
    return { (p1.xValue() + p2.xValue()) / 2,   // constexpr 멤버
             (p1.yValue() + p2.yValue()) / 2 };  // 함수들을 호출
}

constexpr auto mid = midpoint(p1, p2);  // constexpr 함수의 결과를 이용해서
                                        // constexpr 객체를 초기화한다

C++14에서의 setter:

constexpr void setX(double newX) noexcept  // C++14
{ x = newX; }

constexpr void setY(double newY) noexcept  // C++14
{ y = newY; }
  • C++11에서는 두 가지 제약 때문에 setXsetYconstexpr로 선언할 수 없었다.
    • 첫째, constexpr 멤버 함수는 암묵적으로 const로 선언된다.
    • 둘째, 반환 타입이 void인데, C++11에서 void는 리터럴 타입이 아니다.
    • C++14에서는 두 제약 모두 사라졌다.
// 원점을 기준으로 p와 대칭인 Point 객체를 돌려준다(C++14)
constexpr Point reflection(const Point& p) noexcept
{
    Point result;                      // 비const Point를 생성
    result.setX(-p.xValue());          // 그 Point의 x와 y를 설정
    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);                   // reflectedMid의 값은 (-19.1, -16.5)이다;
                                       // 이는 컴파일 도중에 알려진다

🧐 정리

  • constexpr 객체const이며, 컴파일 도중에 알려지는 값들로 초기화된다.
  • constexpr 함수는 그 값이 컴파일 도중에 알려지는 인수들로 호출하는 경우에는 컴파일 시점 결과를 산출한다.
  • constexpr 객체나 함수는 constexpr 객체나 함수보다 광범위한 문맥에서 사용할 수 있다.
  • constexpr은 객체나 함수의 인터페이스의 일부다. constexpr을 나중에 제거하면 클라이언트 코드가 깨질 수 있다.

Cpp 카테고리 내 다른 글 보러가기

댓글남기기