💙ref.

스콧 마이어스, Effective C++ [3판], 곽용재(역), 프로텍미디어, 2015

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


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


define 코드 예제

1
#define ASPECT_RATIO 1.653

소스 코드가 컴파일러로 넘어가기 전에 선행 처리자가 해당 코드를 숫자 상수로 변환시킨다. 결과적으로, ASPECT_RATIO라는 이름은 컴팡리러가 쓰는 기호 테이블에 들어가지 않는다!

숫자 상수로 대체된 코드에서 컴파일 에러가 발생하면 곤란한 상황이 발생한다,

  • 소스 코드엔 ASPECT_RATIO가 있었는데 에러 메시지엔 1.653가 출력됨
    • 만약, 해당 define이 정의된 코드가 직접 작성한 것이 아니라면 어느 코드인지 찾는 데 시간을 낭비하게 됨
    • 기호식 디버거에도 같은 문제가 발생할 수 있음(기호 테이블에 들어가지 않기 때문)


위 예제 코드의 해결안

1
2
const double AspectRatio = 1.653
// 대문자로만 표기하는 이름은 대개 매크로에 쓰는 것이므로, 이름 표기도 바꿔주어야 한다!
  • 언어 차원에서 지원하는 상수 타입의 데이터이므로 컴파일러도 인식 가능
    • 컴파일러 기호 테이블에 저장
    • 상수가 부동소수점 실수 타입일 경우, 컴파일을 거친 최종 코드의 크기가 #define보다 작게 나올 수 있음
      • #define의 경우 1.653의 사본이 등장 횟수마다 만들어지지만, 상수 타입일 경우 딱 한 개만 생성


1. #define => 상수 교체 시 주의사항

1.1. 상수 포인터 정의

  • 상수 포인터(constant pointer) 정의

    • 대개 헤더 파일에서 상수를 정의하므로, 포인터를 반드시 const로 선언

    • 포인터가 가리키는 대상도 함께 const로 선언하는 것이 일반적임

      eg) 어떤 헤더 파일 안에 char* 기반의 문자열 상수를 정의할 경우, const를 두 번 기재

      const char * const authorName = "Scott Meyers";

      const std::string authorName("Scott Meyers"); => string객체 사용을 권장!


1.2. 클래스 멤버로 상수 정의

어떤 상수의 유효범위를 클래스로 한정할 경우, 사본 개수가 한 개를 넘지 못하게 하고 싶다면 정적 멤버로 선언하자.

1
2
3
4
5
6
class GamePlayer{
private:
	static const int NumTurns = 5;	// 상수 선언
    int scores[NumTurns];			// 상수를 사용하는 부분
	...
};
  • 코드 작성 시 선언 후 정의부를 작성하는 것이 일반적임

  • 정적 멤버로 만들어지는 정수류 타입의 클래스 상수는 정의 마련하지 않음

    • 클래스 상수의 주소를 구할 경우에만 정의부 구현

      const int GamePlayer::NumTurns;

      • 별도의 정의를 기재할 경우, 값을 제공하지 않음
        • 이때, 클래스 상수의 정의는 구현 파일에 기재
        • 클래스 상수의 초기값은 해당 상수가 선언된 시점에 바로 초기화


상수 대체 시 주의사항

  • 클래스 상수는 #define으로 만들지 말자

    • #define은 클래스 상수를 정의하는데 사용할 수 x

    • 어떤 형태의 캡슐화 혜택도 받을 수 없음

      eg) private 형태의 #define 구문은 존재하지 않음




2. 클래스 멤버 초기화가 어려운 경우

컴파일러가 정적 클래스 멤버가 선언된 시점에 초기값을 주는 것이 맞지 않는다고 판단할 경우, 초기값은 상수 정의 시점에 기재하자.

1
2
3
4
5
6
7
8
class CostEstimate {
private:
    static const double FudgeFactor;
    ...
};

// 정적 클래스 상수의 정의 => 구현 파일에 적자!
const double CostEstimate::FudgeFactor = 1.35;
  • 이때, 해당 클래스를 컴파일하는 도중 클래스 상수의 값이 필요한 경우가 생김

    • GamePlayer::scores과 같이 배열 멤버 선언

      => “나열자 둔갑술(enmu hack)” 기법을 사용해 해결 가능


1
2
3
4
5
6
7
8
class GamePlayer {
private:
    // "나열자 둔갑술": NumTurns를 5에 대한 기호식 이름으로 만든다.
    enum { NumTurns = 5 };	
    
    int scores[NumTurns];
    ...
};
  • 나열자 둔갑술

    • 동작 방식이 const보다 #define에 가까움

      • const의 주소를 잡아내는 것은 합당하나, enum의 주소를 함부로 잡을 수 x

        (=> #define과 유사함!!)

      • 개발자가 선언한 정수 상수의 주소를 다른 사람이 얻는 게 싫을 경우

      • const 객체에 대한 메모리를 만들고 싶지 않을 경우

        (=> enum의 경우 어떤 형태의 쓸데없는 메모리 할당을 하지 않음)

    • 템플릿 메타프로그래밍의 핵심 기술이 됨



2.1. 오용 사례

매크로 함수

1
2
// a와 b 중 큰 것을 f로 넘김
#define CALL_WHTH_MAX(a, b) f((a) > (b) ? (a) : (b))
  • 함수처럼 보이지만 함수 호출 오버헤드를 일으키지 않는 매크로 구현
    • 이런 식의 매크로는 단점이 많음. 권장하지 않는다.
    • 매크로 본문에 들어 있는 인자마다 반드시 괄호를 씌워 주자.
      • 괄호가 없으면 표현식을 매크로에 넘길 때 오류가 발생할 수 있음


1
2
3
4
int a = 5, b = 0;

CALL_WHTH_MAX(++a, b);		// a 두 번 증가
CALL_WHTH_MAX(++a, b+10);	// a 한 번 증가 
  • f가 호출되기 전에 a가 증가하는 횟수가 달라짐
    • 비교를 통해 처리한 결과가 어떤 것이냐에 따라 달라진다(프로그램에 올 발생)


단점 개선한 코드

1
2
3
4
5
template<typename T>
inline void callWithMax(const T& a, const T& b)
{
	f(a > b ? a : b);
}
  • 템플릿이기 때문에 동일 계열 함수군(family of functions)을 생성

    • 동일한 타입의 객체 두 개를 인자로 받고 둘 중 큰 것으로 f로 넘겨 호출

    • 함수 본문에 괄호를 쓸 필요 x

    • 인자를 여러 번 평가하지 않음

    • callWithMax는 실제로 구현된 함수이므로 유효범위 및 접근규칙을 그대로 따라감

      • cf) 임의의 클래스에서만 사용할 수 있는 인라인 함수에 대해서도 문제가 발생하지 않음

        매크로는 이런 이야기에 대해 관련 x


*🪄Conclusion)* **

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

Leave a comment