스토리텔링 개발자

[Effective Modern C++] 15. constexpr 본문

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
Comments