본문 바로가기
C++공부/Effective C++

Effective C++ 항목 2: #define보단 const, inline을 사용하자.

by daisy0461 2024. 9. 30.

#define의 문제점

#define은 매크로 상수나 매크로 함수를 정의하는데 사용된다. 

이때 발생하는 문제점은 다음과 같다.

 

#define으로 만들어진 매크로 상수는 기호식 이름(symbolic name)으로 보이지만 컴파일러에겐 보이지 않는다.

#define A_define 16.3

#define은 컴파일러가 처리하기 전에 전처리기가 작동해서 코드 내에 있는 모든 A_define를 16.3으로 바꾸는 텍스트 치환을 수행한다.

즉, 컴파일러는 매크로 자체에 대해 아무 정보를 갖지 않고 치환된 값을 기준으로 컴파일한다.

 

그래서 생기는 문제점이 뭔가?

이렇게 정의되어 있다면 A에 16.3이라는 값을 매크로로 할당한 것이다.

그럼 이 값이 어떤 타입(int, double, float)인지 알 수 없다. 컴파일러는 매크로의 타입을 추론하지 않기 때문이다.

특히 16.3과 같은 숫자는 부동소수점 숫자지만 명시적으로 f, d를 붙이지 않음녀 컴파일러가 기본적으로 double로 취급한다.

 

또 다른 문제점은

#define으로 만들어진 것은 기호테이블에 들어가지 않는다.

더보기
더보기
더보기

기호 테이블: 컴파일러가 사용하는 심볼 테이블(symbol table)을 의미한다. 변수, 함수, 클래스와 같은 코드 내의 식별자들에 대한 정보를 저장하는 데이터 구조이다.

우리가 위 A 사용해서 에러가 발생하면 헷갈릴 수 있다.

소스 코드에는 A라고 있는데 에러 코드에는 16.3이라는 숫자가 있기 때문이다. 기호 테이블에 들어가지 않기 때문이다.

혼자 코드를 작성하면 16.3이 어디에서 온지 알 수 있지만 여러 사람과 함께 작업을 하면 다른 사람은 도대체 16.3이라는 숫자가 왜 나타난지 알 수 없다.

 

#define  매크로 함수의 문제점

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

보기에..()일단 너무 많다. 실제 만들 때도 인자마다 반드시 ()를 넣어줘야 하는 것을 반드시 기억해야한다.

()가 없으면 표현식을 매크로로 넘길 때 문제가 발생할 수 있다.

또 다른 심각한 문제가 있다.

int a = 5, b=0;
CALL_WITH_MAX(++a, b);			//a가 1번 증가.
CALL_WITH_MAX(++a, b+10);		//a가 2번 증가.

위처럼 비교 처리를 통해 나온 결과가 무엇이냐에 따라서 a의 증가 횟수가 달라지는 머리 아픈 현상이 발생할 수 있다.

 

이런 문제점이 있다면 당장은 문제가 없을 수 있지만 이후에 문제가 생기면 골치 아프기에 다른 방법을 사용하는 것이 나아보인다.

 

상수는 Const를 사용

const double A = 1.653;

위 방법은 매크로를 사용하지 않고 상수를 쓰는 방법이다.

 

장점

  • 언어 차원에서 지원하는 상수 타입의 데이터이기에 당연히 컴파일러 눈에도 보이며 기호 테이블에도 들어간다.

    위에서 기호 테이블에 들어가지 않아서 발생하는 문제점이 발생하지 않는다.
  • 상수가 부동소수점, 실수 타입일 경우 최종 코드의 크기가 #define보다 작게 나올 수 있다.

    매크로를 사용하면 A_define이 등장하면 선행 처리자에 의해 16.3으로 모두 바뀌면서 코드 안에 16.3의 사본이 등장한 횟수만큼 들어간다. 하지만 상수 타입의 A는 엄청나게 많이 사용하더라도 사본은 딱 하나만 생긴다.

#define을 상수로 교체할 때 주의점

  1. 상수 포인터를 정의하는 경우.
const char* const name = "JJ";

위와 같이 포인터는 꼭 const로 선언해주어야하고 포인터가 가르키는 대상도 const로 선언하는 것이 보통이다.

위처럼 const를 2번 써야한다는 의미이다.

위 코드에서 쓰인 char*보다는 요즘은 string을 사용하는 것이 더 낫다.

const std::string name ("JJ");

갑자기 그렇게 생각할 수도 있을 것 같은데

위에는 const 2개인데 왜 여긴 1개인가?!

포인터와 가르키는 대상을 const로 한다고 설명했었다. 여긴 포인터를 안쓰기에 name을 const로 만든 것이다.

 

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

어떤 상수의 유효범위를 클래스로 한정할 때 사용하는 방법이다.

class GamePlayer {
private:
	static const int Num = 5;
	int scores[Num];
    .....

위처럼 static으로 선언한 경우 해당 상수의 사본 개수가 한 개를 넘지 못한다.

그리고 특히 여기서 신기한 점이 있다.

위에 있는 Num은 '선언'된 것이지 '정의'된 것이 아니다.

더보기
더보기
더보기

선언: 코드에 사용되는 '어떤 대상'의 이름과 타입을 컴파일러에게 알려 주는 것.

extern int x;	//객체 선언
std::size_t num(int number);	//함수 선언
class Widget;	//클래스 선언

위 처럼 구체적인 세부 사항은 선언데 들어있지 않다.

extern은 생소할 수 있는데 메모리 할당 없이 외부에서 정의된 변수 x를 참조한다는 의미이다.

 

정의: 선언에서 빠진 구체적인 세부사항을 컴파일러에게 제공하는 것.

객체의 경우 '정의'는 컴파일러가 그 객체에 대한 메모리를 마련해 놓은 부분을 의미한다.

함수나 함수 템플릿의 경우 '정의'는 그들에 대한 코드 본문을 제공하는 것이다.

int x;		//객체 정의
std::size_t num(int number){	//함수 정의
...
}
class Widget{		//클래스 정의
...
}

의문이 들 수 있다.

...? 무슨 말이지 내가 잘못 이해했나? 오타인가? 라고 생각했다.

 

 처음 알게 된 사실인데

C++에서 정적 멤버로 만들어지는 정수류(정수 타입, char, bool 등) 타입의 클래스 내부 상수는 '정의'가 마련되어 있지 않아도 된다. 주소를 취하지 않는 한 정의 없이 선언만 해도 문제가 없다.

그럼 또 의문이 든다.

static const int Num = 5;

5라는 숫자가 들어가야하는 메모리가 할당되어야 하는 것이 아닌가? 라고

결론은 메모리가 할당되지 않는다. 상수 표현식으로 처리가 가능하기 때문이다.

더보기
더보기
더보기

상수 표현식: 컴파일 타임에 그 값이 결정되는 표현식. 프로그램 실행 중에 값이 변경되지 않는 표현식을 의미.

 

위 Num이라는 클래스 상수 주소를 구하거나 주소를 구하지 않음에도 컴파일러가 정의를 달라고 요청하는 경우에는 별도의 정의를 다음과 같이 제공해야한다.

const int GamePlayer::Num;

이때 클래스 상수 정의는 구현파일에 둔다. 헤더파일에 두지 않는다.

정의에는 상수 초기값이 있으면 안된다. 왜냐하면 클래스 상수의 초기값은 해당 상수가 선언된 시점에 바로 주어지기 때문이다.

더보기
더보기
더보기

 그렇다면 왜 상수가 선언된 시점에 바로 주어지면 안될까?

1. 언어 규칙: C++은 클래스 멤버 변수를 선언할 때 선언한 위치에서 초기화하는 것을 허용하지 않는다.

2. 불변성과 일관성: 클래스 상수는 불변의 특징을 갖고 있으며 객체의 상태와 연결되어야한다. 초기화 시점에 객체 상태가 반영되지 않으면 일관성을 읽는다.

3. 의존성 관리: 클래스 상수가 다른 멤버 변수에 의존하는 경우 그 변수가 초기화 된 이후에 초기화를 해야한다.

4. 객체 지향 원칙: 객체의 생성 과정에서 상태를 초기화하는 것이 객체 지향 프로그래밍의 중요한 원칙이다.

5. 컴파일 타임 오류 방지: 클래스 상수를 선언 시점에 초기화하지 못하도록 하여 컴파일 타임에서 오류 방지

 

#define매크로 대신 inline을 사용하자

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

위와 같이 사용하면 매크로의 효율은 그대로 유지하며 정규 함수의 모든 동작방식 및 타입 안정성도 취할 수 있다.

그리고 template이기 때문에 동일 계열 함수군을 만들어낸다. 동일한 타입의 객체 2개를 받고 둘 중 큰 값은 f에 넘겨서 호출하는 구조이다.

#define에 비해 ()로 신경써야할 이유도 없고, 인자를 여러 번 평가할지도 모르는 가능성이 없어진다.

그리고 유효범위 또한 접근 규칙을 그대로 따라간다.