ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [Effective C++] 02. 선행 처리자보다 컴파일러를 더 가까이 하자
    C++/Effective C++ 2025. 11. 5. 17:31

    원제목 : Prefer consts, enums, and inlines to #defines (#define을 쓰려거든 const, enum, inlne을 떠올리자)

     

    선행 처리자는 #include ,#define, #ifdef/#ifndef 등의 선언문을 의미합니다.

    이번 글에서 살펴볼 선행 처리자는 이 중 #define 입니다.

    #define으로 선언된 코드는 컴파일 시 전처리 과정에서 코드를 지정한 값으로 교체합니다.

    따라서 디버깅 과정에서 해당 심볼은 발견할 수 없으며, 심볼의 사용 횟수에 비례하여 목적 코드의 크기 또한 증가합니다.

    이번 글에서는 #define을 대체할 수 있는 기법들을 살펴보도록 하겠습니다.

     


     

    1. 상수로 사용되는 #define

     

    더보기

    #define을 상수를 정의하는 데 사용할 경우 다음과 같이 사용할 수 있으며

    이는 const로 교체할 수 있습니다.

    // Preprocess
    #define PI 3.141592
    // const value
    const double Pi = 3.141592;
    
    double foo = 2 * PI;
    double bar = 3 * PI;

    이렇게 const를 이용한 상수를 사용하는 것 만으로도 서론에서 언급한 심볼이 가려지는 것, 목적 코드가 커지는 것을 방지할 수 있습니다.

     

    하지만 상수를 사용할 때 주의할 점이 몇 가지 있습니다.

    첫 번째는 문자열 리터럴 입니다.

    #define MY_STR "Hello world!"
    
    // 1. const const
    const char* const myString = "Hello world!";
    // 2. const string
    const string myString("Hello world!");

    문자열 리터럴을 char*로 사용할 경우, const를 두 번 사용해야 합니다. 

    혹은, STL의 string 컨테이너를 사용할 수 있습니다.

     

    두 번째는 클래스 멤버 상수입니다.

    이 경우 상수이기 때문에 static 멤버로 사용하는 것이 좋습니다.

    clss Widget{
    private:
      static const double Pi;
      ...
    };
    
    const double Widget::Pi = 3.141592;

    실수 타입은 선언 시 값을 초기화 할 수 없습니다. (일부 구버전 컴파일러는 정수 또한 불가합니다.)

    따라서 클래스 정의 밖에서 값을 정의해야 합니다. 

     

    2. 상수로 사용되는 #define : C++11 이후

     

    더보기

    이전 문단에서 상수에 대해 #define 대신 const를 사용하는 것, 그리고 주의점을 살펴보았습니다.

     

    우선, #define을 const로 교체하면서 얻을 수 있는 이점은 디버깅에 용이하다는 것, 추가적으로 타입 안정성을 확보할 수 있다는 것 입니다.

    하지만 클래스 정적 멤버로 사용할 수 없는 등, #define의 컴파일 시점의 상수 역할은 수행할 수 없는 경우도 있습니다.

     

    이는 C++11의 constexpr로 해결할 수 있습니다.

    constexpr은 컴파일 시점에 평가가 이루어지는 값에 사용되는 예약어 입니다.

    clss Widget{
    private:
      static constexpr double Pi = 3.141592;
      ...
    };

     

    이를 사용하면 클래스 정적 멤버를 선언할 때 값을 지정할 수 있습니다.

     

    3. 함수로 사용되는 #define

     

    더보기

    #define을 함수로 사용할 경우 다음과 같이 사용할 수 있으며

    이는 inline으로 교체할 수 있습니다.

    // Preprocess
    #define CALL_WITH_MAX(a, b) f((a) > (b) ? (a) : (b))
    
    // const value
    template<typename T>
    inline void callWithMax(const T& a, const T& b){
      f(a > b ? a : b);
    }

    위 매크로와 inline함수는 전달받은 a와 b중 더 큰 값을 사용해 함수 f를 호출합니다.

     

    이러한 함수의 교체는 상수 사용시 얻는 타입 안정성 등의 문제 뿐만 아니라 다음의 문제 또한 해결합니다.

    int a = 5, b = 0;
    
    CALL_WITH_MAX(++a, b);    // a가 두 번 증가
    CALL_WITH_MAX(++a, b+10); // a가 한 번 증가
    
    // 매크로 대입
    f((++a) > (b) ? (++a) : (b));

    매크로가 단순히 코드를 교체해주기 때문에, 위와 같이 표현식이 중복 평가되는 일이 발생할 수 있습니다.

    함수를 이용할 경우 표현식이 한 번만 평가되어 이러한 문제를 해결할 수 있습니다.

     

    4. 함수로 사용하는 #define : C++11 이후

     

    더보기

    기존 코드의 inline함수는 상수의 const와 마찬가지로 컴파일 타임에 평가가 불가능하다는 단점이 있습니다.

    // 가능
    #define SQUARE(x) ((x) * (x))
    int arr[SQUARE(5)];
    
    // 불가능
    inline int square(int x) { return x * x; }
    int arr[square(5)];

    inline이 함수의 본문을 대체하는 예약어이기 때문입니다.

     

    이 또한 C++11 이후 constexpr을 통해 개선할 수 있습니다.

    // 가능
    constexpr int square(int x) { return x * x; }
    int arr[square(5)];

    constexpr로 선언한 함수는 컴파일 시점에 평가가 이루어집니다.

    따라서 #define이 수행하던 역할인 컴파일 시점의 평가가 가능합니다.

     

     


     

    상수를 지정하여 재사용성과 가독성을 늘리는 것은 좋은 코딩 습관입니다.

    하지만 사용하기에 따라 크고 작은 문제가 발생할 수 있기 때문에, 이를 안전하게 대체할 수 있는 방법을 알아두는 것이 좋겠습니다.

     

    이번 글이 도움이 되셨기를 바랍니다.

    감사합니다.

    댓글

Designed by Tistory.