아래 줄과 비슷한 코드를 썼다고 가정해 보죠


#define ASPECT_RATIO 1.653


우리에겐 이미 ASPECT_RATIO 가 기호식 이름(symbolic name) 으로 보이지만 컴파일러에겐 전혀 보이지 않습니다.

소스 코드가 어떻게든 컴파일러에게 넘어가기 전에 선행 처리자가 밀어버리고 숫자 상수로 바꾸어 버리기 때문입니다.


소스 코드엔 분명히  ASPECT_RATIO가 있었는데 에러 메시지엔 1.653이 있어 여러분이 작성한 것이 아니면 대체 1.653이 어디에서 왔는지 모를수도 있고, 이것을 찾아 들어가느라 시간을 허비할 일도 생킬 테지요


이 문제의 해결법은 매크로 대신 상수를 쓰는 것입니다.


const double AspectRatio = 1.653;    // 대문자로만 표기하는 이름은 대개 매크로에

   // 쓰는 것이어서, 이름 표기도 바꿉니다.


AspectRatio는 언어 차원에서 지원하는 상수 타입의 데이터이기 때문에 당연히 컴파일러의 눈에도 보이며 기호 테이블에도 당연히 들어갑니다.



참, #define을 상수로 교체하려는 분께는 딱 두 가지 경우만 특별히 조심하라고 말씀드리겠습니다.


첫째는 상수 포인터(constant pointer)를 정의하는 경우입니다.

상수 정의는 대게 헤더 파일에 넣는 것이 상례이므로(다른 소수 파일이 이것을 인클루드해서 쓰게 되지요) 포인터(pointer)는 꼭 const로 선언해 주어야 하고, 이와 아울러 포인터가 가리키는 대상까지 const로 선언하는 것이 보통입니다.


이를테면 어떤 헤더 파일 안에 char* 기반의 문자열 상수를 정의한다면 다음과 같이  const를 두번 써야 한다는 말입니다.


const char* const authorName = "Scott Meyears";


참, 문자열 상수를 쓸 때 위와 같이 char* 기반의 구닥다리 문자열보다는 string 객체가 대체적으로 사용하기 괜찮습니다.


const std::string authorName("Scott Meyers");





두 번째 경우는 클래스 멤버로 상수를 정의하는 경우, 즉 클래스 상수를 정의하는 경우입니다.


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


class GamePlayer{

private:

static const int NumTurns = 5;        //상수 선언

int scores[NumTurns];             // 상수를 사용하는 부분

...

};


위에서 보신 NumTurns는 '선언(declaration)'된 것입니다. '정의'가 아니니 주의하세요.

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


이들에 대해 주소를 취하지 않는 한, 정의 없이 선언만 해도 아무 문제가 없게 되어 있습니다. 단, 클래스 상수의 주소를 구한다든지, 여러분이 주소를 구하지 않는데도 여러분이 쓰는 컴파일러가 잘못 만들어진 관계로 정의를 달라고 떼쓰는 경우에는 별도의 정의를 제공해야 합니다. 아래가 그 예입니다.


const int GamePlayer::Numturns;    // NumTurns의 정의. 값이 주어지지 않는

// 이유는 아래를 계속 보시면 나옵니다.


이때 클래스 상수의 정의는 구현 파일에 둡니다. 헤더 파일에는 두지 않습니다. 정의에는 상수의 초기값이 있으면 안 되는데, 왜냐하면 클래스 상수의 초기값은 해당 상수가 선언된 시점에서 바로 주어지기 때문입니다(즉, NumTurns는 선언될 당시에 바로 초기화된다는 것입니다).


조금 오래된 컴파일러는 위의 문법을 받아들이지 않는 경우가 종종 있습니다. 이유는 간단합니다. 정적 클래스가 멤버가 선언된 시점에 조기값을 주는 것이 대개 맞지 않다고 판단하기 때문이죠. 게다가 클래스 멤버가 내부 초기화를 허용하는 경우가 정수 타입의 상수에 대해서만 국한되어 있으니 말입니다. 어쨌든 위의 문법이 먹히지 않는 컴파일러를 쓸 때는, 초기값을 상수 '정의' 시점에 주도록 하십시오.


class CostEstimate{

private:

static const double FudgeFactor;        // 정적 클래스 상수의 선언

....                                                // 이것은 헤더 파일에 둡니다.

};


const double CostEstimate::FudgeFactor = 1.35;        // 정적 클래스 상수의 정의 , 이것은 구현 파일에 둡니다.


웬만한 경우라면 이것으로 충분합니다. 딱 한 가지 예외가 있다면 해당 클래스를 컴파일하는 도중에 클래스 상수의 값이 필요할 때인데, 이를테면 GamePlayer::scores 등의 배열 멤버를 선언할 때가 대표적인 예입니다(컴파일러는 컴파일 과정에서 이 배열의 크기를 알아야 한다며 버틸 것입니다.) 그렇기 때문에 정수 타입의 정적 클래스 상수에 대한 클래스 내 초기화를 금지하는(다시 말하지만 이것은 표준에 어긋난 구식입니다) 구식 컴파일러에 대한 배려로서 괜찮은 방법을 추천한다면, '나열자 둔갑술(enum hack)' 이라는 통칭으로 멋스럽게(그리고 냄새 나지 않는 이미지로) 알려진 기법을 생각할 수 있겠습니다.


class GamePlayer{

private:

enum { NumTurns = 5 };        // "나열자 둔갑술": NumTurns를

// 5에 대한 기호식 이름으로 만듭니다.


int scores[NumTurns];            // 깔끔하게 해결!

};





#define  지시자의 또 다른 오용 사례는 매크로 함수입니다. 함수처럼 보이지만 함수호출 오버헤드를 일으키지 않는 매크로를 구현하는 것이지요. 일단 아래의 예를 보세요. 매크로 인자들 중 큰것을 사용해서 어떤 함수 f를 호출하는 매크로 입니다.


// a와 b중에 큰 것을 f에 넘겨 호출합니다.

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


이런 식의 매크로는 단점이 한두 개가 아닙니다. 그냥 생각만 해 보는 데도 마음이 아프죠.


다른 프로그래밍 책에서 익히 들어왔겠지만, 이런 매크로를 작성할 때는 매크로 본문에 들어 있는 인자마다 반드시 괄호르 ㄹ씌워 주는 센스를 잊지 말아야 합니다. 이게 안 되어 있으면, 표현식을 매크로에 넘길 때 골치 아픈 일이 발생할 수 있으니까요. 그런데 이 부분을 제대로 처리한다고 해서 끝난 것일까요? 괴현상을 아래에서 직접 만나보시기 바랍니다.


int a = 5, b = 0;


CALL_WITH_MAX(++a, b);                // a가 두 번 증가합니다.

CALL_WITH_MAX(++a, b+10);           // a가 한 번 증가합니다.


어떻습니까? f가 호출되기 전에 a가 증가하는 횟수가 달라지죠? 바로, 비교를 통해 처리한 결과가 어떤 것이냐에 따라 달라지니 까무러칠 노릇이죠.


C++에서는 함수 호출을 없애 준다는 명목 하에 자행되는 이런 어처구니없는 작태를 참을 필요가 없습니다. 천만다행이죠 기존 매크로의 효율을 그대로 유지함은 물론 정규 함수의 모든 동작방식 및 타입 안정성까지 완벽히 취할 수 있는 방법이 있으니까요 바로, 인라인 함수에 대한 템플릿(항목 30 참조)을 준비하는 것입니다.


template<typename T>                                        // T가 정확히 무엇인지

inline void callWithmax(const T& a, const T& b)        // 모르기 때문에, 매개변수로

{                                                                     // 상수 객체에 대한 참조자를

f(a > b ? a : b);                                            // 씁니다. 항목 20 참조

}





const, enum, inline의 친절한 손길이 우리 가까이에 있다는 사실을 늘 유념해 두면, 선행 처리자(특히 #define)를 꼭 써야 하는 경우가 많이 줄어들게 됩니다. 그렇다고 현실적으로 완전히 뿌리 뽑기는 힘듭니다. 예를 들어 #include는 부동의 필수 요소로 남아 있고, #ifdef  /  #ifndef도 컴파일 조정 기능으로 현장에서 아주 잘 뛰고 있습니다. 선행 처리자는 은퇴시기가 아직 꽤 남았다고 봅니다만, 기회가 될 때마다 장기 휴가를 자주 보내줌으로써 마음의 준비를 해 두도록 합시다.



이것만은 잊지 말자!

* 단순한 상수를 쓸 때는, #define보다 const 객체 혹은 enum을 우선 생각합시다.

* 함수처럼 쓰이는 매크로를 만들려면, #define 매크로보다 인라인 함수를 우선 생각합시다.

+ Recent posts