Home Effective C++ - Item.2
Post
Cancel

Effective C++ - Item.2

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

가급적 전처리기(Preprocessor) 보다 컴파일러를 가까이 하자

  #define의 경우 전처리기에서 이미 치환시켜 값을 대입하기 때문에 컴파일러가 쓰는 기호 테이블에 값이 들어가지 않는다. 따라서 숫자 상수로 대체된 코드에서 컴파일 에러가 발생한다면 이를 디버깅하기가 쉽지 않다. 이 문제는 마찬가지로 기호 테이블에 이름이 들어가 있지 않기 때문에 기호식 디버거(Symbolic Debugger)에서도 나타날 수 있다.

이를 해결하기위해 매크로를 대신하여 상수 변수를 사용한다.

상수 변수는 언어 차원에서 지원하는 상수 타입의 데이터이기 때문에 컴파일러가 인식할수 있으며 기호 테이블에도 들어가게 된다.

또한 만약 상수가 부동소수점 실수 타입일 경우 컴파일을 거친 최종 코드의 크기가 #define보다 작게 나올수 있다. 매크로를 사용하면 정의한 매크로가 등장하는 코드마다 전처리기에 의해 값이 치환되면서 결국 코드 안에 값의 사본이 등장하는 만큼 들어가게 되지만, 상수 타입의 변수는 아무리 여러번 쓰이더라도 사본은 딱 한개만 생기게 된다.

1
2
#define ASPECT_RATIO 1.653        // bad
const double AspectRatio = 1.653  // Good

  #define 을 상수로 교체할 때 두가지를 고려해야 한다.

  • 상수 포인터(Constant Pointer) 정의를 하는 경우
  • 클래스 멤버로 상수를 정의하는 경우, 즉 클래스 상수를 정의하는 경우

  상수 포인터를 정의하는 경우, 상수 정의는 대게 헤더파일에 넣는 것이 일반적이므로 포인터는 꼭 const로 선언해 주어야 하고, 이와 아울러 포인터가 가리키는 대상까지 const로 선언하는 것이 보통이다.

1
2
3
4
5
// 상수 포인터를 정의 할때는 포인터와 가리키는 값 모두 const로 선언한다.
const char* const authorName = "InSeock Suh";

// 문자열 상수를 사용할때는 string 객체를 사용한다.
const std::string authorName("InSeock Suh");

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

1
2
3
4
5
6
7
8
class GamePlayer {
private:
  static const int NumTurns = 5;  // 정적 클래스 상수 선언. '정의' 아님을 주의
  int scores[NumTurns];           // 상수를 사용하는 부분

  ...

};

C++에서는 사용하고자 하는 것에 대한 정의가 마련되는것이 보통이지만, 정적 멤버로 만들어지는 정수류(각종 정수타입, char, bool등) 타입의 클래스 내부 상수는 예외이다. 이들에 대해선 주소를 취하지 않는 한, 정의없이 선언만 해도 아무 문제없다. 그러나 클래스 상수의 주소를 구한다던지, 잘못 구현된 컴파일러가 정의를 달라고 하는 경우에는 별도의 정의를 제공해야 한다.

1
const int GamePlayer::NumTurns;   // 정적 클래스 상수 정의

이때 클래스 상수의 정의는 구현파일에 둔다. 헤더 파일에 두지 않는다.

정의에는 상수의 초기값이 오면 안 되는데, 그 이유는 클래스 상수의 초기값은 해당 상수가 선언된 시점에서 바로 주어지기 때문이다. 즉, 위의 예에서 NumTurns 변수는 선언될 당시에 바로 초기화 된다.

이때 주의해야 할 점은, #define은 유효범위의 개념이 존재 하지 않기 때문에 일단 정의되면 컴파일이 끝날 때 까지(중간에 #undef 되지 않으면) 유효하게 된다. 즉, #define은 클래스 상수를 정의하는데 쓸수 없을 뿐만 아니라 어떤 형태의 캡슐화 혜택도 받을 수 없다.(‘private’ 성격의 #define은 존재하지 않는다)

조금 오래된 컴파일러는 위의 문법을 받아들이지 않는 경우가 있다. 이는 컴파일러에서 정적 클래스 멤버가 선언된 시점에 초기값을 주는 것이 대개 맞지 않다고 판단하기 때문이다. 이와 같은 경우에는 아래와 같이 한다.

1
2
3
4
5
6
7
8
9
10
class CostEstimate {
private:
  static const double FudgeFactor;  // 정적 클래스 상수 선언
                                    // 이것은 헤더파일에 위치
  ...

};

const double CostEstimate::FudgeFactor = 1.35;  // 정적 클래스 상수 정의
                                                // 이것은 구현 파일에 위치

  웬만한 경우에는 위의 예제만으로 충분하다. 딱 한가지 예외가 있다면 해당 클래스를 컴파일 하는 도중에 클래스 상수의 값이 필요할 때 이다. 이를테면 GamePlayer::scores 등의 배열 멤버를 선언할 때 이다. 즉 컴파일러는 컴파일 과정에서 이 배멸의 크기를 알아야 선언 가능하므로 컴파일 타임에 상수의 값을 요구할 것이다. 이런경우에 대한 대안으로는 Enum Hack 이라는 기법을 사용할 수 있다.

Enum Hackenum 타입의 값은 int 가 놓일 곳에도 쓸수 있는 C++의 문법을 활용하여 상수처럼 사용하는 것이다.

1
2
3
4
5
6
7
class GamePlayer {
private:
  enum { NumTurns = 5 };  // 'Enum Hack'
                          // NumTurn를 5에 대한 기호식 이름으로 표현

  int scores[NumTurns];
};

Enum Hack의 동작 방식은 const 보다 #define에 더 가깝게 동작하며, 이를 이용한 Enum Hack 기법의 특징은 아래와 같다.

  • Enum Hack 기법의 상수에 대해서 참조나 주소를 얻을수 없다.
    • const 변수의 주소를 잡아내는 것은 합당하지만, enum의 주소를 취하는 것은 불법이며 #define의 주소를 얻는 것 역시 마찬가지 이다.
  • enum 타입은 #define 처럼 어떤 형태의 쓸데없는 메모리 할당을 하지 않는다.
  • 이미 Enum Hack기법은 상당히 많은 코드에서 쓰이고 있으며 템플릿 메타프로그래밍의 핵심 기법이다.

  #define의 또다른 오용 사례는 매크로 함수 이다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 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가 두번 증가한다.
                              // ++a > b ? ++a : b;

CALL_WITH_MAX(++a, b + 10);   // a가 한번 증가한다.
                              // ++a > b+10 ? ++a : b+10;

                              // f가 호출되기 전에 a가 증가하는 횟수가 달라진다.

위와같은 문제를 피하기 위해, 기존 매크로의 효율을 그대로 유지하면서 정규 함수의 모든 동작방식 및 타입 안정성까지 완벽히 취할 수 있는 방법으로는 inline 함수에 대한 템플릿을 준비하는 것이다.

1
2
3
4
template<typename T>
inline void callWithMax(const T& a, const T& b){
  f(a > b ? a : b);   // T가 정확이 무엇인지 모르기 때문에,
}                     // 상수 객체에 대한 참조자를 사용한다.(Item. 20 참조)

이 함수는 템플릿이기 때문에 동일 계열 함수군(family of function : 하나의 템플릿을 통해 만들어 질것으로 예측 가능한 모든 함수들의 집합 ) 을 만들어 낸다. 또한 callWithMax는 진짜 함수이기 때문에 유효범위 및 접근 규칙을 그대로 따라가며 인자를 여러번 평가할지도 모른다는 걱정도 없어지게 된다.


  const, enum, inline 의 존재를 늘 유념해 두면, 전처리기(특히 #define)를 꼭 써야 하는 경우가 많이 줄어들게 된다. 그렇다고 현실적으로 완전히 뿌리뽑기는 힘들다. 예를 들어 #include는 부동의 필수 요소로 남아 있고, #iddef, #ifndef도 컴파일 조정 기능으로 현장에서 자주 쓰이고 있다.

요점

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



참고. Effective C++ 3/E - Scott Meyers


This post is licensed under CC BY 4.0 by the author.