관리 메뉴

기억을 위한 기록들

[CPP-effective] 1-1 #define쓰려거든 다른걸 떠올리자. 본문

C & CPP/Effective C++

[CPP-effective] 1-1 #define쓰려거든 다른걸 떠올리자.

에드윈H 2021. 2. 21. 15:29

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

(전처리기보단 컴파일러를 가까이 하자.)

#define PI 3.1415926535

- 해당 PI가 있으면 컴파일러에게 넘어가기전에 전처리기가 밀어버리고 숫자 상수로 바꾸어버린다.

 그 결과로 PI라는 이름은 컴파일러가 쓰는 기호 테이블에 들어가지 않는다.  

소스안에는 PI가 있는데 에러 메시지엔3.1415926535가 있으므로, 숫자 상수로 대체된 코드에서 컴파일 에러라도 발생하면 꽤나 헷갈릴 수 있다.  게다가 PI를 내가 작성하지 않았다면 더욱 찾기 힘들 수 있다. 찾아 들어가느라 시간을 허비할 수도 있다.

 

해결법으로는 상수(const)를 쓰는 것이다.

const double PI = 3.1415926535;

 그리고 매크로를 쓰면 코드에 PI가 등장하기만 하면 전처리기에 의해 모두 3.1415926535 로 바뀌면서 목적 코드 안에 3.1415926535의사본이등장횟수만큼들어가는데, 상수타입의 PI는 아무리 여러번 써도 사본은 딱 한개만 생긴다.

 

하지만, #define를 상수로 교체할때의 주의사항이 2가지가 있다.

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

- 상수 정의는 대게 헤더 파일에 넣는 것이 상례이므로, 포인터(pointer)는 꼭 const로 선언한다. 어울러 가리키는 대상도 const로 선언.

const char* const myName = "Hello";
const std::string myName = "Hello";

 

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

선언을 먼저보면,

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

- 위와 같이 상수의 사본 개수가 한 개를 넘지 못하게 하고 싶다면 정적(static) 멤버로 만들어야 한다.

여기서 NumTurns은 '선언' 된 것이다. '정의'가 아니다.

- 보통 '정의'로 있어야 하지만, 정적 멤버로 만들어지는 정수(정수, char, bool 등) 타입의 클래스 내부 상수는 예외이다.

(이들에 대한 주소를 취하지 않는 한, 정의 없이 선언만 해줘도 아무 문제가 없다. 단, 클래스 상수의 주소를 구한다던가, 컴파일러가 잘못 만들어져 정의를 달라고 할 수도 있음으로 정의를 제공해야 한다.

 

정의는 아래와 같이 된다.(구현파일에 둔다. 헤더파일X)

const int GamePlayer::NumTurns;

 

정의에는 상수의 초기값이 있으면 안 되는데, 클래스 상수의 초기값은 해당 상수가 선언된 시점에서 바로 주어지기 때문.(즉, NumTruns는 선언될 당시에 바로 초기화가 된다.)

 

조금 오래된 일부 컴파일러는 위의 문법을 받아들이지 않는 경우가 종종 있다.

이유로 정적(static) 클래스 멤버가 선언된 시점에 초기값을 주는 것이 대게 맞지 않다고 판단.

클래스 내부 초기화를 허용하는 경우가 정수 타입의 상수에 대해서만 국한되어 있다.

//헤더파일
class GamePlayer{
private:
     static const int NumTurns;//정적 클래스 상수의 선언
     
     //..
}

//구현파일

//..
const int GamePlayer::NumTurns = 5; //정적 클래스 상수의 정의 
//..

웬만한 경우라면 이것으로 충분하다.(싱글턴 패턴도 비슷한 느낌)

딱 한 가지 예외가 있다면 해당 클래스를 컴파일 도중에 클래스 상수의 값이 필요할 때인데,  예를 들어 배열 멤버를 선언할 때이다.

class GamePlayer{
private:
     enum { NumTurns = 5 }; //NumTurns를 5에 대한 기호식 이름으로 만든다.
     int score[NumTurns];  //이렇게 사용
     //..
}

나열자 둔갑술(enum hack)이라는 통칭이다.

 

1. 나열자 둔갑술은 동작 방식이 const보다는 #define에 더 가깝다.

2. 실용적인 이유로 많은 코드에서 쓰고 있다. 그리고 나중에 쓸 템플릿 메타 프로그래밍의 핵심기법이다.

 

 

 

 

 

 

#define의 오용 사례(매크로 함수 사용)

매크로 함수는 함수처럼 보이지만 함수 호출 오버헤드를 일으키지 않는 매크로를 구현하는 것이다. 예를 들어

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

더 큰 것을 사용해서 어떤 함수 f를 호출하는 매크로 함수 calc가 있다.

이런 식의 매크로는 단점이 많은데,

int a=5;
int b=0;
calc(++a, b); //a 두번 증가
calc(++a, b+10); //a 한번 증가

함수 f가 호출하기도 전에 a증가 횟수가 달라진다. 이 방법 대신 정규 함수의 동작 방식 및 타입 안전성까지 완벽히 취할 수 있는 방법이 있다. 바로 인라인 함수에 대한 템플릿 사용이다.

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

이전에 비해 괄호로 분칠 할 필요가 없고, 인자를 여러 번 평가할지도 모른다는 걱정도 없어진다.

게다가 calc는 진짜 함수이기 때문에 유효 범위 및 접근 규칙을 그대로 따라간다.

 

정리

1. 단순한 상수를 쓸 때엔, #define 보다 const 객체 혹은 enum을 우선 생각.

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