02: #define을 쓰려거든 const, enum, inline을 떠올리자
핵심은 가급적 선행 처리자보다 컴파일러를 더 가까이 하자이다. #define은 전처리기에서 처리되므로 컴파일러가 사용할 기호 테이블에 들어가지 않는다. 이 때, 문제가 야기될 수 있는데 다음을 보자.
#define ASPECT_RATIO 1.653
컴파일러에게 넘어가기 전에 전처리기에선 ASPECT_RATIO를 숫자 상수로 모두 바꾸어 버린다. 이렇게 숫자 상수로 대체된 코드에서 컴파일 에러가 발생하면 에러 메시지에는 ASPECT_RATIO대신 1.653이 있을 것이다.
이런 문제의 해결법은 매크로 대신 상수를 쓰는 것이다.
const double AspectRatio = 1.653;
이렇게 하면 당연히 컴파일러의 기호 테이블에도 들어가며 #define보다 코드의 크기도 작게 나올 수 있다.
#define을 상수로 교체할 때 특별히 조심할 두 가지
첫째는 상수 포인터를 정의하는 경우이다. 다른 소스 파일이 include해서 사용할 수 있도록 상수 정의는 대개 헤더에 하는 것이 상례이다. 여기서 포인터는 꼭 const로 선언해 주어야 하고, 이와 아울러 포인터가 가리키는 대상까지 const로 선언하는 것이 보통이다.
const char* const authorName = "Scott Meyers";
두번째는 클래스 멤버로 상수를 정의하는 경우, 즉 클래스 상수를 정의하는 경우이다. 어떤 상수의 유효범위를 클래스로 한정하고자 할 때는 그 상수를 멤버로 만들어야 하는데, 그 상수의 사본 개수가 한 개를 넘지 못하게 하고 싶다면 정적 멤버로 만들어야 한다.
class GamePlayer
{
private:
static const int NumTurns = 5;
int scores[NumTurns];
};
위의 NumTurns는 선언된 것이지 정의된 것은 아니다. 선언은 메모리 할당이 아직 되지 않은 것이다. C++에서는 보통 우리가 사용하고자 하는 것에 대해서는 정의가 마련되어 있어야 한다. 하지만 정적 멤버로 만들어지는 정수류 타입의 클래스 내부 상수는 예외이다. 이들에 대해 주소를 취하지 않는 한, 정의 없이 선언만 해도 아무 문제가 없다.
#define으로 클래스 상수를 만드는 것은 말이 안 된다. #define은 유효범위도 모르고 일단 정의되면 컴파일이 끝날 때까지 유효하다는 점이다. 캡슐화 혜택도 받을 수 없다.
구식 컴파일러의 경우
조금 오래된 컴파일러에서는 위의 정적 멤버 선은을 받아들이지 않는 경우가 종종 있다. 정적 클래스 멤버가 선언된 시점에 초기값을 주는 것이 대개 맞지 않다고 판단하기 때문이다. 구식 컴파일러를 쓸 때는, 초기값을 상수 정의 시점에 주도록 하자.
class CostEstimate
{
private:
static cons double FudgeFactor; // 정적 클래스 상수의 선언
};
const double CostEstimate::FudgeFactor = 1.35; // 정적 클래스 상수의 정의
웬만한 경우라면 위의 것으로 충분하다. 예외가 있다면 해당 클래스를 컴파일하는 도중에 클래스 상수의 값이 필요할 때이다. 이 때는 나열자 둔갑술(enum hack)을 생각할 수 있다.
class GamePlayer
{
private:
enum { NumTurns = 5 };
int scores[NumTurns];
};
enum타입은 int가 놓일 곳에도 쓸 수 있다는 C++의 진실을 적극 활용하는 기법이다. 이 기법을 알아 두는 것은 두 가지 장점이 있다.
첫째, 나열자 둔갑술은 동작 방식이 const보다 #define에 더 가깝다. 상수의 주소를 잡아내는 것은 합당하지만 #define이나 enum의 주소를 얻는 것은 맞지 않다. 그러므로 내가 선언한 정수 상수를 가직 다른 사람이 주소를 얻는다든지 참조자를 쓴다든지 하는 것을 막을 수 있다.
둘째, 상당히 많은 코드에서 이 기법이 쓰이고 있다. 눈을 단련해두자.
#define 지시자의 또 다른 오용 사례 매크로 함수
#define CALL_WITH_MAX(a, b) f((a) > (b) ? (a) : (b))
int a = 5, b= 0;
CALL_WITH_MAX(++a, b);
CALL_WITH_MAX(++a, b + 10);
위의 예시는 하나의 괴현상이 발생한다. 상황에 따라 a가 1번 증가하거나 2번 증가한다. f에 들어가는 b의 값에 따라 달라진다.
기존 매크로의 효율을 그대로 유지하고 정규 함수의 모든 동작방식 및 타입 안정성까지 완벽히 취할 수 있는 방법이 inline함수에 대한 템플릿을 준비하는 것이다.
template<typename T>
inline void callWithMax(const T& a, const T& b)
{
f(a > b ? a : b);
}
이것만은 잊지 말자!
- 단순한 상수를 쓸 때는, #define보다 const 객체 혹은 enum을 우선 생각하자.
- 함수처름 쓰이는 매크로를 만들려면, #deifne 매크로보다 inline함수를 우선 생각하자.
'C++ > Effective C++' 카테고리의 다른 글
[Effective C++] 06: 컴파일러가 만들어낸 함수가 필요 없으면 확실히 이들의 사용을 금해 버리자 (0) | 2023.10.02 |
---|---|
[Effective C++] 05: C++가 은근슬쩍 만들어 호출해 버리는 함수들에 촉각을 세우자 (1) | 2023.10.02 |
[Effective C++] 04: 객체를 사용하기 전에 반드시 그 객체를 초기화하자 (0) | 2023.09.30 |
[Effective C++] 03: 낌새만 보이면 const를 들이대 보자! (0) | 2023.09.28 |
[Effective C++] 01: C++를 언어들의 연합체로 바라보는 안목은 필수 (0) | 2023.09.28 |