항목 2 : #define을 쓰려거든 const, enum, inline을 떠올리자
#define ASPECT_RATIO 1.653 이라는 코드를 작성했을때
작성자에게는 ASPECT_RATIO가 기호식 이름으로 보이지만 컴파일러에게는 보이지가 않습니다.
선행 처리자가 숫자 상수로 바꾸어 버리기 때문입니다.
그 결과로, ASPECT_RATIO라는 이름은 컴파일러가 쓰는 기호 테이블에 들어가지 않습니다.
만약 숫자 상수로 대체된 코드에서 컴파일 에러라도 발생하면 헷갈릴 수 있는 여지가 존재합니다.
이 문제의 해결법은 매크로 대신 상수를 쓰는 것입니다.
const double AspectRatio = 1.653;
AspectRatio는 언어 차원에서 지원하는 상수 타입의 데이터이기 때문에 컴파일러의 눈에도 보이며 기호 테이블에도 들어갑니다.
또한 매크로 사용시 코드에 ASPECT_RATIO가 등장하면 선행 처리자에 의해 1.653으로 모두 바뀌면서 목적 코드 안에 1.653의 사본이 등장 횟수만큼 들어가게 되지만, AspectRatio는 여러 번 쓰이더라도 사본은 딱 한 개만 생기게 됩니다.
#define을 상수로 교체할 때 주의해야 할 두 가지 경우가 존재합니다.
1. 상수 포인터를 정의하는 경우
2. 클래스 멤버로 상수를 정의하는 경우
첫 번째
상수 포인터를 정의하는 경우에 포인터는 꼭 const로 선언해 주어야 하고, 이와 아울러 포인터가 가리키는 대상까지 const로 사용하는게 보통입니다.
예를 들면 어떤 헤더 파일 안에 char* 기반의 문자열 상수를 정의한다면 다음과 같이 const를 두 번 써야 한다는 말입니다.
1
|
const char* const authorName = "Scott Meyers";
|
tip ) 문자열 상수를 쓸 때는 char* 기반의 문자열보다는 string 객체가 대체적으로 사용하기가 더 좋습니다.
const std::string authorName("Scott Meyers");
두 번째
클래스 멤버로 상수를 정의하는 경우, 즉 클래스 상수를 정의하는 경우
어떤 상수의 유효범위를 클래스로 한정하고자 할 때는 그 상수를 멤버로 만들어야 하는데, 그 상수의 사본 개수가 한 개를 넘지 못하게 하고 싶다면 정적 멤버로 만들어야 합니다.
1
2
3
4
5
6
7
8
9
10
11
|
class GamePlayer {
private :
static const int NumTurns = 5; // 상수 선언
int scores[NumTurns]; // 상수를 사용하는 부분
...
};
Colored by Color Scripter
|
위에 보이는 NumTurns는 '선언'된 것입니다. '정의'가 아니니 주의하세요.
C++에서는 사용하고자 하는 것에 대해 '정의'가 마련되어 있어야 하는 게 보통이지만, 정적 멤버로 만들어지는 정수류(각종 정수 타입, char, bool 등) 타입의 클래스 내부 상수는 예외입니다.
클래스 상수에 대해 주의해야 할 것이 있습니다.
클래스 상수를 #define으로 만드실 생각을 하면 안됩니다. 애초에 #define은 클래스 상수를 정의하는 데 쓸 수도 없을 뿐 아니라 어떤 형태의 캡슐화 혜택도 받을 수 없습니다.
이와 대조적으로 상수 데이터 멤버는 캡슐화가 됩니다.
조금 오래된 컴파일러는 위의 문법을 받아들이지 않는 경우가 종종 있습니다.
이유는 간단합니다. 정적 클래스 멤버가 선언된 시점에 초기값을 주는 것이 대개 맞지 않다고 판단하기 때문입니다.
클래스 내부 초기화를 허용하는 경우가 정수 타입의 상수에 대해서만 국한되어 있으니 말이죠
위의 문법이 먹히지 않는 컴파일러를 쓸 때는, 초기값을 상수 '정의' 시점에 주도록 하십시오
1
2
3
4
5
6
7
8
9
10
11
|
class CostEstimate{
private:
static const double FudgeFactor; // 정적 클래스 상수의 선언 이것은 헤더 파일에 둡니다.
...
};
const double CostEstimate::FudgeFactor = 1.35; // 정적 클래스 상수의 정의 이것은 구현 파일에 둡니다.
|
웬만한 경우엔 이것으로 충분하지만 한 가지 예외로써 해당 클래스를 컴파일하는 도중에 클래스 상수의 값이 필요할 때가 있습니다.
이럴 때 사용할 수 있는 방법으로 '나열자 둔갑술'이 있습니다.
1
2
3
4
5
6
7
8
9
10
|
class GamePlayer {
private :
enum { NumTurns = 5 }; // "나열자 둔갑술" : NumTurns를 5에 대한 기호식 이름으로 만듭니다.
int scores[NumTurns]; // 깔끔하게 해결!
...
};
Colored by Color Scripter
|
나열자 둔갑술은 알아 두는 것은 도움이 됩니다.
1. 나열자 둔갑술은 동작 방식이 const보다는 #define에 더 가깝습니다, 하지만 enum은 #define처럼 어떤 형태의 쓸데없는 메모리 할당도 절대 저지르지 않습니다.
2. 나열자 둔갑술은 템플릿 메타프로그래밍의 핵심 기법입니다.
이번에는 #define에 대한 오용 사례를 보여드리겠습니다.
1
2
3
4
5
6
7
|
#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가 한 번 증가합니다.
|
f가 호출되기 전에 a가 증가하는 횟수가 달라집니다. 비교를 통해 처리한 결과가 어떤 것이냐에 따라 달라지니 큰 문제입니다.
기존 매크로의 효율을 그대로 유지함은 물론 정규 함수의 모든 동작방식 및 타입 안전성까지 완벽히 취할 수 있는 방법이 있습니다.
바로, 인라인 함수에 대한 템플릿을 준비하는 것입니다.
1
2
3
4
5
6
|
template<typename T>
inline void callwithMax(const T& a, const T& b) // T가 정확히 무엇인지 모르기 때문에, 매개변수로 상수 객체에 대한 참조자를 씁니다.
{
f( a > b ? a : b );
}
Colored by Color Scripter
|
이 함수는 템플릿이기 때문에 동일 계열 함수군을 만들어냅니다.
동일한 타입의 객체 두 개를 인자로 받고 둘 중 큰 것을 f에 넘겨서 호출하는 구조입니다.
꼭 잊지 말아야 할 것!
1. 단순한 상수를 쓸 때는, #define보다 const 객체 혹은 enum을 우선 생각하자
2. 함수처럼 쓰이는 매크로를 만들려면, #define 매크로보다 인라인 함수를 우선 생각하자
'언어 > C++' 카테고리의 다른 글
[Effective C++] 컴파일러가 만들어낸 함수가 필요 없으면 확실히 이들의 사용을 금해 버리자 (0) | 2020.01.06 |
---|---|
[Effective C++] C++가 은근슬쩍 만들어 호출해 버리는 함수들에 촉각을 세우자 (0) | 2020.01.05 |
[Effective C++] 객체를 사용하기 전에 반드시 그 객체를 초기화하자 (0) | 2020.01.04 |
[Effective C++] 낌새만 보이면 const를 들이대 보자! (0) | 2020.01.03 |
[Effective C++] C++를 언어들의 연합체로 바라보는 안목은 필수 (0) | 2020.01.01 |