Coding Memo

[C++] 상수 (define, constexpr) 본문

Language/C++

[C++] 상수 (define, constexpr)

minttea25 2023. 7. 25. 10:53

C++에서 상수를 정의하는 방법으로 2가지가 있다.

 

첫 번째로, define을 이용하여 매크로로 상수를 정의하는 방법이 있고,

두 번째로는 constexpr을 이용하여 상수를 정의하는 방법이 있다.

#define PI 3.141592

constexpr double PI = 3.141592;

 

두 방법의 특징과 차이점을 보고 무엇을 사용하면 더 좋을지 확인해보자.


define

 

define은 전처리기(preprocessor) 지시자로, C++의 전처리기를 통해 상수를 정의하는 방법이다. 이는 매크로를 이용하는데, 매크로는 컴파일 전에 사용된 매크로값이 텍스트 치환을 통해 해당 정의 내용으로 그대로 바뀐다.

(말 그대로 매크로 값에 정의했던 내용이 그대로 들어간다. 단순히 텍스트 치환 기능이다.)

define의 경우 별도의 타입 검사나 범위 지정이 없기 때문에, 텍스트 치환 후 컴파일 과정에서 다른 헤더 파일에 정의되어 있는 매크로 값과 사용자가 정의한 매크로 값이 중복되어 오류나 모호성이 발생할 수 있고, 컴파일 이후 런타임에서 타입에 대한 예상치 못한 오류나 예외가 발생할 수도 있다.

 

#define PI 3.14
#define SQUARE(x) ((x) * (x))

...
double s = SQUARE(5) * PI;
...

위 코드에서 컴파일 시, SQUARE 부분과 PI 부분의 다음과 같이 치환된다.

#define PI 3.14
#define SQUARE(x) ((x) * (x))

...
double s = (5 * 5) * 3.14;
...

constexpr

 

constexpr은 C++11부터 추가된 키워드로, 변수 뿐만 아니라 함수까지도 컴파일 시간에 미리 계산될 수 있도록 지정할 수 있는 키워드이다. 런타임 시 결정되는 값이 아닌 컴파일 시 결정되는 값으로 런타임 중에는 상수로 취급된다.

define과 달리 변수를 선언하는 방법과 같게, 타입 지정이나 리터럴 지정이 가능하다.

constexpr double PI = 3.14;

// 함수도 constexpr로 표현하여 컴파일 전에 미리 계산 할 수 있다.
constexpr int square(int x) { return x * x; }

위 선언 이후에 등장하는 PI는 3.14라는 값의 상수로 취급되고 코드 상에 등장하는 square 함수는 x의 값에 맞게 미리 계산된다.

예를 들어 square(3)이라는 코드가 있으면 이 코드는 컴파일 시 3*3=9라는 값으로 컴파일 된다.

constexpr 경우는 컴파일 시간에 결정되는 타입에 대해서만 사용할 수 있다는 것에 주의하자. (volatile 등 사용 불가)

 

constexpr 함수 사용시 조건사항

1. 함수 바깥의 데이터를 읽거나 쓰면 안 된다.

2. if나 for 같은 제어 구조가 없어야 한다.

3. 하나의 계산 문장만 간단하게 담을 수 있다.

4. 다른 constexpr 함수만 호출이 가능하다.

 

***지역변수가 없어야한다는 조건은 C++14 부터 사라졌다고 한다.***

 

추가적으로, 템플릿도 사용가능하고, 컴파일 시점 계산을 지원하는 형식인 경우에는 사용자 정의 형식(객체)도 사용할 수 있다.

template <typename T>
constexpr T square(T x) { return x * x; }

constexpr int factorial(int n) {
  if (n == 0) return 1;
  else return n * factorial(n - 1);
}

constexpr 함수를 잘 활용하면 프로그램 실행 시간을 줄일 수 있다.

 

Note: 몇몇 C++ 버전과 컴파일러에 따라 주요 함수들이 constexpr로 구현되어 있는 경우도 있고 그렇지 않는 경우도 있으니까 주의하자. 
(cmath의 floor 함수가 g++ 컴파일러에서는 constexpr로 구현되어 constexpr 함수에 이 함수를 호출 할 수 있지만, clang++에서는 floor가 constexpr로 구현되어 있지 않기때문에 호출할 수 없다.)

 

Note2: C++20 부터는 string과 vector 또한 특정 조건 하에 constexpr에 사용할 수 명세되어 있다.. (하지만 최종적으로는 컴파일러에 따라 지원 여부는 갈린다.)

 

Note3: constexpr 키워드는 대부분은 컴파일 시점에서 값을 결정하지만, 컴파일러가 런타임에 결정하는 것이 더 낫다고 판단할 경우 그렇게 하기도 한다. 대신에, consteval (C++20) 키워드를 사용하면 이를 무시하고 반드시 컴파일 시점에서 평가하라고 강제할 수 있다.


그래서 무엇을 사용해야 하는가?

 

문자열 치환 등과 같이 반드시 define으로 정의하여 텍스트 치환을 목적으로 사용할 것이 아니라면 constexpr을 사용하도록 하자.

 

사실, define으로 매크로를 정의하여 사용하는 방법은 오래된 방법(old)이다. 여러 서적과 글들을 보면 constexpr을 사용하는 것을 권장하고 있다. 

 

define을 사용 시 타입 문제가 생길 수도 있고 단순 텍스트 치환이기 때문에 컴파일 상 이점도 없다. 따라서 C++의 언어적 기능을 활용하기도 어렵게한다.

 

반면에, constexpr은 타입 검사가 가능하고 함수 호출에 대한 constexpr를 사용하였을 때, 실행 시간을 줄일 수 있다는 이점도 존재한다. 더 안전하고 확실하고 좋은 성능을 기대할 수 있는 방법이다.


정리

 

define과 constexpr 모두 컴파일 시점에서 이루어지지만 define은 단순히 텍스트 치환으로 타입 검사를 하지 않는 반면에, constexpr은 타입을 명시적으로 지정하여 타입 검사를 한다.

또한 constexpr은 함수에도 사용할 수 있는데, 이는 컴파일 시에 함수 값을 미리 계산하여 프로그램의 성능 향상을 기대할 수도 있다.