본문 바로가기
Object Oriented Programming(C++)/Effective C++

Effective C++ | 항목 02 #define을 쓰려거든 const, enum, inline을 떠올리자

by continue96 2021. 8. 21.

항목 02 #define을 쓰려거든 const, enum, inline을 떠올리자

2.1 매크로 상수 대신 const 상수

#define ASPECT_RATIO 1.653

 우리에겐 이 ASPECT_RATIO가 기호식 이름(symbolic name)으로 보이지만, 선행 처리자는 소스 코드가 컴파일러에게 넘어가기 전에 ASPECT_RATIO를 밀어버리고 숫자 상수로 바꾸어 버린다. 그 결과, ASPECT_RATIO라는 이름은 컴파일러가 쓰는 기호 테이블에 들어가지 않는다. 그래서 숫자 상수로 대체된 코드에서 컴파일 에러가 발생하게 되면 대체 1.653이 어디서 왔는지 찾느라 시간을 허비하게 된다. 마찬가지로 기호 테이블에 이름이 들어가지 않기 때문에 기호식 디버거(symbolic debugger)에서도 문제가 나타날 수 있다.

 

const double AspectRatio = 1.653;

 이 문제의 해결법은 매크로 대신 상수를 쓰는 것이다. AspectRatio는 언어 차원에서 지원하는 상수 타입의 데이터이기 때문에 당연히 컴파일러의 눈에 보이고 기호 테이블에도 들어간다. 게다가 상수가 부동소수점 실수 타입일 경우에는 여러 번 쓰이더라도 사본은 딱 한 개만 생기기 때문에 컴파일을 거친 최종 코드의 크기가 #define을 썼을 때보다 작게 나올 수 있다.

 단, 딱 두 가지 경우만 특별히 조심하라고 말하겠다. 첫 번째는 상수 포인터(constant pointer)를 정의하는 경우이다. 포인터는 꼭 const로 선언해 주어야 하고 이와 더불어 포인터가 가리키는 대상까지 const로 선언하는 것이 보통이다.

/* char* 기반의 문자열 상수 */
const char* const authorName = "Scott Meyers";

/* string 기반의 문자열 상수 */
const std::string authorName("Scott Meyers");

 

 두 번째는 클래스 멤버로 상수를 정의하는 경우, 즉 클래스 상수를 정의하는 경우이다. 상수의 유효 범위를 클래스로 한정하고 그 사본의 개수가 한 개를 넘지 못하게 하고 싶다면 정적(static) 멤버로 만들어야 한다.

class GamePlayer {
private:
	/* int형 정적 상수 멤버 Numturns 선언 */
	static const int Numturns = 5;
	int scores[Numturns];
};

/* int형 정적 상수 멤버  Numturns 정의(옵션) */
const int GamePlayer::Numturns;

 

 C++에서는 사용하고자 하는 것에 대해 정의가 마련되어 있어야 하는 게 보통이지만, 이들에 대해 주소를 취하지 않는 한 정적 멤버로 만들어지는 정수류(각종 정수 타입, char, bool 등) 타입의 클래스 내부 상수는 선언만 해도 아무 문제가 없다. 그러나 클래스 내 초기화를 허용하지 않는 정수 외 타입의 상수나 위의 문법이 먹히지 않는 컴파일러를 쓸 때는 초기값을 상수 '정의' 시점에 준다.

class CostEstimate {
private:
	/* double형 정적 상수 멤버 ForgeFactor 선언 */
	static const double ForgeFactor;
};

/* double형 정적 상수 멤버  ForgeFactor 정의(필수) */
const double CostEstimate::ForgeFactor = 1.35;

 조금 오래된 컴파일러는 위의 문법을 받아들이지 않는 경우가 종종 있다. 이유는 정적 클래스 멤버가 선언된 시점에서 초기값을 주는 게 대개 올바르지 않다고 판단하기 때문이다. 그렇기 때문에 구식 컴파일러에 대한 배려로 나열자 둔갑술(enum hack) 기법을 생각할 수 있다.

class GamePlayer {
private:
	enum { Numturns = 5 }; /* 나열자 둔갑술은 Numturns를 5의 기호식 이름으로 만듭니다. */
	int scores[Numturns];
};

 

2.2 매크로 함수 대신 인라인 함수

 상당히 많은 경우에서 발견할 수 있는 #define 지시자의 또 다른 오용 사례는 매크로 함수이다. 매크로를 작성할 때는 매크로 본문에 들어있는 인자마다 반드시 괄호를 씌워주어야 한다. 그런데 기상천외한 괴현상은 또 나타나니, 바로 전위 증감 연산자를 전달하였을 때 비교를 통해 처리한 결과에 따라 a가 증가하는 횟수가 달라진다.

#define CALL_WITH_MAX(a, b) ((a) > (b) ? (a) : (b))

int a = 5, b = 0;
CALL_WITH_MAX(++a, b); /* a가 두 번 증가하여 7이 됩니다. */
CALL_WITH_MAX(++a, b+10); /* a가 한 번 증가하여 6이 됩니다. */

 

 그러나 C++에서는 기존 매크로의 효율을 그대로 유지함은 물론이고 정규 함수의 모든 동작 방식 및 타입 안전성까지 완벽히 취할 수 있는 방법이 있으니, 바로 인라인 함수에 대한 템플릿이다. 이 함수는 템플릿이기 때문에 함수 본문에 괄호를 칠 필요가 없고 인자를 여러 번 평가하지도 않는다.

template<typename T>
inline void callWithMax(const T& a, const T& b) {
	a > b ? a : b;
}

int a = 5, b = 0;
callWithMax(++a, b); /* a가 한 번 증가하여 6이 됩니다. */
callWithMax(++a, b+10); /* a가 한 번 증가하여 6이 됩니다. */

 

NOTE
① 단순한 상수를 쓸 때는 #define보다 const 객체 혹은 enum class를 우선 생각한다.
② 함수처럼 쓰이는 매크로를 만들려면 #define 매크로보다 inline 함수를 우선 생각한다.

댓글