1장 C++에 왔으면 C++의 법을 따릅시다 🏳

Effective C++ 책을 읽고 정리하고자 합니다.

궁금한 점, 보안점 남겨주시면 성실히 답변하겠습니다👍
+ 감상평 댓글로 남겨주시면 힘이됩니다. 🙇

#define의 문제점

우리가 해당 항목에서 기억해야할 점은 “가급적 선행 처리자보다 컴파일러를 가까이 하자” 이다.

C 언어에서 상수를 정의할 때, #define 을 활용하여 많이 정의했다.

#define ASPECT_RATIO 1.653

우리는 ASPECT_RATIO가 기호식 이름으로 보이지만 컴파일러에게는 기호식 이름 (ASPECT_RATIO) 가 전혀보이지 않는다. 소스코드가 넘어가기 전에 선행처리자가 어떻게든 전처리기는 메크로를 숫자 혹은 문자로 변환하려고 하기 때문이다. 따라서, 컴파일러에게는 ASPECT_RATIO 가 아닌 1.653 만 남아버리고 compile 가 관리하는 기호 테이블에 들어가지 않는다. 현 상황에서 에러가 발생할 경우 꽤나 혼란이 올 수 있다. 에러메세지에는 1.653 가 출력될 것이고, 다행히 바로 에러를 만난다면 찾기쉽겠지만, 훗날 만나게 될 경우 많은 시간을 소요하게 될 수도 있다.

이 문제의 해결법은 메크로 대신 상수 사용 이다.

const double AsepctRatio 1.653    // 대문자 표기는 메크로에 사용하는 것.
                                  // 보통 Camel형식으로 표기한다.

위와 같이 구성할 경우 AspectRatio는 언어 차원에서 지원하는 상수타입으로 인식되고, 컴파일러는 기호테이블에 해당 상수를 넣고 관리하게 된다. 따라서, 동일한 에러를 만나도 당황하지 않고 AspectRatio를 찾아가 수정할 수 있다.

추가로, 상수를 사용할 경우 컴파일의 최종 코드 사이즈가 줄어든다는 장점이 있다. 보통 메크로로 정의할 경우 전처리기에 의해 상수 사본이 여러개 생기기 된다. 즉, ASPECT_RATIO를 만나는 곳마다 사본이 생기기 됨으로 미약하지만 코드 사이즈가 늘어날 수 있다. (예전에 HW 자원이 부족할 땐 중요한 문제였을 수도..🤔)

#define 의 문제점을 해결하기 위해 상수로 변환하는데 있어 2가지만 주의하자.

상수 변환시 주의할 점

상수 변환 시 주의할 점은 상수 포인터 (constant point) 정의클래스 멤버로 상수 정의 2 가지이다.

🌱 상수 포인터 (const pointer) 정의 할 경우

상수는 대게 헤더 파일에 넣는 것이 상례 이다. (다른 소스 파일에서도 상수를 사용함.) 따라서, 헤더 파일 내 상수를 정의 할 경우 꼭 const 로 선언해야한다. 아울러 상수 포인터 같은 경우 포인터를 const 할 뿐아니라 가리키는 대상 까지 const로 정의해야한다. const에 관한 내용은 항목 3 에서 자세히 다루도록한다.

const char* const autorName = "doodoo";

// 보통 C++에서는 문자열을 포인터가 아닌 string 객체로 정의 함.

const std::string authorName = "doodoo";

🌱 클래스 멤버로 상수를 정의 할 경우

어떤 상수를 전역으로 사용하지 않고 한정적으로 사용하고 싶을 경우, 예를 들어 클래스 내에서만 사용, 할 경우에는 클래스 내 상수를 멤버로 만들 수 있다. 클래스 내에서 사용하는 상수가 사본이 아닌 오직 한개만 존재하기 위해선 static 멤버로 정의해야한다 (고유 상수로 만드는 방법).

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

위의 NumTurns는 상수를 정의한 것이 아닌 선언한 것이다. 보통 C++에서는 선언이 있을 경우 정의가 존재해야한다. 하지만, static으로 선언 될 경우 정수류 (각종 정수 타입, bool, char 등) 타입의 클래스 내부 상수는 예외로, 선언과 동시에 초기화하고 정의를 하지 않아도 된다. 단, 이들에 대해 주소를 취하거나, 오래된 컴파일러를 사용할 경우 별도의 정의를 제공해야한다.

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

클래스의 정의 파일 같은 경우 구현부에 둔다. 정의부에는 값이 주어지지 않는데, 선언부에서 값을 선언함과 동시에 초기화 하기 때문이다. 따라서, 정의부에 작성할 때 값을 따로 줄 필요가 없다.

조금 오래된 컴파일러는 상수 선언을 아예 받아들이지 않을수도 있다. 즉, 정적 클래스 멤버가 선언될 때 초기화하는 것이 잘못됐다고 판단하고 허용하지 않을수도 있다. 그럴 경우 아래와 같이 작성하면 된다.

class CostEstimate {
private:

  static const double FudgeFactor;    // 정적 클래스 상수의 선언
  ... // 해당 선언부는 헤더파일에 작성함.
};

const double
  CostEstimate::FudgeFactor = 1.35;   // 상수의 선언에 대한 정의.

// 윗 부분은 구현 파일에 작성한다.

웬만한 경우 위의 케이스로, 상수에 대한 모든 것이 통제가 가능해진다. 딱 한 가지 예외가 있는데, 오래된 컴파일러를 사용하면서 컴파일 도중 클래스 상수 값이 필요한 경우이다. 대표적으로, 윗 예시에서 작성한 GamePlayer::scroes를 초기화 하는 경우이다. 컴파일러는 컴파일 과정에서 해당 배열의 크기를 알 수 없을 경우 에러를 출력한다. 우리가 오래된 컴파일러를 사용할 때, 선언부에서 scores 를 클래스 상수로 정의해 초기화 할 시점에 컴파일러는 클래스 상수의 정의부 초기화 값을 알 수 없다. 따라서, scores 배열을 올바르게 만들 수 없게 된다. 이를 해결할 방법을 알아보자.

나열자 둔갑술 (enum hack)

오래된 컴파일러의 문제를 해결하기 위한 좋은 방법은 나열자 둔갑술 (enum hack)이다. 해당 방법은 enummerator 타입이 int 타입 놓일 곳에 사용할 수 있다는 C++ 언어의 원리를 활용한 방법이다.

class GamePlayer {
private:
  enum { NumTurns = 5 };    // enum hack :
                            // Numturns 의 기호식을 5로 변환함.

  int socres [NumTurns];    // 오래된 컴파일러라도 const 선언 정의 관계없이
                            // 깔끔하게 해결 가능
}

나열자 둔갑술은 알아 두면 여러가지 좋은 방면이 존재한다.

🌱 첫째, 나열자 둔갑술 동작 방식이 const 보다 #define에 가깝다. const 는 주소를 알아 내는 것이 합당하지만, enum은 주소를 취하는 일이 불가능하다. 따라서, 좀 더 메크로에 가깝다고 볼 수 있다. 선언한 정수 상수를 가지고 다른 사람이 주소를 얻는다든지 참조자를 쓴다든지 하는 것이 싫다면 enum 이 좋은 자물쇠가 될 것이다. 추가적으로, enum#define과 같이 추가적인 사본을 만들지 않아 메모리 측면에서도 절약된다. 만약 정수상수를 써야한다면 enum을 적극활용 하도록 하자.

🌱 둘째, 템플릿 메타프로그래밍에 핵심이다. (눈에 익혀두도록 하자. 항목 48에서 배울 것임.. 🙄)

지금까지 #define 메크로 중 상수로 변환할 경우 발생하는 문제를 다루고 해결방법들을 알아보았다. 하지만, 메크로는 상수만 있는 것이아니라 함수도 존재한다. 메크로 함수에서 어떤 문제가 존재하는지 해결방법은 무엇인지 알아보자.

메크로 함수의 문제점

C 언어를 사용하거나 오픈소스를 보면 #define을 활용한 메크로를 많이봤으며, 반복하기 귀찮은 부분들은 메크로로 전환해서 사용하는 것을 많이 보았다. 메크로 함수를 사용하는 가장 큰 이유는 함수 호출의 오버헤드를 줄이고자하는 것이다.

#define CALL_WITH_MAX(a, b) f((a) > (b) ? (a) : (b))

위 예시는 메크로 인자 중 큰 것을 사용해서 임의의 함수 f를 부르는 것이다. MAX라는 함수를 만드는 것 대신 치환해 사용하면 보기에는 간단하다.

하지만, 해당 메크로 함수에는 문제점들이 존재한다. 우선 메크로 함수 는 괄호를 여러번 써야하는 귀찮음이 존재한다. 해당 인자마다 괄호를 써주지 않으면 인자인지 아닌지 해석을 못하기 때문이다. 또 다른 문제를 보자.

int a = 5, b = 0;

CALL_WITH_MAX(++a, b);      // a가 클 경우 두 번 증가함.
CALL_WITH_MAX(a, b + 10);   // b가 클 경우 한 번 증가함.

위의 예시를 보면, 전달하는 인자의 크기가 클 수록 증가하는 횟수가 달라진다. 이건 무슨 괴현상일까? 😱

함수호출을 제거한다는 명목하에 이런 일이 자행됐다니.. C++에서는 이런 문제를 template inline function 을 활용해 해결할 수 있다.

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

위의 설명은 항목 30에서 자세히 다루도록 할 것이다.

메크로 함수가 아닌 템플릿 인라인 함수를 사용해야한다는 점을 기억하기 바란다.

Appendix