들어가며
이번 포스트에서는 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 키워드를 이용한 함수관한 내용을 이어서 살펴 보도록 하겠다.