본문 바로가기

업무용 언어/C++

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

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

컴파일 타임 상수 대체하기

아래와 같은 define 문이 있을 때,

#define ASPECT_RATIO 1.653

ASPECT_RATIO라는 이름은 심볼은 컴파일러에겐 전혀 보이지 않는다(컴파일러의 심볼 테이블에 들어가지 않음). 선행 처리자가 숫자 상수로 바꾸어 버리기 때문이다.

이 때문에 생길 수 있는 문제는 다음과 같다.

  • 컴파일 에러가 발생하면 ASPECT_RATIO라는 심볼보다는 1.653이라는 상수를 마주하게 될 것이므로, 버그를 찾기가 어려워 진다. (더군다나 ASPECT_RATIO가 정의된 파일이 프로젝트 내에 있지 않을 경우, 찾기가 더욱 곤란해 질 수 있음)
  • 심볼릭 디버거에서도 ASPECT_RATIO라는 심볼 대신에 숫자를 보여주므로 디버깅이 어려워 질 수 있다.

이 문제를 해결할 수 있는 방법은 매크로 대신 상수를 쓰는 것이다.

const double AspectRatio = 1.653; // 대문자로만 표기하는 이름은 보통 매크로에서 쓰는 것이라서, 이름 표기도 바꿔줌

AspectRatio는 컴파일러의 심볼 테이블에 들어가게 된다.
또한 추가로 얻을 수 있는 이점은 컴파일된 코드의 크기가 작아질 수 있다는 것인데, #define을 쓸 경우 사용된 개수만큼 해당 숫자의 사본이 생기게 되는데, 상수의 경우는 사본이 딱 한개만 생기기 때문이다(몇몇 CPU 아키텍쳐에서는 작은 정수 값에 대해서 Instruction Code 내부에 Immediate 타입의 값을 직접 저장할 수 있으므로, 해당이 되지 않을 수 있음).

#define 을 상수로 교체하려는 경우, 두가지 경우만 조심하자.

  1. 상수 포인터를 정의하는 경우: 보통 헤더 파일에 넣는 것이 관례이므로, 포인터는 꼭 const로 선언해 주어야 하고, 포인터가 가리키는 대상까지 const로 선언해 주어야 한다.
  2. const char* const authorName = "Scott Meyers"; // const의 의미와 사용법에 대한 자세한 사항은 항목 3 참조

    문자열 상수에는 char*같은 구닥다리 문자열 보다는 string 객체가 더 사용하기 편하다.

    const std::string authorName("Scott Meyers");
  3. 클래스 상수를 정의하는 경우: 어떤 상수의 유효범위를 클래스로 한정하고자 할 때, 그 상수의 사본 개수가 한개를 넘지 못하게 하고 싶다면 정적(static) 멤버로 만들어야 한다.
  4. class GamePlayer {
    private:
        static const int NumTurns = 5; // 상수 선언(declaration)
        int scores[NumTurns];
        // ...
    };

C++ 에서는 대부분의 것들에서 정의가 마련되어 있어야 하지만, 정적 멤버로 만들어지는 정수류(각종 정수 타입, char, bool 등) 타입의 클래스 내부 상수는 예외이다.
이들에 대해 주소를 취하지 않는 한, 정의 없이 선언만 해도 아무 문제가 없다.

단, 클래스 상수의 주소를 구해야 한다면 얘기가 달라진다.

const int GamePlayer::NumTurns;

이 클래스 상수의 정의는 구현 파일에 두어야 한다. 또한 클래스 상수의 초기값은 해당 상수가 선언된 시점(헤더 파일)에 바로 주어지기 때문에 정의(구현 파일)에는 초기 값을 주지 않는다.

상수의 주소를 구한다거나, 상수의 참조자를 취하는 일을 막으려면 enum을 쓰면 된다.

class GamePlayer {
private:
    enum { NumTurns = 5 };
    int scores[NumTurns];
    // ...
};

const의 주소를 구하는 것은 합당하지만, enum의 주소를 구하는 것은 안되기 때문이다.
enum은 어떠한 형태의 쓸데없는 메모리 할당도 절대 저지르지 않는다.

매크로 함수 대체하기

#define을 잘못 사용하는 경우는 종종 매크로 함수에서 볼 수 있다.

// 매크로 함수의 인자는 항상 괄호로 싸서, 표현식이 변형되는 것을 막아주자.
#define CALL_WITH_MAX(a, b) func((a) > (b) ? (a) : (b))
int a = 5, b = 0;
CALL_WITH_MAX(++a, b); // a가 두 번 증가
CALL_WITH_MAX(++a, b+10); // a가 한 번 증가

이처럼 표현식의 결과에 따라 인자가 평가되는 횟수가 달라진다.

C++ 에서는 기존 매크로의 효율을 그대로 유지하면서 정규 함수의 모든 동작방식 및 타입 안전성까지 완벽하게 취할 수 있는 방법이 있다.
바로 인라인 템플릿 함수(항목 30 참조)를 만드는 것이다.

template<typename T>
inline void callWithMax(const T& a, const T& b) // T가 정확히 어떤 타입인지 모르기 때문에, 상수 객체에 대한 참조자를 씀. (항목 20 참조)
{
    f(a > b ? a : b);
}

함수 본문에 지저분하게 괄호를 넣을 필요도 없고, 인자를 여러 번 평가하지도 않는다.
뿐만 아니라 진짜 함수이기 때문에, 유호범위 및 접근 규칙을 그대로 따라간다.
임의의 클래스 안에서만 쓸 수 있는 인라인 함수가 가능하다는 얘기다.

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