[Effective C++]#define을 쓰려거든 const, enum, inline을 떠올리자

C++에 왔으면 C++의 법을 따릅시다, 두 번째 이야기

Posted by SungBeom on November 20, 2019 · 7 mins read

매크로 상수보다 const 객체

#define ASPECT_RATIO 1.653
우리에겐 ASPECT_RATIO가 기호식 이름(symbolic name)으로 보이지만 컴파일러에겐 전혀 그렇게 보이지 않습니다. 소스 코드가 컴파일러에게 넘어가기 전에 선행 처리자가 ASPECT_RATIO를 숫자 상수로 바꾸어 버리기 때문입니다. 그 결과로, ASPECT_RATIO라는 이름은 컴파일러가 쓰는 기호 테이블에 들어가지 않지요. 그래서 숫자 상수로 대체된 코드에서 컴파일 에러가 발생하게 되면, 소스 코드엔 분명히 ASPECT_RATIO가 있었는데 에러 메시지엔 1.653이 있으므로 꽤나 헷갈릴 수 있습니다.

위 문제의 해결법은 매크로 대신 상수를 쓰는 것입니다.
const double AspectRatio = 1.653;
AspectRatio는 언어 차원에서 지원하는 상수 타입의 데이터이기 때문에 당연히 컴파일러의 눈에도 보이며 기호 테이블에도 당연히 들어갑니다. 게다가 상수가 부동소수점 실수 타입일 경우에는 컴파일을 거친 최종 코드의 크기가 #define을 썼을 때보다 작게 나올 수 있습니다. 매크로를 쓰면 코드에 존재하는 모든 ASPECT_RATIO가 선행 처리자에 의해 모두 1.653으로 바뀌면서 목적 코드 안에 1.653의 사본이 ASPECT_RATIO의 등장 횟수만큼 들어가게 되지만, 상수 타입의 AspectRatio는 아무리 여러 번 쓰이더라도 사본은 딱 한 개만 생기기 때문입니다.

#define을 상수로 교체할 때 주의점

#define을 상수로 교체할 때 특별히 조심해야하는 경우 2가지가 있습니다.
첫 번째는 상수 포인터(constant pointer)를 정의하는 경우입니다. 포인터(pointer)는 꼭 const로 선언해 주어야 하고, 포인터가 가리키는 대상까지 const로 선언하는 것이 보통입니다.
const char * const autorName = "Scott Meyers";
다만 문자열 상수를 쓸 때는 char* 기반의 문자열보다는 string 객체가 대체적으로 사용하기 좋습니다.
const std::string authorName("Scoot Meyers");

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

1
2
3
4
5
6
class GamePlayer {
private:
    static const int NumTurns = 5;  // 상수 선언
    int scores[NumTurns];           // 상수를 사용하는 부분
    ...
};
cs

위에서 보신 NumTurns는 '선언(declaration)된 것입니다. C++에서는 사용하고자 하는 것에 대해 '정의'가 마련되어 있어야 하는 게 보통이지만, 정적 멤버로 만들어지는 정수류(각종 정수 타입, char, bool 등) 타입의 클래스 내부 상수는 예외입니다.

#define은 클래스 상수를 정의하는데 쓸 수도 없을 뿐 아니라 어떤 형태의 캡슐화 혜택도 받을 수 없습니다. 대조적으로 상수 데이터 멤버는 캡슐화가 됩니다.

조금 오래된 컴파일러는 위의 문법을 받아들이지 않는 경우가 종종 있습니다. 정적 클래스 멤버가 선언된 시점에 초기값을 주는 것이 대개 맞지 않다고 판단하기 때문입니다. 해당 컴파일러를 쓸 때는, 초기값을 상수 '정의' 시점에 주어야 합니다.

1
2
3
4
5
6
7
8
class CostEstimate {
private:
    static const double FudgeFactor;   // 정적 클래스 상수의 선언
    ...                                // 이것은 헤더 파일에 둡니다.
};
 
const double                           // 정적 클래스 상수의 정의
    CostEstimate::FudgeFactor = 1.35;  // 이것은 구현 파일에 둡니다.
cs

나열자 둔갑술

웬만한 경우라면 이것으로 충분하지만, 해당 클래스를 컴파일하는 도중에 클래스 상수의 값이 필요할 때는 예외입니다. 이러한 경우에 괜찮은 방법은 '나열자 둔갑술(enum hack)'입니다.

1
2
3
4
5
6
7
8
class GamePlayer {
private:
    enum { NumTurns = 5 };  // "나열자 둔갑술": NumTurns를
                            // 5에 대한 기호식 이름으로 만듭니다.
 
    int scores[NumTurns];   // 깔끔하게 해결!
    ...
};
cs

나열자 둔갑술을 알아 두는 것은 여러 가지 이유로 큰 도움이 됩니다. 나열자 둔갑술은 동작 방식이 const 보다는 #define에 더 가깝고, 상당히 많은 코드에서 쓰이는 기법으로 템플릿 메타프로그래밍의 핵심 기법입니다.

매크로 함수보다 인라인 함수

#define 지시자의 또 다른 오용 사례는 매크로 함수입니다.
#define CALL_WITH_MAX (a, b) f((a) > (b) ? (a) : (b))
매크로를 작성할 때는 매크로 본문에 들어 있는 인자마다 반드시 괄호를 씌워 주어야 합니다.

1
2
3
4
int a = 5, b = 0;
 
CALL_WITH_MAX(++a, b);     // a가 두 번 증가합니다.
CALL_WITH_MAX(++a, b+10);  // a가 한 번 증가합니다.
cs

비교를 통한 처리 결과가 어떤 것이냐에 따라 a가 증가하는 횟수가 달라집니다.

기존 매크로의 효율을 그대로 유지함은 물론 정규 함수의 모든 동작방식 및 타입 안전성까지 완벽히 취할 수 있는 방법이 있습니다. 인라인 함수에 대한 템플릿을 준비하는 것입니다.

1
2
3
4
5
template<typename T>                             // T가 확히 무엇인지
inline void callWithMax(const T& a, const T& b)  // 모르기 때문에, 매개변수로
{                                                // 상수 객체에 대한 참조자를
    f(a > b ? a : b);                            // 씁니다.
}
cs

이 함수는 템플릿이기 때문에 동일 계열 함수군(family of functions)을 만들어냅니다. 동일한 타입의 객체 두 개를 인자로 받고 둘 중 큰 것을 f에 넘겨서 호출하는 구조입니다.


정리

단순한 상수를 쓸 때는, #define보다 const 객체 혹은 enum을 우선 생각합시다.
함수처럼 쓰이는 매크로를 만들려면, #define 매크로보다 인라인 함수를 우선 생각합시다.