참고 서적 : Effective C++ - Meyers / 곽용재 옮김 / 피어슨에듀케이션코리아
1. C++에 왔으면 C++의 법을 따릅시다.
가장 근본적인 것들을 다루고 있는 단원.
항목 2 : #define을 쓰려거든 const, enum, inline을 떠올리자
#define은 C++ 언어 자체의 일부가 아닌 것으로 취급될 수 있습니다. 아래 줄과 비슷한 코드를 썼다고 가정해 봅시다.
#define ASPECT_RATIO 1.653 |
우리에겐 이미 ASPECT_RATIO가 기호식 이름(symbol name)으로 보이지만 컴파일러에겐 전혀 보이지 않습니다. 소스 코드가 어떻게든 컴파일러에게 넘어가기 전에 선행 처리자가 밀어버리고 숫자 상수로 바꾸어 버리기 때문입니다. 그 결과로, ASPECT_RATIO라는 이름은 컴파일러가 쓰는 기호 테이블에 들어가지 않지요. 그래서 숫자 상수로 대체된 코드에서 컴파일 에러라도 발생하게 되면 꽤나 헷갈릴 수 있습니다. 소스 코드엔 분명히 ASPECT_RATIO가 있었는데 에러 메시지엔 1.653이 있으니까요. 이 문제는 기호식 디버거(symbolic debugger)에서도 나타날 소지가 있습니다. 마찬가지로 기호 테이블에 이름이 들어가지 않기 때문입니다.
이 문제의 해결법은 매크로 대신 상수를 쓰는 것입니다.
const double AspectRatio = 1.653; |
AspectRatio는 언어 차원에서 지원하는 상수 타입의 데이터이기 때문에 당연히 컴파일러의 눈에도 보이며 기호 테이블에도 당연히 들어갑니다. 게다가(위의 예제처럼) 상수가 부동소수점 실수 타입일 경우에는 컴파일을 거친 최종 코드의 크기가 #define을 썼을 때보다 작게 나올 수 있습니다. 가만히 생각해 보면 이유를 알 수 있는데요. 매크로를 쓰면 코드에 ASPECT_RATIO가 등장하기만 하면 선행 처리자에 의해 1.653으로 모두 바뀌면서 결국 목적 코드 안에 1.653의 사본이 등장 횟수만큼 들어가게 되지만, 상수 타입의 AspectRatio는 아무리 여러 번 쓰이더라도 사본은 딱 한 개만 생기기 때문입니다.
#define을 상수로 교체할 때 특별히 조심해야 할 두 가지를 말씀 드리겠습니다. 첫째는 상수 포인터(constant pointer)를 정의하는 경우입니다. 상수 정의는 대개 해더 파일에 넣는 것이 상례이므로 포인터(pointer)는 꼭 const로 선언해 주어야 하고, 이와 아울러 포인터가 가리키는 대상까지 const로 선언하는 것이 보통입니다. 이를테면 어떤 헤더 파일 안에 char* 기반의 문자열 상수를 정의한다면 다음과 같이 const를 두 번 써야 한다는 말입니다.
const char* const authorName = "Scott Meyers"; |
문자열 상수를 쓸 때 위와 같이 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는 선언될 당시에 바로 초기화된다는 것입니다.)
그런데 주의할 것이 하나 있습니다. 혹시 클래스 상수를 #define으로 만드실 생각을 해보는 건 아니죠? 방법 자체가 말이 안 됩니다. 대저 #define은 유효범위란 게 뭔지도 모르는 피조물이니까요. 매크로는 일단 정의되면 컴파일이 끝날 때까지(중간에 #undef되지만 않으면) 유효하다는 점을 기억해 두시기 바랍니다. 정리하면, #define은 클래스 상수를 정의하는 데 쓸 수도 없을 뿐 아니라 어떤 형태의 캡슐화 혜택도 받을 수 없습니다. 말자하면 ‘private’ 성격의 #define 같은 것은 없다는 이야기입니다. 물혼, 이와 대조적으로 상수 데이터 멤버는 캡슐화가 되죠. NumTurns가 그거입니다.
위의 문법이 먹히지 않는 컴파일러를 쓸 때는, 초기값을 상수 ‘정의’ 시점에 주도록 하십시오.
class CostEstimate { private: static const double FudgeFactor; // 정적클래스상수의선언 ... // 이것은헤더파일에둡니다. }; const double // 정적클래스상수의정의 CostEstimate::FudgeFactor = 1.35; // 이것은구현파일에둡니다. |
웬만한 경우라면 이것으로 충분합니다. 딱 한 가지 예외가 있다면 해당 클래스를 컴파일하는 도중에 클래스 상수의 값이 필요할 때인데, 이를테면 GamePlayer::scores 등의 배열 멤버를 선언할 때가 대표적인 예입니다.(컴파일러는 컴파일 과정에서 이 배열의 크기를 알아야 한다며 버틸 것입니다.) 이럴 때는 ‘나열자 둔갑술(enum hack)’이라는 기법을 생각할 수 있겠습니다. 이 기법의 원리는 나열자(enumerator) 타입의 값은 int가 놓일 곳에도 쓸 수 있다는 C++의 진실을 적극 활용하는 것입니다. 그러니까 GamePlayer는 다음과 같이 정의할 수 있다는 거죠.
class GamePlayer { private: enum { NumTurns = 5 // "나열자둔갑술" : NumTurns를5에대한기호식이름으로만듭니다. }; int scores[NumTurns]; // 깔끔하게해결! ... }; |
이 나열자 둔갑술은 알아 두는 것이 여러 가지 이유로 피가 되고 살이 됩니다.
첫째, 나열자 둔갑술은 동작 방식이 const보다는 #define에 더 가깝습니다.
둘째, 상당히 많은 코드에서 이 기법이 쓰이고 있으므로 혹시 이런 것을 발견하면 쉽게 알아보도록 눈을 단련시켜 두라는 것입니다. 이 나열자 둔갑술은 템플릿 메타프로그래밍의 핵심 기법이기도 합니다.
상당히 많은 경우에서 발견할 수 있는 #define 지시자의 또 다른 오용 사례는 매크로 함수입니다. 함수처럼 보이지만 함수 호출 오버헤드를 일으키지 않는 매크로를 구현하는 것이지요. 아래의 예를 보세요. 아래의 예는 매크로 인자들 중 큰 것을 사용해서 어떤 함수 f를 호출하는 매크로입니다.
// a와b 중에큰것을f에넘겨호출합니다. #define CALL_WITH_MAX(a, b) f((a) > (b) ? (a) : (b)) |
이런 식의 매크로는 단점이 한두 개가 아닙니다. 이런 매크로의 단점을 보완하는 방법이 있습니다. 기존 매크로의 효율을 그대로 유지함은 물론 정규 함수의 모든 동작방식 및 타입 안전성까지 완벽히 취할 수 있는 방법이 있으니까요. 바로, 인라인 함수에 대한 템플릿을 준비하는 것입니다.
// T가정확히무엇인지모르기때문에, 매개변수로상수객체에대한참조자를씁니다. template<typename T> inline void callWithMax(const T& a, const T& b) { f(a > b ? a : b); } |
이 함수는 템플릿이기 때문에 동일 계열 함수군(family of functions)을 만들어냅니다. 동일한 타입의 객체 두 개를 인자로 받고 둘 중 큰 것을 f에 넘겨서 호출하는 구조입니다. 보시면 알겠지만 함수 본문에 괄호로 분칠을 해 댈 필요가 없고, 인자를 여러 번 평가할지도 모른다는 걱정도 없어집니다. 그뿐 아니라 callWithMax는 진짜 함수이기 때문에 유효범위 및 접근 규칙을 그대로 따라갑니다.
Const, enum, inline의 친절한 손길이 우리 가까이에 있다는 사실을 늘 유념해 두면, 선행 처리자(특히 #define)를 꼭 써야 하는 경우가 많이 줄어들게 됩니다.
이것만은 잊지 말자!
l 단순한 상수를 쓸 때는, #define보다 const 객체 혹은 enum을 우선 생각합시다.
l 함수처럼 쓰이는 매크로를 만들려면, #define 매크로보다 인라인 함수를 우선 생각합시다.
'Programming > C/C++' 카테고리의 다른 글
[Effective C++] 객체를 사용하기 전에 반드시 그 객체를 초기화 하자 (0) | 2015.04.16 |
---|---|
[Effective C++] 낌새만 보이면 const를 들이대 보자! (0) | 2015.04.16 |
[Effective C++] C++를 언어들의 연합체로 바라보는 안목은 필수 (0) | 2015.04.16 |
[C] 30자리 곱셈 연산 프로그램 (0) | 2015.04.16 |
[C++] 연산자 우선순위 (0) | 2015.04.16 |