Effective C++ 공부 내용입니다.



상수

첫 번째 오용 사례

#define ASPECT_RATIO 1.653

  • 나에게는 ASPECT_RATIO 라는 것이 symbolic name으로 보이지만 컴파일러에겐 그렇지 않다.
    • 소스 코드가 컴파일 되기 전에 선행 처리자가 ASPECT_RATIO를 상수로 바꾸기 때문이다.
  • 그리하여 ASPECT_RATIO라는 symbol이 컴파일러의 Symbol Table에 들어가지 않는다.


  • 그래서 오류가 발생한다면 소스 코드에는 ASPECT_RATIO를 사용했지만 오류 코드에는 1.653이 있어서 당황할 수 있다.
  • 내가 만든 것이 아니라면 해당 숫자가 어디에서 왔는지도 모르고 시간을 허비할 것이다.


해결법은 메크로 대신 그냥 상수를 사용하는 것이다. const double AspectRatio = 1.653;


  • const는 언어 차원에서 지원하는 상수 타입의 데이터이기에 컴퍼일러가 인지하며 Symbol Table에도 들어간다.

  • 그리고 #define을 썼을 때 보다 최종 크기가 작아질 수 있다.

    • 왜냐하면 #defineASPECT_RATIO가 나오는 부분마다 1.653으로 바뀌면서 1.653이 나온 횟수만큼 사본이 만들어지지만, 상수 타입은 아무리 여러 번 쓰더라도 사본이 딱 한 개 생긴다.


#define을 상수로 교체하는 경우 두 가지 조심할 점

1. 상수 포인터를 정의하는 경우

  • 포인터가 가르키는 대상까지 const로 선언해야 한다.
    • const char* const 처럼


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

  • 어떤 상수의 유효 범위를 클래스로 한정하고자 한다면 멤버 변수로 만들어야 하며, 본의 개수가 한 개를 넘지 못하게 하려면 정적으로 만들어야 한다.
class GamePlayer{
private:
	static const int Num = 5;
	int scores[Num];
	...
};
  • c++에서는 사용하고자 하는 것에 대해 정의가 마련되어 있어야 하는 게 보통이지만, 정적 멤버로 만들어지는 정수류 타입의 클래스 내부 상수는 예외이다.
    • 이들은 주소를 취하지 않는 한, 정의 없이 선언만 해도 아무 문제가 없게 설계되어 있다.


  • 다만, 클래스 상수의 주소를 구하던가, 컴파일러가 잘못 만들어진 관계로 정의를 달라고 하는 경우 정의를 만들어야 한다.
    • const int GamePlayer::Num;
  • 이 때 보편적으로 상수 정의는 헤더 파일에 넣지만, 클래스 상수의 정의는 구현 파일에 둔다.
    • 정의에는 상수의 초기값이 있으면 안 된다.
    • 왜냐면 상수 선언시 초기화를 바로 하기 때문이다.


  • 하지만 엤날 컴파일러 같은 경우 위 같은 문법이 받아들이지 않는 경우가 있다.
    • 이유는 정적 클래스 멤버가 선언된 시점에 초기값을 주는 것이 말이 안 된다고 판단하기 때문이다.
    • 이 경우에는 간단하게 위에서 말한 문법과 다르게 초기값을 정의 시점에 설정하면 된다.


  • 하지만 이 방법은 클래스를 컴파일 하는 도중 클래스 상수의 값이 필요할 때 문제가 생긴다.
    • enum hack라는 기법으로 해결할 수 있다.


enum hack

  • 해당 기법은 TMP에서도 많이 쓰이기에 눈에 익혀 나쁠 것이 없다.

  • 원리는 enum타입의 값은 int가 놓일 곳에도 쓸 수 있기 때문이다.

class GamePlayer{
private:
	enum { Num = 5 };
	int scores[Num];
	...
};
  • enum은 주소를 취할 수 없으니, 선언한 정수 상수를 다른 사람이 주소를 얻거나 참조하는 것이이 싫다면 좋은 자물쇠가 된다.
  • enum#define처럼 어떤 쓸데없이 메모리 할당을 하지 않는다.


메크로 함수수

두 번째 오용 사례

  • 두 번째 오용 사례는 메크로 함수이다.

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

비교 연산으으로 인해 처리 결과에 따라 결과가 달라진다.


  • 이런 점을 보완하며 메크로의 효율은 그대로 유지하며, 정규 함수의 모든 동작방식 및 타입 안정성까지 얻는 방법이 있다.
  • 바로 인라인 함수에 대한 템플릿을 만드는 것이다.

  • 괄호를 어지럽게 쓸 필요도 없고, 템플릿 함수이기에 인자를 여러 번 평가할지도 모른다는 걱정도 없어진다.
template<typename T>
inline void MaxValue(const T& a, const T& b){
    f(a > b ? a : b);
}


단순한 상수를 쓸 때는, #define보다 const 객체 또는 enum을 생각하자. 함수처럼 사용하는 메크로를 만들 때, #define 메크로보다 인라인 함수를 생각하자.

댓글남기기