서적 정리/Effective C++

2.#define을 쓰려거든 const, enum, inline을 떠올리자

민돌이2 2021. 12. 10. 23:48

제목은 선행처리자보다 컴파일러를 더 가까이 하자라는 의미이다.

#define ASPECT_RATIO 1.653

#define은 개발자에겐 가호식 이름(symbolic name)으로 보이지만, 컴파일러에겐 선행 처리자가 숫자 상수로 바꿔 버리기 때문에 컴파일러의 (*주 1)기호 테이블에 들어가지 않는다. 그래서 컴파일 에러발생시 헷갈릴 수 있다. 소스 코드엔 A

SPECT_RATIO라고 있는데 에러 메세지엔 1.653이라 써있다. 또한 협업시 1.653이 어디에 있는지 찾느랴 시간허비할 가능성도 있다.

 

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

const double AspectRatio = 1.653;

#define과 다르게 컴파일러의 눈에도 보이며 컴파일의 최종 코드의 크기가 작아질 수 있다. 예를 들어 ASPECT_RATIO가 등장하면 선행 처리자에 의해 1.653로 바뀌면서 1.653의 사본이 등장 횟수만큼 늘어나지만, 상수 타입의 AspectRatio는 아무리 여러 번 쓰이더라도 사본은 딱 한개만 생성된다.

 

#define을 상수로 교체시 조심해햐할 경우가 있다.

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

상수 정의는 대개 헤더 파일에 넣는 것이 대부분인데 포인터는 꼭 const로 선언해 주어야 하고, 포인터가 가르키는 대상까지 const로 선언하는 것이 보통이다.

const char* const authorName = "Scott Meyers";

 

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

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

class GamePlayer
{
private:
	static const int NumTurns = 5; //상수 선언
	int score[NumTurns]; //상수를 사용하는 부분
}

NumTurns는 선언(declaration)된 것이지 정의(definition)가 아니다(*주 2). C++에서는 사용하고자 하는 것에 대해 정의가 마련되어 있어야 하는데, 정적 멤버로 만들어지는 정수류(char, bool 등) 타입의 클래스 내부 상수는 예외이다. 이들의 주소를 취하지 않는 한, 정의 없이 선언만 해도 아무 문제 없다. 클래스 상수의 주소를 구한다든지, 컴파일러가 잘못된 관계로 정의가 필요한 경우 별도의 정의를 제공해야한다. 상수의 정의는 구현 파일에 둔다.

const int GamePlayer::NumTurns; //NumTurns의 정의

클래스 상수의 정의는 구현 파일에 두고 헤더 파일에는 두지 않는다. 정의에는 상수의 초기값이 있으면 안되는데, 클래스 상수의 초기값은 해당 상수가 선언된 시점에서 바로 주어지기 때문이다.

컴파일러가 오래된 경우 위의 문법을 받아들이지 않는 경우가 종종 있다. 정적 클래스 멤버가 선언된 시점에서 초기값을 주는 것이 맞지 않다고 판단하기 때문이다. 문법이 막힐때는 반대로 초기값을 정의 시점에 주면 된다.

class GamePlayer
{
private:
	static const int NumTurns; //상수 선언
	int score[NumTurns]; //상수를 사용하는 부분
};
const int GamePlayer::NumTurns = 5; //NumTurns의 정의

 

웬만한 경우 위 방법으로 해결이 가능한데, 해당 클래스를 컴파일 하는 도중 클래스 상수의 값이 필요할 때는 예외이다.

대표적으로 배열의 크기를 멤버 변수로 선언할 때이다. 위 방법은 컴파일 도중에는 NumTurns의 값을 몰라 score의 배열크기를 알 수가 없다. 나열자 둔갑술(enum hack)로 해결할 수 있다.

나열자(enumerator) 타입의 값은 int가 놓일 곳에도 쓸 수 있다는 점을 활용한 것이다(enum의 값은 int).

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

동작 방식이 const보다 #define에 가깝다.

const의 주소를 사용하는 것은 합당하지만, enum의 주소를 사용하는 것은 불가능하여, #define역시 불가능하다.

개발자가 선언한 정수 상수를 갖고 다른 사람이 주소를 사용하는 것이 싫다면 enum은 아주 좋은 방법이 될수 있다(18장 참고). 또한 템플릿 메타프로그래밍의 핵심 기법(48장 참고)이니 쉽게 알아볼 수 있도록 하여야 한다.

 

#define의 문제점

#define 지시자의 오용 사례가 많다(매크로 함수).

#define CALL_WITH_MAX(a, b) f((a) > (b) ? (a) : (b)) //a와 b중 큰 것을 f에 넘겨 호출

void f(int a)
{
	cout << "결과 : " << a << endl;
}

int main()
{
	int a = 5, b = 0;

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

f함수가 호출 되기전 a가 증가하는 횟수가 달라진다.

달라지는 이유에 대해 내 생각은 CALL_WITH_MAX매크로 함수에서 ++a 증감연산을 한 번 하고, 증감 연산 한  a와 b를 비교했을 때  a가 더 크다면 f(++a)로 작동하기에 두 번 증가하는 것이 원인이라 생각한다.

매크로의 효율을 그대로 유지하면서 정규 함수의 동장방식 및 데이터타입 안정성까지 완벽히 해결할 수 있는 방법으로 inline함수에 대한 템플릿(30장 참고)을 사용하는 것이다.

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

 

#define으로 상수를 만들면 문제가 있다.

#define은 유효범위가 뭔지 모르고 매크로는 정의되면 컴파일이 끝날 때까지 유효하기 때문이다(#undef 예외).

어떠한 캡슐화 혜택도 받을 수 없고 상수를 정의하는데 쓸 수도 없으므로, private성격의 #define은 존재하지 않다.

 

이것만은 잊지 말자

단순한 상수를 쓸 떄는, #define보다 const 객체 혹은 enum을 우선 생각하자.

함수처럼 쓰이는 매크로를 만들려면, #define 매크로보다 inline 함수를 우선 생각하자.

 

 

(*주 1) 기호 테이블 : 컴파일러나 어셈블러 같은 언어 번역기가 기계어 프로그램으로 번역하는 과정에서 프로그램에 사용되는 모든 기호의 이름과 정보를 관리하는 자료 구조. 프로그램에서 기호에 대한 참조가 발생한 부분을 해결하는 용도로 사용

 

(*주 2) 선언(declaration)과 정의(definition)의 차이 :

선언 : 컴파일러에게 대상에 대한 정볼르 알린다. 함수가 어떤 인수들을 전달 받으며 어떤 타입을 반환하는지 알려주고, 정보만 제공하는 것이므로 본체를 가지지 않고 실제 코드를 생성하지도 않는다. 또한 여러 번 중복되어도 상관없다.

 

정의 : 대상에 대한 정보로부터 대상을 만든다. 정의로부터 본첼르 컴파일하여 코드를 생성한다. 함수의 인수 목록을 컴파일러에게 알려 주기도 하므로 선언을 겸한다. 그래서 함수를 호출부보다 더 앞쪽에 정의하면 컴파일러가 이 함수의 본첼르 만들면서 모든 정보를 파악할 수 있으므로 원형 선언을 따로 하지 않아도 된다.

  역활 메모리 정보의 완전성 중복 가능성
선언 알린다. 사용 안함 불완전해도 됨 가능
정의 생성한다. 할당 항상 완전해야 함 불가능

 

728x90