[C++] 컴파일 시 사용 여부를 알려주는 constexpr 키워드

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 멤버 함수에 관한 내용은 여기서 볼 수 있습니다.

 

[C++] const 포인터, const 멤버 함수 및 const 클래스

const 포인터변수를 상수로 바꾸기 위해선 두 가지가 필요합니다.첫 번째는 const 키워드를 변수명 앞에 붙입니다.두 번째는 변수를 선언 시 초기화해야 됩니다.const int a; // compile error !! 선언과 동

codingembers.tistory.com

 

다음은 이 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)

 

 

  • 네이버 블로그 공유
  • 네이버 밴드 공유
  • 페이스북 공유
  • 카카오스토리 공유