본문 바로가기
프로그래밍/C\C++

[Effective C++ 정리 #2] #define 대신 const, enum, inline으로 대체하는 이유

by 허구의 2025. 6. 14.
728x90

이 글은 『Effective C++』를 읽고 개인적으로 공부한 내용을 정리한 기록입니다.

저는 컴퓨터공학을 전공하지 않았으며, 프로그래밍을 공부하는 과정에서의 이해와 생각을 정리하기 위해 글을 작성하고 있습니다.

따라서 내용 중 일부에 오류나 부정확한 설명이 있을 수 있으며, 피드백은 언제든지 환영합니다. 확인 후 수정하도록 하겠습니다.

전문적인 해설이 아닌 개인적 시선에서의 정리임을 참고하고 읽어주시면 감사하겠습니다.

Item 2: #define 대신 const, enum, inline을 사용하라

C++에서 #define은 오랜 역사를 가진 기능이지만, 현대적인 C++ 코드에서는 대부분 const, enum, inline으로 대체하는 것이 권장됩니다. 이 Item에서는 #define의 문제점과, 그것을 대체할 수 있는 구체적인 방법들을 소개합니다.


#define의 주요 문제점

#define은 전처리기(preprocessor)가 처리하기 때문에, 컴파일러는 그 정의를 인식하지 못합니다. 즉, 다음과 같은 매크로를 정의했다면:

#define ASPECT_RATIO 1.653

컴파일러는 ASPECT_RATIO라는 이름 자체를 모르고, 그냥 1.653으로 대체된 상태에서 컴파일됩니다. 그래서 컴파일 에러나 디버깅 과정에서 매크로 이름이 아닌 숫자 상수만 보이게 되어 문제 추적이 어려워질 수 있습니다.

해결 방법은 간단합니다. const를 사용하는 것이죠:

const double AspectRatio = 1.653; // 매크로가 아니므로 심볼 테이블에 등록됨

AspectRatio는 이제 컴파일러가 인식 가능한 이름이 되며, 디버거에서도 정상적으로 나타납니다. 게다가 #define은 값이 여러 번 삽입될 수 있지만, const는 메모리에 단 한 번만 존재할 수 있어 오히려 더 최적화될 수 있습니다.

게다가 #define은 클래스 내부에서 스코프(scope), 접근 제어(public/private)를 따르지 않기 때문에, 객체 지향 설계와는 전혀 어울리지 않습니다. 예를 들어 클래스 내부에서만 유효한 상수를 만들거나, 접근을 제한하고 싶은 경우 #define은 이를 지원할 수 없습니다.


포인터 상수 선언 시 주의할 점

헤더 파일에 const char* 문자열을 정의할 때는, 포인터 자체와 포인터가 가리키는 내용을 모두 상수로 지정해야 합니다.

const char * const authorName = "Scott Meyers";

그러나 일반적으로는 C 스타일 문자열보다는 std::string을 사용하는 것이 바람직합니다:

const std::string authorName("Scott Meyers");

클래스 내 상수 정의: static const와 enum hack

클래스 내부에 상수를 정의하고 싶다면, static const로 선언하고, 초기값도 함께 제공할 수 있습니다:

class GamePlayer {
private:
    static const int NumTurns = 5; // 선언 및 초기화
    int scores[NumTurns];          // 상수를 배열 크기로 사용
};

이 선언은 정의(definition)가 아닌 선언(declaration)입니다. 일반적으로는 C++에서 어떤 값을 사용하려면 정의도 제공해야 하지만, static이면서 정수형(integral type)인 클래스 상수의 경우에는 예외적으로 정의 없이도 사용할 수 있습니다.

그 이유는 컴파일 타임에 값이 확정되므로, 예를 들어 배열 크기 같은 곳에 사용할 때 별도로 메모리를 할당하지 않고도 값을 참조할 수 있기 때문입니다.

다만 컴파일러가 정의를 요구하거나, 주소를 참조할 경우 별도로 정의를 제공해야 합니다:

const int GamePlayer::NumTurns; // 정의 — 초기값은 생략 (이미 선언 시 제공됨)

또는 아래처럼 아예 enum을 활용하는 방식도 있습니다:

과거 C++98에서는 클래스 내에서 static const int 멤버를 정의하고 배열 크기에 쓰는 것이 일부 컴파일러에서 오류를 냈습니다. 이를 회피하기 위해 등장한 기법이 바로 “enum hack”입니다.

enum { NumTurns = 5 };처럼 정의하면 타입도 없고 주소도 참조할 수 없는 완전한 정수 상수가 됩니다. 단순히 컴파일 타임 상수 숫자만 필요할 때 가장 안전하고 단순한 방식으로, 지금도 종종 사용되는 고전적인 패턴입니다.

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

다만, enum hack은 타입이 없고 참조도 불가능하기 때문에 정말로 값 하나만 필요한 상황에서만 사용하는 게 좋습니다. 요즘은 대부분의 컴파일러가 static const int를 지원하므로 이 방식이 더 선호됩니다.


static const double 등 정수 외 타입의 클래스 상수 정의

double 등 정수 이외의 타입은 위 enum 방식이 불가능하며, 다음처럼 헤더에 선언하고 소스 파일에 정의해야 합니다:

class CostEstimate {
private:
    static const double FudgeFactor; // 선언 (헤더 파일)
};

const double CostEstimate::FudgeFactor = 1.35; // 정의 (소스 파일)

이러한 방식은 값이 클래스 구현 중 컴파일 시점에 필요한 경우에는 사용할 수 없습니다.


매크로 함수 대신 inline template 사용

다음은 흔히 보이는 매크로 함수 형태입니다:

#define CALL_WITH_MAX(a, b) f((a) > (b) ? (a) : (b))

하지만 이 방식은 심각한 문제점을 안고 있습니다:

int a = 5, b = 0;

CALL_WITH_MAX(++a, b);      // a가 두 번 증가됨
CALL_WITH_MAX(++a, b + 10); // a가 한 번만 증가됨

이는 매크로가 인자를 여러 번 평가하기 때문입니다. 대신 다음처럼 inline template을 사용하면 모든 문제가 해결됩니다:

참고로 여기서 inline은 함수를 호출하지 말고 호출 위치에 함수 본문을 직접 삽임하도록 컴파일러에게 알려줍니다. 최적화의 장점도 있지만, 중복 정의를 허용하여 헤더 파일에서의 함수 정의 시 유용합니다.

template<typename T>
inline void callWithMax(const T& a, const T& b)
{
    f(a > b ? a : b);
}

이 방식은 다음과 같은 장점이 있습니다:

  • 매개변수가 단 한 번만 평가됨
  • 타입 안정성(type safety) 확보
  • 스코프 제한, 접근 제한(private 등) 적용 가능

추가로, 템플릿 함수는 명시적으로 inline을 쓰지 않아도 암묵적으로 inline처럼 처리된다고 하는 것 같습니다.


핵심 요약

  • 단순 상수에는 const 또는 enum을 사용하라
  • 매크로 함수inline 함수로 대체하라

감사합니다.

728x90