- constexpr 객체는 const이며, 컴파일 도중에 알려지는 값들로 초기화된다.
컴파일 시점에 알려지는 값들에는 특별한 권한이 있다. 예를 들어 그런 값들을 읽기 전용 메모리에 배치될 수 있다.
좀 더 광범위한 프로그래머들에게 중요한 점은, 상수이자 컴파일 시점에 알려진 정수 값을 C++에서 정수 상수 표현식이 요구되는 문맥에서 사용할 수 있다는 것이다.
배열 크기나 정수 템플릿 인수, 열거자 값, 정합 지정자를 지정하는 등의 여러 문맥이 그런 문맥에 해당한다.
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 객체
const가 constexpr에서와 동일한 보장을 제공하지는 않음을 주목하기 바란다.
const 객체가 반드시 컴파일 시점에서 알려지는 값으로 초기화되지는 않기 때문이다.
- constexpr 함수는 그 값이 컴파일 도중에 알려지는 인수들로 호출하는 경우에는 컴파일 시점 결과를 산출한다.
constexpr 함수는 컴파일 시점 상수를 인수로 해서 호출된 경우에는 컴파일 시점 상수를 산출한다.
실행시점이 되어서야 알려지는 값으로 호출하면 실행시점 값을 산출한다.
1. 컴파일 시점 상수를 요구하는 문맥에 constexpr 함수를 사용할 수 있다. 그런 문맥에서, 만일 constexpr 함수에 넘겨주는 인수의 값이 컴파일 시점에서 알려진다면, 함수의 결과는 컴파일 도중에 계산된다. 인수의 값이 컴파일 시점에서 알려지지 않는다면, 코드의 컴파일이 거부된다.
2. 컴파일 시점에서 알려지지 않는 하나 이상의 값들로 constexpr 함수를 호출하면 함수는 보통의 함수처럼 작동한다. 즉, 그 결과는 실행시점에서 계산된다. 이는 같은 연산을 수행하는 함수를 두 버전, 즉 컴파일 시점 상수를 위한 버전과 다른 모든 값을 위한 버전으로 나누어 구현할 필요가 없음을 뜻한다. 그냥 하나의 constexpr 함수를 두 가지 용도로 사용하면 된다.
constexpr 함수를 작성하는데 제약이 있는데, C++11과 C++14의 제약들이 조금 다르다.
C++11의 경우 실행 가능한 문장이 많아야 하나(1개 혹은 0개)이어야 하고 보통의 경우 그 문장은 return문일 수밖에 없다. 그리고, constexpr 멤버 함수는 암묵적으로 const로 선언되며, C++11에서 void는 리터럴 타입이 아니라서 반환 타입이 void가 될 수 없다.
constexpr 함수는 반드시 리터럴 타입(literal type)들을 받고 돌려주어야 한다. C++11에서 void를 제외한 모든 내장 타입이 리터럴 타입에 해당한다. 그리고 생성자와 적절한 멤버 함수들이 constexpr인 사용자 정의 타입도 리터럴 타입이 될 수 있다.
C++14에서는 constexpr 함수가 리터럴 타입이 아닌 타입도 돌려줄 수 있게 되었고, constexpr 함수의 return 문이 많아야 하나이어야 한다는 제약도 사라졌다.
컴파일 도중에 3n의 크기를 갖는 array를 만드려면 어떻게 해야 할까?
수학적 기능성을 제공하는 C++ 표준 라이브러리 함수로는 std::pow가 있지만, 두 가지 문제점 때문에 이것을 그대로 사용할 수 없다.
첫째로, std::pow는 부동소수점 타입에 대해 작동하지만 지금 필요한 것은 정수 결과이다.
둘째로, std::pow는 constexpr이 아니다. 이 두 가지 문제점은 지금 요구에 맞는 pow함수를 직접 작성하면 해결된다.
constexpr // pow는 결코 예외를 던지지 않는 constexpr 함수
int pow(int base, int exp) noexcept
{
// 구현은 나중에
}
constexpr auto numConds = 5; // 조건들의 개수
std::array<int, pow(3, numConds)> results; // results 개의 3^numConds 개의 요소들을 담는다.
pow 앞에 constexpr이 있다고 해서 pow가 반드시 const 값을 돌려주는 것은 아님을 기억하기 바란다.
만일 base나 exp 중 하나라도 컴파일 시점 상수가 아니면 pow의 결과가 실행시점에서 계산될 수 있다.
즉, 다음과 같이 pow를 사용할 수 있다.
auto base = readFromDB("base"); // 실행시점에서 값들을
auto exp = readFromDB("exponent"); // 구한다.
auto baseToExp = pow(base, exp); // 실행시점에서 pow 함수를 호출한다.
constexpr 함수는 컴파일 시점 값들로 호출했을 때 반드시 컴파일 시점 결과를 산출할 수 있어야 하므로, 구현에 일정한 제약들이 따른다.
C++11에서 constexpr 함수는 실행 가능 문장이 많아야 하나(1개 혹은 0개)이어야 한다.
이것이 큰 제약이긴 하지만, 두 가지 요령을 이용하면 constexpr 함수의 표현력을 생각 이상으로 확장할 수 있다.
하나는 조건부 연산자(삼항 연산자) "?:"을 if-else 문 대신 사용하는 것이고, 또 하나는 루프 대신 재귀를 사용하는 것이다.
다음은 이 요령들을 이용해서 pow를 구현한 예이다.
constexpr int pow(int base, int exp) noexcept
{
return (exp == 0 ? 1 : base * pow(base, exp - 1));
}
C++14에서는 constexpr 함수에 대한 제약이 상당히 느슨해져서, 다음과 같은 구현이 허용된다.
constexpr int pow(int base, int exp) noexcept // C++14
{
auto result = 1;
for (int i = 0; i < exp; ++i)
result *= base;
return result;
}
constexpr 함수는 반드시 리터럴 타입 (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(void) const noexcept { return x; }
constexpr double yValue(void) 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); // OK, constexpr 생성자가 컴파일 시점에서 "실행됨"
constexpr Point p2(28.8, 5.3); // 역시 OK
마찬가지로 컴파일 도중에 알려진 값으로 초기화된 Point 객체에 대해 호출된다면 조회용 멤버 함수(getter; 줄여서 조회 함수) 역시 constexpr이 될 수 있다.
따라서 다음과 같은 함수를 작성하는 것이 가능하다.
constexpr
Point midpoint(const Point& p1, const Point& p2) noexcept
{
return { (p1.xValue() + p2.xValue()) / 2, (p1.yValue() + p2.yValue()) / 2 };
// constexpr 멤버 함수들을 호출
}
constexpr auto mid = midpoint(p1, p2); // constexpr 함수의 결과를 이용해서 constexpr 객체를 초기화한다.
C++11에서는 두 가지 제약 때문에 Point의 멤버 함수 setX와 setY를 constexpr로 선언할 수 없다.
첫째로, 이 멤버 함수들은 대상 객체를 수정하는데, C++11에서 constexpr 멤버 함수는 암묵적으로 const로 선언된다.
둘째로, 이 멤버 함수들은 반환 타입이 void인데, C++11에서 void는 리터럴 타입이 아니다. C++14에서는 두 제약 모두 사라졌다. 그래서 C++14에서는 Point의 설정용 멤버 함수 (setter; 줄여서 설정 함수)들도 constexpr이 될 수 있다.
class Point {
public:
constexpr void setX(double newX) noexcept
{
x = newX;
}
constexpr void setY(double newY) noexcept
{
y = newY;
}
};
이를 이용해서 이제 이런 함수를 작성할 수 있다. 원점을 기준으로 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)이다;
'PROGRAMMING > Effective Modern C++' 카테고리의 다른 글
17. 특수 멤버 함수들의 자동 작성 조건을 숙지하라. (0) | 2021.03.09 |
---|---|
16. const 멤버 함수를 스레드에 안전하게 작성하라. (0) | 2021.03.09 |
14. 예외를 방출하지 않을 함수는 noexcept로 선언하라. (0) | 2020.12.27 |
13. iterator보다 const_iterator를 선호하라. (0) | 2020.12.26 |
12. 재정의 함수들을 override로 선언하라. (0) | 2020.12.26 |
댓글