constexpr 변수
constexpr 변수는 컴파일 시에 알 수 있는 값을 가진 상수임을 선언한 변수입니다.
이 변수는 const 속성이 있으므로, 선언 시 반드시 초기화를 해야 합니다.
constexpr int count = 0;
이러한 변수들은 컴파일 시에 값을 알아야 되는 구문에 사용할 수 있습니다.
예를 들어, std::array는 선언 시, 원소의 타입과 원소의 개수를 템플릿 매개변수로 입력받습니다.
( 그리고, 이러한 템플릿 객체는 컴파일 시 구체화되기 때문에, 컴파일 시 원소의 타입과 개수를 알아야 합니다. )
따라서, 다음과 같은 선언은 오류입니다.
int sz = 100;
array<int, sz> intArr; // error !
변수 sz의 값은 위의 코드가 실행될 때가 돼서야 값을 알게 되기 때문입니다.
하지만, 만약 sz의 값을 컴파일 시에 값을 알 수 있다면, 다음과 같은 초기화가 가능하게 됩니다.
const int sz = 100; // 컴파일 시 값을 알 수 있음
constexpr int sz_expr = sz;
std::array<int, sz_expr> intArr; // ok
const int로 선언한 sz의 값은 컴파일 시에 알 수 있습니다.
그렇기 때문에, constexpr 속성을 선언한 sz_expr 변수에 sz의 값을 대입하는 것도 합법입니다.
그리고, 이 constexpr 변수는 intArr을 인스턴스화하는 데 사용할 수 있습니다.
위의 sz와 sz_expr 변수와 같이 컴파일 시 값을 알 수 있는 상수 객체와, 1, 1.31415, 'c' 같은 값( literal )을 합하여 컴파일 시 상수( compile-time constant )라고 합니다.
그리고, 위의 예문과 같이 constexpr 변수는 컴파일 시 값을 평가할 수 있는 표현식( 여기서는 sz )으로 초기화해야 합니다.
이러한 표현식을 상수 표현식( constant expression )이라고 부르며, 이 표현식은 컴파일 시 상수( compile-time constant )와 컴파일 시 값을 알 수 있는 함수( constexpr 함수, 다음 항목에서 설명 )들로 구성됩니다.
그런데, const 속성과 constexpr의 차이점은 무엇일까요?
sz_expr 대신 sz으로 intArr을 생성하더라도 아무런 차이가 없는데 말이죠.
그것은, const 변수의 값은 항상 컴파일 시 알 수 있는 것이 아니라는 것입니다.
다음의 예문은 실행 시간이 되어야 값을 알 수 있는 const 변수를 보여줍니다.
int main(){
int val;
std::cin >> val;
const int cVal = val; // 실행 시간에 값을 알 수 있음
constexpr int int_expr = cVal; // 상수 표현식이 아님. error !
}
▼출력
error: the value of 'cVal' is not usable in a constant expression
위의 cVal의 값은 실행 시간이 되어서야 초기화됩니다.
이 경우 cVal은 컴파일 시 상수가 아닙니다.
따라서, cVal은 상수 표현식이 아니고, int_expr은 상수 표현식으로만 초기화되어야 하므로, 마지막 문장은 오류입니다.
constexpr 함수
constexpr 함수는 컴파일 시 값을 평가할 수 있는 함수라고 선언한 함수입니다.
약간 혼동이 올 수 있는데, 이렇게 선언한 함수는 컴파일 시 알 수 있는 값을 인자로 보내면, 그에 합당한 값을 컴파일 시에 알려주겠다는 약속을 하는 함수입니다.
그렇다는 얘기는, 이 함수는 주어지는 인자에 따라, 컴파일 시에 반환값을 알 수도 그렇지 않을 수도 있다는 말이 됩니다.
아래의 예문은 constexpr 함수의 예를 보여줍니다.
// constexpr 함수
constexpr int add( int a, int b){
return a + b;
}
int main(){
constexpr int sum1 = add( 10, 15); // 컴파일 시 값을 알 수 있음. ok
int a = 10;
int b = 15;
int sum2 = add( a, b); // 실행 시간에 값을 알 수 있음. ok
constexpr int sum3 = add( a, b); // error !
}
첫 번째 줄의 add( 10, 15 )의 값은 10과 15의 값을 컴파일 시에 당연히 알고 있으므로, add의 결과도 컴파일 시 알 수 있습니다.
이전 항목에서 상수 표현식( constant expression )의 정의를 설명할 때, 컴파일 시 값을 알 수 있는 함수도 상수 표현식이 될 수 있다는 것을 얘기했습니다.
add( 10, 15 )는 상수 표현식이 되고, 이 상수 표현식으로 constexpr 변수인 sum1을 초기화할 수 있습니다.
그런데, 두 번째 add( a, b ) 함수의 값은 컴파일 시 알 수 없습니다.
변수 a와 b의 값을 컴파일 시 알 수 없기 때문입니다.
그럼에도 불구하고, 이 문장은 아무런 문제가 없습니다.
그것은 constexpr 함수는 상수 표현식이 안되면 ( 다른 말로, 컴파일 시 값을 평가할 수 없다면 ) 일반 함수로서 기능을 수행하도록 설계되었기 때문입니다.
그래서, 프로그래머는 아래와 같이 const 속성의 멤버 함수와 non-const 속성의 멤버 함수를 따로 작성하는 것과 같은 일을 겪을 필요가 없습니다.
class A{
public:
int add( int a, int b );
int add( int a, int b ) const; // const 멤버 함수
};
참고로, 두 번째 const add 함수는 클래스 A의 객체가 const 객체일 때 호출되는 멤버 함수입니다.
그렇지만, 예문의 마지막 문장은 오류입니다.
constexpr int sum3 = add( a, b); // error !
이것은 add( a, b )가 상수 표현식이 아닌데도, constexpr 변수인 sum3를 초기화하려고 했기 때문입니다.
그리고, 이러한 constexpr 함수는 리터럴 타입( literal type )의 값을 반환해야 합니다.
여기서, 리터럴 타입은 컴파일 시 값을 결정할 수 있는 타입을 말합니다.
int, char, double 같은 원시 타입이 이러한 타입이고, 적절한 생성자와 멤버 함수를 가진 사용자 클래스도 리터럴 타입이 될 수 있습니다.
constexpr 사용자 객체
클래스의 객체는 생성자를 통해서 초기화되고, 이 생성자는 함수이므로, 생성자가 constexpr 속성을 갖고 있다면, 이 객체의 값을 컴파일 시 평가할 수 있을 것입니다.
따라서, 이러한 클래스의 객체는 constexpr 객체로 선언할 수 있습니다.
다음의 예문에서는 생성자( constructor )와 멤버 함수들을 constexpr로 선언한 클래스와 이 클래스를 매개변수로 하는 함수를 보여줍니다.
class MyPoint{
int m_x, m_y;
public:
constexpr MyPoint( int x, int y) : m_x(x), m_y(y){}
constexpr int getX() const { return m_x; }
constexpr int getY() const { return m_y; }
constexpr void setX(int x){ m_x = x; }
constexpr void setY(int y){ m_x = y; }
};
// 위의 클래스를 매개변수로 받는 함수
constexpr MyPoint add( const MyPoint& pt1, const MyPoint& pt2){
return MyPoint( pt1.getX() + pt2.getX(), pt1.getY() + pt2.getX());
}
이 클래스를 다음과 같이 선언하고 사용할 수 있습니다.
int main(){
constexpr MyPoint pt1(10, 15);
constexpr MyPoint pt2(11, 16);
constexpr MyPoint addPt1 = add( pt1, pt2);
int x = 29;
int y = 35;
MyPoint pt3(x, y); // ok
MyPoint addPt2 = add( pt1, pt3);
constexpr MyPoint addPt3 = add( pt1, pt3); // error !
pt1.setX(20); // error !
}
먼저, pt1과 pt2 객체는 컴파일 시 값을 알 수 있습니다. ( MyPoint의 모든 멤버들의 값을 알 수 있습니다. )
따라서, 이들을 constexpr로 선언하는 것이 가능합니다.
그리고, pt1과 pt2가 상수 표현식이므로, add( pt1, pt3 )도 상수 표현식입니다.
그러므로, addPt1을 constexpr로 선언할 수 있습니다.
그렇지만, MyPoint 객체는 일반 사용자 객체로도 사용될 수 있습니다.
pt3은 변수 x, y 컴파일 시 값을 알 수 없으므로, constexpr 객체로 선언할 수는 없지만 일반 객체로 생성할 수 있습니다.
이것은 생성자가 constexpr 함수이며, 이러한 함수는 보통 함수 역할도 수행하기 때문입니다.
이는 addPt2 객체가 일반 객체로 초기화가 가능한 이유이기도 합니다.
그러나, constexpr로 선언한 addPt3의 초기화 문장은 틀렸습니다.
constexpr MyPoint addPt3 = add( pt1, pt3); // error !
이것은, pt3이 constexpr 객체가 아니고, 따라서 add( pt1, pt3 )은 상수 표현식이 아니기 때문입니다.
그리고, 마지막 setX 함수를 호출한 문장 역시 오류입니다.
pt1.setX(20); // error !
pt1객체는 constexpr 객체이고, 이러한 객체는 const 속성입니다.
따라서, 이 객체의 멤버 변수의 값은 변경될 수 없습니다.
그럼, 클래스 멤버 변수의 값을 변경하는 constexpr 멤버 함수는 어떻게 사용해야 하는 걸까요?
이러한 setX 함수를 사용하는 방법을 얘기하기 전에, 이 함수를 constexpr로 선언할 수 있도록 C++ 14 이후에 변경된 사항을 먼저 얘기하겠습니다.
먼저, 위에서 constexpr 함수는 리터럴 타입을 반환해야 한다고 말했습니다.
그리고, C++ 14 이후부터 void 타입도 리터럴 타입( literal type )으로 변경되었습니다.
또, C++ 11에서는 constexpr로 선언된 멤버 함수는 암시적으로 const 속성을 가졌었습니다.
따라서, setX 함수처럼 멤버 변수의 값을 변경하는 함수는 constexpr로 선언할 수 없었습니다.
그렇지만, 이러한 제한도 C++ 14 이후에 없어졌습니다.
그래서, 다음과 같은 문장이 가능한 것입니다.
constexpr void setX( int x ){ m_x = x; }
참고로, const 멤버 함수에 관한 내용은 여기서 볼 수 있습니다.
다음은 이 setX 함수를 사용하는 예문입니다.
constexpr MyPoint create_from_point( const MyPoint& pt, int x, int y){
MyPoint newPt(0,0); // 비 상수 객체
newPt.setX( pt.getX() + x); // set 함수
newPt.setY( pt.getY() + y);
return newPt;
}
int main(){
// ... 위와 같음
constexpr MyPoint addPt = add( pt1, pt2);
constexpr MyPoint createdPt = create_from_point( addPt, -3, 10); // ok
}
create_from_point 함수 호출 시, 모든 매개변수( pt, x, y )의 값을 컴파일 시 알 수 있다면, newPt는 상수 표현식 ( 컴파일 시 모든 값을 알 수 있는 )이 됩니다.
그러므로, 이 함수는 상수 표현식이 되고, constexpr 객체를 초기화할 수 있습니다.
정리
- 함수와 객체의 값이 컴파일 시 평가될 수 있다면, constexpr 속성을 선언할 수 있습니다.
- constexpr 객체는 상수 표현식으로 초기화되어야 합니다.
- constexpr 속성을 가진 함수와, constexpr 생성자와 멤버 함수를 가진 사용자 객체는, 일반함수와 일반 사용자 객체로서도 사용이 가능합니다.
이 글과 관련있는 글들
비-형식 템플릿 매개변수( non-type template parameter)
'C, C++ > C++ 언어' 카테고리의 다른 글
[C++] 템플릿의 형식 추론( template type deduction ) 규칙 (0) | 2024.09.07 |
---|---|
[C++] 범위 있는( scoped ) enum에 관하여 (0) | 2024.09.05 |
[C++] 재정의( overriding )와 override 키워드 (0) | 2024.09.01 |
[C++] 초기화( initialization )의 종류 정리 (0) | 2024.08.31 |
[C++] 예외( exception ) 클래스 (0) | 2024.08.24 |