hubring

[EC++] 항목 2 : #define을 쓰려거든 const, enum, inline을 떠올리자. 본문

C++/Effective C++

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

Hubring 2021. 6. 12. 00:46


해당 항목 제목을 가급적 선행 처리자보다 컴파일러를 더 가까이 하자 로 가는게 더 괜찮았을 것 같다고 책에서 말하고 있다.
(....지금 항목 제목이 더 잘 기억에 남을듯 하다.)

#define의 문제


아래 코드를 썼다고 가정하자.

#define ASPECT_RATIO 1.653

코드를 눈으로 볼 때는 ASPECT_RATIO가 기호식 이름으로 보이나 컴파일러에게는 선행 처리자가 해당 기호를 사용한 코드를 숫자 상수로 바꾸어 버리기 때문에 이름이 아닌 숫자로 보게된다.
만약 숫자 상수로 대체된 코드에서 컴파일 에러가 발생하게 되면 기호식 이름으로 알려주는 것이 아닌 숫자로 보이게 되어 에러의 원인을 찾는데 어려울 수 있다.
이 문제는 기호식 디버거에서도 나타날 소지가 있다.
=> 추가 예로 만약 #define TEST 100 같이 선언 하고 어디선가 TB_TEST 와 같은 변수 코드를 사용했다 가정하면.. 의도치 않게 TB_100 이라는 변수 이름으로 변경되는 걸 볼 수 있을 것이다... 생각보다 흔하게 발생할 수 있는 문제이다.



해결법


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

const double AspectRatio = 1.653;

상수 타입의 데이터이기 때문에 컴파일러에게도 기호로 보이며 기호 테이블에도 들어가게 된다.
게다가 예제처럼 상수가 부동소수점 실수 타입일 경우 컴파일을 거친 최종 코드의 크기가 #define을 썼을 때보다 작게 나올 수 있다.

해당 이유는 매크로를 쓰면 코드에 ASPECT_RATIO 들어간 위치마다 선행 처리자에 의해 상수로 변경되면서 목적코드에 사본이 등장 횟수만큼 들어가게되고,
반면 상수 타입을 쓰면 아무리 여러번 사용하더라도 사본은 딱 한 개만 생기기 때문이다.



#define을 상수로 교체 시 주의점

1. 상수 포인터(constant pointer)를 정의하는 경우

상수 정의는 대개 헤더 파일에 넣는 것이 상례로 포인터는 꼭 const로 선언해야한다.
이와 아울러 포인터가 가리키는 대상까지 const로 선언해 주어야한다. (상세한건 항목3을 참고)

const char * const authorName = “Scott Meyers”


2. 클래스 멤버로 상수를 정의하는 경우

즉 클래스 상수를 정의하는 경우, 어떤 상수의 유효범위를 클래스로 한정하고자 할 때 그 상수를 멤버로 만들어야 하는데...
그 상수의 사본 개수가 한 개를 넘지 못하게 하고 싶다면 정적(static) 멤버로 만들어야 한다.

class GamePlayer {
private :
static const int Numturns = 5; // 상수 선언
}

Numturns는 정의된 것이 아닌 선언됨.
정적 멤버로 만들어지는 정수류(char, bool 등) 타입의 클래스 내부 상수는 이들에 대해 주소를 취하지 않는 한, 정의 없이 선언만 해도 문제가 없다.
단, 클래스 상수의 주소를 구한다든지, 사용하고 있는 컴파일러가 정의를 달라고 하는 경우 별도의 정의를 제공해야 한다.

아래가 그 예시이다.

const int GamePlayer::NumTurns;  

클래스 상수의 정의는 헤더 파일이 아닌 구현 파일에 둔다.
해당 정의에는 초기값이 있으면 안된다. => 클래스 상수의 초기값은 해당 상수가 선언된 시점에서 바로 주어지기 때문이다.

참고로 #define은 유효범위라는게 없어 클래스 상수로 만드는 것 자체가 불가능하며 어떤 형태의 캡슐화 혜택도 받을 수 없다.

만약 오래된 컴파일러를 사용하여 위 문법이 받아들여지지 않는 경우, 초기값을 상수 정의 시점에 주도록한다.

class CostEstimate {
  private :
    static const double FudgeFactor; // 정적 클래스 상수의 선언 (헤더 파일에 둔다)
    ...
};
const double CostEstimate::FudgeFactor = 1.35; // 정적 클래스 상수의 정의 (구현 파일에 둔다)


웬만한 경우 이것으로 충분하지만 예외가 있다면 해당 클래스를 컴파일하는 도중에 클래스 상수의 값이 필요할 때이다.
예를 들어 GamePlayer::scores 등의 배열 멤버를 선언할 때가 대표적이다.
이 경우 구식 컴파일러를 사용한다면 나열자 둔갑술(enum hack) 기법을 생각하자.

나열자 둔갑술


이 기법의 원리는 나열자(enumerator) 타입의 값은 int가 놓일 곳에서도 쓸 수 있다는 C++ 을 이용하는 것이다.

class GamePlayer {
private :
enum { NumTurns = 5 }; // 나열자 둔갑술 NumTurns를 5에 대한 기호식 이름으로 만듦
Int socres[NumTurns];
}

나열자 둔갑술을 알아 두면 여러가지로 도움이 된다.

1. 나열자 둔갑술 동작 방식이 const 보다는 #define에 더 가깝다.
const에서 주소를 잡아낼 수 있으나 #define에서는 주소를 얻는 것은 맞지 않다. 이는 enum도 동일하다.
따라서 정수 상수를 가지고 다른 사람이 주소를 얻거나 참조자를 쓰도록 원하지 않는 다면 enum은 좋은 자물쇠가 될 수 있다.
또한 #define 처럼 쓸데없는 메모리 할당도 하지 않는다.

2. 상당히 많은 코드에서 이 기법을 사용중이며 템플릿 메타프로그래밍의 핵심 기법이다.
혹시 발견하면 쉽게 알아볼 수 있도록 눈에 익혀두자


#define의 또 다른 문제 매크로 함수


아래 예는 매크로 인자들 중 큰 것을 사용해서 어떤 함수 f() 를 호출하는 매크로이다.

#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가 한 번 증가함.

비교에 따라 처리한 결과가 어떤 것이냐에 따라 a의 증가값이 달라져 버린다.


해결법


인라인 함수에 대한 템플릿을 준비하는 것이다. (항목 30 참조)

template<typename T>
Inline void callWithMax(const T& a, const T& b){ // T가 정확히 무엇인지 모르기 때문에 매개변수로 상수 객체 참조자를 사용
f( a > b ? a : b );
}

이 함수는 템플릿이기 때문에 동일 계열 함수군(family of function)을 만들어낸다.
동일한 타입의 객체 두개를 인자로 받고 둘 중 큰 것을 f()에 넘겨서 호출하는 구조이다.

이 방법은 위 #define 매크로 함수 문제를 해결해준다.
그뿐 아니라 진짜 함수이기 떄문에 유효범위 및 접근 규칙을 그대로 따라갈 수 있다.

정리

const, enum, inline 유념하며 선행 처리자(특히 #define) 사용을 줄이자.
현실적으로 아직은 #include, #ifdef/#ifndef 를 현장에서 많이 사용하고 있어 완전히 없애는 건 불가능하나 기회가 될 때마다 없애도록 하자.


이것만은 잊지 말자!

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