[Effective C++] 항목 2: #define을 쓰려거든 const, enum, inline을 떠올리자
C++에서는 선언(declaration)과 정의(definition)를 구분합니다:
- 선언: 컴파일러에게 이름과 타입을 알려주는 것
- 정의: 실제 메모리 할당이 이루어지는 것
1. #define을 지양해야 하는 이유들
1-1. 컴파일 곤란
#define ASPECT_RATIO 1.653
만약 이러한 코드를 쓰게 된다면
우리는 ASPECT_RATIO라는 이름으로 쓰지만 컴파일러는 이를 전부 1.653으로 인식하게 됩니다.
컴파일러로 넘어가기 전 선행처리자가 전부 1.653으로 바꾸어 버리기 때문입니다.
만약에 이부분에서 컴파일 에러가 발생하면 1.653이 에러메시지에 떠서 문제를 찾기 어려워질 수 있습니다.
매크로 상수는 기호 테이블에 기록되어 있지 않습니다.
그저 1.653으로 대체되어서 컴파일 될뿐입니다.
해결책
const double AspectRatio = 1.653;
const를 쓰면 컴파일러도 이 변수명을 알고 기호 테이블에도 들어가게 됩니다.
1-2. 용량 문제
#define ASPECT_RATIO 1.653
선행 처리자에 의해서 ASPECT_RATIO가 전부 1.653으로 바뀌게 되면서
코드 안에 1.653의 사본이 등장 횟수만큼 들어가게 되지만
const double AspectRatio = 1.653;
const를 이용하게 되면 여러번 사용하게 되더라도 사본은 하나만 생기게 됩니다.
// #define 사용 시
#define PI 3.14159
double a = PI * 2; // 컴파일러는 "double a = 3.14159 * 2;" 로 봄
double b = PI * 3; // 컴파일러는 "double b = 3.14159 * 3;" 로 봄
// const 사용 시
const double pi = 3.14159; // 심볼 pi는 컴파일러에 의해 추적됨
double a = pi * 2; // 컴파일러는 pi가 const임을 알고 있음
double b = pi * 3; // 컴파일러는 같은 pi 참조를 인식함
물론 현대 컴파일러는 매우 발전되어 있어 대부분의 경우 중복된 리터럴도 최적화할 수 있지만
항상 그런 것은 아니며 컴파일러와 최적화 설정에 따라 달라질 수 있습니다.
const를 사용하면 컴파일러에게 명확한 힌트를 제공하므로 더 일관된 최적화가 가능합니다.
2. #define을 상수로 교체할 때 주의할 점
2-1. 상수 포인터를 정의하는 경우
포인터와 포인터가 가리키는 대상까지 const로 선언해야 합니다.
Ex)
const char * const Name ="yeoul";
그러나 그냥 string쓰면 한번만 쓸 수 있다.
const std::string authorName("yeoul");
2-2. 클래스 멤버를 상수로 정의 하는 경우
상수의 유효 범위를 클래스로 하고싶다면 그 상수를 멤버 변수로 만들어야 하고
사본 개수가 1개를 넘지 못하게 하고 싶으면 static 멤버로 만들어야 합니다.
class Yeoul
{
private:
static const int YeoulNum = 5;
int scores[YeoulNum];
}
YeoulNum은 선언된 것이지 정의된 것이 아니다.
일반적으로 정의가 되는 것이 보통이지만 static member로 만들어지는 정수류 타입의 클래스 내부 상수는 예외다.
주소를 취하지 않는 한 정의가 없어도 된다. (요즘 컴파일러는 다 알하서 해줘서 문제 없음)
const int Yeoul::YeoulNum;
이렇게 하면 YeoulNum이 정의 된다. (cpp 파일에서)
상수가 선언될 때 초기값이 주어지기에 정의할땐 초기값이 없어도 된다.
주의 : 클래스 상수를 #define으로 하지 말자
유효 범위가 없는 아이를 써버리면 안된다.
어떠한 캡슐화 혜택도 못받는다.
즉 private 성격의 #define따위는 없다. (상수 데이터 멤버는 캡슐화 됨 ex : YeoulNum)
+ 가끔 선언과 초기화를 안해주는 컴파일러도 있는데 그럴땐 정의할때 초기화 해주자.
3. 나열자 둔갑술
구식 컴파일러에서는 선언과 초기화를 같이 안해줌으로
배열의 크기등을 상수 변수로 정해줄 수 없습니다.
그래서 쓰는 방법이 enum hack 즉 나열자 둔갑술 입니다.
3-1. 나열자 둔갑술은 const보다는 #define에 가깝다.
const는 주소를 가진것이 합당하지만 #define은 아니다.
enum도 #define처럼 주소를 얻는 것이 불가능하다.
그러므로 개발자가 선언한 정수 상수를 이용해서 주소를 얻는 다던지 참조자를 쓰는것이 싫다면
enum은 아주 좋은 자물쇠가 됩니다.
enum은 #define처럼 쓸데없는 메모리 할당도 저지르지 않습니다.
+ 이렇게 쓰는 문법에 익숙해 지자
class Yeoul
{
public:
enum {num = 3};
int scores[num];
};
4. 매크로 함수의 문제
#define CALL_WITH_MAX(a,b) f ( (a) > (b) ? (a) : (b) )
이런식으로 매크로를 만들어주면 아주 큰일이 납니다.
int a=5;
int b=0;
CALL_WITH_MAX(++a,b); // a가 두 번 증가함
CALL_WITH_MAX(++a,b+10); // a가 한 번 증가함
2번 증가 하는 이유는
f((++a) > (b) ? (++a) : (b))
이렇게 실행되면 첫번째가 참이 되어서 두번째도 실행되고 결국 a는 2번 증가합니다.
2번째는 조건이 거짓이 되어서 1회만 증가하게 됩니다.
이렇듯 우리가 예상하지 못한 결과가 자주 일어나게 됩니다.
4-1. 해결책
인라인 함수에 대한 템플릿을 준비하자
template<typename T>
inline void CallWithMax(const T& a, const T& b)
{
f(a>b ? a : b);
}
이렇게 하면 매크로 처럼 귀찮게 괄호를 안써줘도 되고
매크로 처럼 예상치 못한 결과가 일어날 가능성도 없습니다.
또한 함수이기에 유효범위와 접근 규칙도 함수와 똑같이 따라간다.
5. 결론
const, enum, inline이 우리 곁에 있다는 사실을 알고 define 사용을 줄이자.
#include, #ifdef #ifndef같이 꼭 필요한 경우도 있긴 합니다.
이것 만은 잊지 말자
- 단순한 상수를 쓸 때는, #define보다 const 객체 혹은 enum을 우선 생각합시다.
- 함수처럼 쓰이는 매크로를 만들려면, #define 매크로보다 인라인 함수를 우선 생각합시다.
항목 3
https://yeoul0714.tistory.com/28
[Effective C++] 항목 3: 낌새만 보이면 const를 들이대 보자!
0. 함수에서 const 위치 개념잡기0-1. const가 앞에 있을 경우const int getValue() { return 42;} 이 경우, const는 함수의 반환값이 const라는 것을 의미합니다. 반환되는 int 값을 수정할 수 없습니다. 하지만 기
yeoul0714.tistory.com