이 글은 『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 함수
로 대체하라
감사합니다.
'프로그래밍 > C\C++' 카테고리의 다른 글
[Effective C++ 정리 #5] 컴파일러가 몰래 생성하는 특별 함수들 (6) | 2025.06.15 |
---|---|
[Effective C++ 정리 #4] 예측 불가능한 버그를 막는 습관, 객체 초기화는 왜 필수인가? (0) | 2025.06.15 |
[Effective C++ 정리 #3] const — 타입 안정성과 효율성의 시작점 (4) | 2025.06.15 |
[Effective C++ 정리 #1] C++는 하나의 언어가 아니다 (6) | 2025.06.13 |
[Effective C++ 정리 #0] C++를 진짜 잘 쓰기 위한 전제 조건들 (2) | 2025.06.13 |