본문 바로가기

진리는어디에/C++

[C++] constexpr 상수

들어가며

이번 포스트에서는 C++11 부터 새로이 추가된 constexpr 키워드에 대해 살펴 보도록 하겠다. 먼저 constexpr에 대해 이해하기 위해서는 기존 C++의 const에 대해 먼저 이해 해야할 필요가 있다. 아래 예제를 살펴 보자.

int main()
{
    const int c1 = 10;

    c1 = 20; // error!
}

위 예제는 간단하게 const 상수를 정의하고 값을 변경하고 있다. C++에서 const 한정자로 정의된 변수는 값을 바꿀수 없음을 의미하므로 위 코드를 컴파일 하게 되면 에러가 발생하게 된다.

그리고 C++11 부터 상수를 만드는 또 다른 키워드인 constexpr이 등장한다.

    constexpr c2 = 10;
    c2 = 20; // error!

constexpr 역시 변수의 값을 변경할 수 없다는 것을 의미하므로 위 코드도 앞의 예제와 같이 컴파일 에러가 발생한다.

여기서 의문이 들것이다. 기존 C++에 이미 상수를 만드는 키워드가 존재하는데 왜 C++11에서는 constexpr이라는 새로운 키워드를 추가했는가? 그리고 컴파일 타임에 결정되는 상수 값이라는 것은 어떤 의미인가?

이번 포스트는 constexpr이 기존 C++의 const와 차이점은 무엇인지, 어떠한 목적으로 C++11에 추가 되었는지 C와 C++의 표준 제정 역사와 함께 알아 보도록 하겠다.

기존 const의 문제점

앞에서 const와 constexpr을 살펴 보며 변수의 값을 바꿀 수 없다는 것을 의미하는 키워드라는 것을 살펴 보았다. 그렇다면 동일한 역할을 하는 키워드를 왜 추가한 것일까? 이번 섹션에서는 const의 특성과 그에 따른 문제점을 살펴 보고 왜 constexpr이 추가 되어야만 했는지 알아 보도록 하겠다.

아래 코드는 g++에서 컴파일하게 되면 문제 없이 컴파일 되지만 cl 컴파일러(visual studio)에서 컴파일하게 되면 컴파일 에러가 발생한다.

int main()
{
    int arr1[10]; // ok
    
    int s1 = 10;
    int arr2[s1]; // error at C89
    
    return 0;
}

1989년에 처음 제정된 C의 표준 C89에서는 배열을 생성하기 위해서는 '컴파일 타임에 크기를 알 수 있어야 한다'고 정의한다.

위 코드의 arr1의 크기는 상수 10으로 선언 되어 있고, 이는 컴파일 타임에 크기를 알 수 있으므로 정상적으로 컴파일 된다. 하지만 arr2의 경우 런타임에 얼마든지 변경 될 수 있는 '변수' s1 이용해 선언 되었으므로 컴파일 타임에 크기를 알 수 없다. 그러므로 C89 스펙에서는 컴파일 에러가 발생한다.

다시 1999년, C는 다시 한번 표준을 제정하여 C99를 발표하게 된다. C99에서는 '배열의 크기로 변수도 사용 가능'하도록 표준이 변경 되었다. 이는 g++ 컴파일러의  경우에는 정상적으로 지원 되지만 cl 컴파일러, 즉, visual studio에서는 지원되지 않는다. 

int s1 = 10;
int arr2[s1]; // C99. g++ OK, cl error!

C99 버전 컴파일러로 위 코드를 컴파일하게 되면 arr2 부분에서 cl 컴파일러가 에러를 발생 시키는 것을 확인할 수 있다.

이제 부터는 cl 컴파일러로만 예제를 컴파일 한다고 가정하자.

아래 예제는 s2 상수를 만들고 그 변수를 이용해 배열을 선언한다. 아래 코드는 컴파일에 성공할 것인가? 아니면 실패할 것인가?

 

const int s2 = 10;
int arr3[s2]; // ??

변수 s2는 const 한정자로 지정되었고 상수를 이용해 초기화 되고 있으므로 컴파일 타임에 값을 알 수 있다. 그러므로 정상적으로 컴파일에 성공한다.

그럼 이번에는 다음과 같이 변수로 초기화 되는 상수를 이용해 배열을 선언하는 경우는 어떻게 될까?

int s1 = 10;
const int s3 = s1;
int arr4[s3]; // ??

앞에서 cl 컴파일러가 지원하는 C89에서는 배열의 크기는 단순히 상수일 뿐만 아니라 컴파일 타임에 값을 알 수 있어야 한다고 했다. s1은 상수가 아니라 변수이므로 값이 프로그램의 실행 중에 값이 언제든 바뀔수 있다. 컴파일 타임에는 배열의 크기를 알 수 없다는 말이다.

정리하자면 기존 const 키워드는 컴파일 타임에 값을 결정하는 '컴파일 상수'의 의미와 단순히 변수의 값을 변경하지 못하는 '런타임 상수'. 이렇게 두 가지 의미를 동시에 가지고 있다는 것이다.

const 키워드는 '컴파일 상수'와 '런타임 상수'라는 두 가지 의미를 동시에 가진다.

 

int n = 10;
const int c1 = 0; // 컴파일 타임 상수. 배열의 크기로 사용 가능
const int c2 = c1; // 런 타임 상수. 배열의 크기로 사용 불가능

이렇게 하나의 키워드에 여러 의미를 가지고 있다 보니 어쩔때는 배열의 크기로 사용할 수 있고, 어쩔때는 사용할 수 없는, 상황에 따라 다른 의미를 가지는 혼란을 야기하게 되었다.

constexpr 상수의 개념

앞에서 살펴 보았듯이 const는 상황에 따라 다른 의미를 갖는다고 했다. 이를 해결하기 위해 컴파일 타임 상수만으로 사용 될 수 있는 constexpr 이라는 키워드가 C++11 부터 추가 되었다. constexpr은 아래와 같은 특징을 가진다.

  • '컴파일 타임 상수'만을 만들 수 있다.
  • '컴파일 타임에 계산될 수 있는 값'으로만 초기화할 수 있다.
  • '템플릿 인자로 사용'될 수 있다.
int main()
{
    int n = 10;
    constexpr int c3 = 10; // ok
    constexpr int c4 = n; // error!
    
    return 0;
}

constexpr 한정자를 이용해 변수를 초기화하면 위 코드 처럼 const에서는 허용 되었던 변수를 이용해 초기화하는 런타임 상수가 허용되지 않고 컴파일 에러가 발생하는 것을 확인할 수 있다.

constexpr는 배열을 크기를 지정하는 변수에만 사용될 수 있는 것이 아니다. 컴파일 타임에 값이 결정 되므로 C++ 주요 요소중 하나인 템플릿의 인자로도 사용 가능하다. 기존에는 템플릿 인자로 1, 2와 같은 직접 숫자를 표현하는 리터럴 상수만을 사용할 수 있었다면 constexpr 한정자가 붙긴 하지만 어쨋든 변수를 이용해 가독성 높은 코드를 작성할 수 있다는 것이다. 또한 컴파일 타임 상수라고 명시를 해두면 컴파일러가 런타임 상수인지 컴파일 타임 상수인지 헷깔리지 않으므로 컴파일러의 최적화 과정에서도 도움이 된다.

마치며

이상 컴파일 타임 상수로만 사용 될 수 있다는 의미를 가진 키워드인 constexpr에 대해 살펴 보았다. C++11 이상을 지원하는 컴파일러에서는 런타임 상수는 const를 컴파일 타임 상수에는 constexpr을 명확히 구분하여 사용하는 습관을 들이도록 하자.

다음 포스트 constexpr 함수에서는 constexpr 키워드를 이용한 함수관한 내용을 이어서 살펴 보도록 하겠다.

부록 1. 같이 읽으면 좋은 글

유익한 글이었다면 공감(❤) 버튼 꾹!! 추가 문의 사항은 댓글로!!