본문 바로가기

Effective C++/1. C++에 왔으면 C++ 법을 따릅시다

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

항목은 가급적 선행 처리자(#, preprosessor) 보다 컴파일러를 가까이 하자 라는 말임

 

#define RATIO 3.141592 라는 코드를 짰다고 생각하면, 컴파일러에게 넘어가기 전에 preprosessor RATIO라는 글자를 밀어버리고 3.14라는 상수로 바꾼다. RATIO라는 이름은 컴파일러가 쓰는 기호 테이블에 들어가지 않게 된다. 그래서 컴파일 에러라도 발생하게 되면 꽤나 헷갈릴 있다. RATIO 에러가 났다고 해도 컴파일러는 3.14라는 상수에서 에러가 것으로 알기 때문에 에러 메세지에 RATIO라는 글자가 것이고 그럼 어디서 틀렸는지 햇갈릴 있다.

 

문제는 symbolic debugger(기호식 디버거)에서 나타날 수도 있다. 마찬가지로 기호 테이블에 RATIO라는 이름이 안들어가기 때문이다.

 

문제에 대한 해결책은 메크로(#) 대신 상수(const) 쓰는 것이다.

 

const double ratio = 3.141592;     // 대문자로만 표기하는 이름은 대게 매크로에서 쓰는 것이기 때문에 이름도 바꿔준다.

 

이렇게 하면 최종적인 코드 크기를 줄일 있을 뿐더러, 메모리 점유율도 줄일 있을 것이다. #define 모든 RATIO 3.141592 바꾸는 것이기 때문에 ratio 글자 보다 길어질 것이다.

 

 

#define => const 바꿀

 

  1.  상수 포인터를 정의하는 경우는 주의 해야 한다.

 

상수 정의는 대게 헤더 파일에 넣는 것이 상례이다.(다른 소스 파일이 이것을 include 해서 ) 그래서 pointer const 선언하자.

 

예를 들어 헤더 파일안에 char* 기반의 문자열 상수를 정의한다면

 

const char* const authorName = "Scott Meyers";

 

이런식으로 포인터(authorName)에도 const 붙여야 하고, 포인터가 가르키는 대상("Scott Meyers" 첫번째 요소 S)에도 const 붙어야됨

 

원래 문자열 상수 => cosnt char* authorName = "Scott Meyers"

 

문자열의 첫번째 요소의 type char const 하지만 pointer authorName에는 const 없기 때문에 이런 코드가 가능한데

 

pointer(authorName)에까지 const 걸어버리면 위와 같은 코드는 에러가 뜬다.

 

원래 const type이름 뒤에 붙는 다는 사실을 기억하자.

 

 

 

 

const char* T라고 치면

 

T const authorName; 되는 것이다.

 

근데 이런 구닥다리 문자열 보다는 string 객체가 대체적으로 사용하기 괜찮다.

 

const std::string authorName("Scott Meyers")

 

이러면 string 객체인 authorName const 걸린 것임

 

 

 

  1. Class member 상수를 정의 하는 경우(클래스 상수를 정의하는 경우)

 

그냥 상수말고 Class 상수맴버를 쓰는 이유는, 상수의 유효범위를 클래스로 한정하고자 할 때이다.

 

상수의 copy 갯수가 개를 넘지 못하게 하고 싶다면, (전역변수로 const 때처럼) static member 만들어야 한다.

 

class GamePlayer {

private:

static const int NumTurns= 5;      // 상수 선언(정의가 아님!!!!)

int scores[NumTurns];                  // 클래스 내에서 상수를 사용하는 모습

};

 

상수 선언이 아닌 정의 인것에 주목하자. C++에서는 사용하고자 하는 것에 대해 "정의" 마련되어 있어야 하는게 일반적이지만, Static Member 만들어지는 정수류(각종 정수 type, char, bool ) 타입의 클래스 내부 상수는 예외이다.

 

참고)

 

 

static 맴버는 전역 변수인데, 맴버변수 하는 , notice 보면 동일 클래스 안에서 정보 교환을 위해 사용됨, 그리고 클래스 안에서는 선언만 한다. 클래스 밖에서 정의 초기화를 (전역변수 이기 때문)

 

 

, 클래스 상수의 주소를 구해야 한다던가, 컴파일러가 정의를 달라고 하면  정의를 제공해야 하는데,

 

const int GamePlayer::NumTurns;   // NumTurns의 정의, 초기화를 하지 않는다.

 

정의는 클래스 내부(헤더파일) 아니라 외부(구현 파일) 둔다. 상수(const) 선언과 동시에 바로 초기화를 한다. 그냥 static이면 외부에서 초기화를 해도 되는데, 선언과 동시에 초기화를 했기 때문에 외부에서 초기화를 하면 안됨

 

, 주의해야 점은 클래스 상수를 #define으로 만들 생각이 들면 안된다. #define으로 클래스 안에서 정보 교환을 한다? 안됨.

 

왜냐하면 #define 유효범위라는 개념 자체가 없기 때문이다.(#undef 의존함) 매크로(#) 자체가 일단 정의되면 컴파일이 끝날때까지 유효하다(#undef 전까지) 그래서 #define 클래스 상수로 수도 없고, encapsulation 불가능하다. 이와 대조적으로 상수 맴버는 가능하다.

 

조금 오래된 컴파일러는 위와 같은 문법을 받아드리지 않는 경우가 있다

이유는 static 클래스 멤버가 선언된 시점에 초기값을 주는 것이 대개 맞지 않다고 판단하기 때문이. 그리고 클래스 내부 초기화가 허용되는 경우가 정수 타입의 상수에 대해서만 한정되어 있기 때문이다. 이 경우 초기값을 상수의 '정의' 시점에 주도록 하자.

-> 이게 되는 이유는 선언 시점에서 초기화를 하지 않았기 때문이다.

 

 

선언과 정의를 나누어서 하였다. 하지만 함수밖에서 정의한 것은 에러가 않고, 함수 안에서 정의한 것은 에러가 났다.

 

 

in declaration => 함수 안에서는 선언을 포함한 정의라 보고(?) 동일한 이름의 변수가 선언된 것으로 판단한 하다. 이게 안되는지 도저히 모르겠다..

 

때도 가지 예외가 있는데 바로

 

 

클래스를 컴파일 하는 도중에 클래스 상수 값이 필요할 때이다.

 

정수 type static class const memeber 대한 class 내부 initialize 금지하는(표준에 맞지않는 구식의) 컴파일러에 대한 배려로 괜찮은 방법을 추천하면. "나열자 둔갑술(enum hack)" 이라 알려진 기법이 있다.

 

소년코딩 - C++ 05.04 - 열거형, enum (tistory.com)

 

코드를 나열자 둔갑술로 다시 표현하면

 

 

 

NumTurns 라는 클래스 상수(5) 만들 , 5 대한 기호식 이름으로 바꾸는 것이다.

 

나열자 둔갑술은 동작 방식이 const 보다 #define 가깝다. enum 일종의 class 인데, 상수와 11 대응되는 ID 맴버인 class라고 생각하면 편함. -> 상수랑 11 대응이므로 #define 가까움

 

  1. const 주소를 잡아내는건 가능, enum 주소를 취하는건 불가능(내가 선언한 정수 상수를 다른 사람이 주소를 얻는다던지, 참조자를 쓴다던지 하는 것이 싫으면 매우 좋은 자물쇠임)
  2. 정수 type const 객체는 컴파일러가 메모리공간을 할당 안하는게 국룰이지만, 하는 컴파일러도 있음 => 어떤 컴파일러에서든 메모리공간을 할당안하고 싶다면 enum 사용해라. #define 처럼 메모리를 할당하지 않음

 

출처: <https://gpgstudy.com/forum/viewtopic.php?t=7037>

 

class CA {

public:

enum E_LIST{e1, e2, e3, e4};

};

 

enum = class 이기 때문에 type 정의임, type 정의는 메모리 안잡아먹음

 

여기서

CA::E_LIST m_elist;

 

이런식으로 하면 m_elist 라는 변수(type=CA::E_LIST) 선언/정의 하면 메모리공간이 생김(type 크기만큼)

 

class GamePlayer{

private:

    enum { NumTurns = 5 };

    int scores[NumTurns];

 

};

 

다시 코드를 보면 이때 NumTurns 변수가 아님(변수 선언이 없다.) 메모리 공간을 잡아 먹지 않음

지금 핵심은 상수를 enum type으로 대체하는 것임. C++에선 enum type 값은 int 놓인 곳에서도 있다는 것을 활용하는 것이다.

 

 

 

 

다시 선행 처리자에 집중하면,,

 

#define 다른 오용 사례는 매크로 함수 이다. 함수처럼 보이지만 함수 호출 오버헤드를 일으키지 않는 매크로를 구현하는 것이다. 아래의 예를 보자

 

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

 

매크로 인자들 것을 사용해서 어떤 함수 f 호출하는 매크로 이다.

이런 식의 매크로는 단점이 한두개가 아니다. 일단 이런 매크로를 작성할 때는 매크로 본문에 들어 있는 인자마다 반드시 괄호를 씌워 주는 센스가 필요하다.(?) 이게 안되있으면 표현식을 매크로에 넘길 골치 아픈 일이 발생할 있다. 근데 괄호만 씌워준다고 해서 끝난걸까? 아래 예제를 보자.

 

 

 

 

f 호출 되기 전에 a 증가하는 횟수가 다른 것을 있다. 비교를 통해 처리한 결과가 어떤 것이냐에 따라 달라지기 때문이다.

 

첫번째 CALL_WITH_MAX f( ++a > b ? ++a : b) 이렇게 되는데, 비교문에서 한번 증가하고, 삼항연산자의 결과가 ++a 이므로 이때 한번 증가한다.

두번째 CALL_WITH_MAX 비교문에서 한번 증가하고, 삼항연산자의 결과가 b+10이기 때문에 증가하지 않는다.

 

C++ 에서는 함수 호출을 없애 준다는 명목 하에 자행되는 이런 필요가 없다. 매크로의 효율을(함수 호출를 줄여줌) 그대로 유지함과 동시에 정규 함수의 모든 동작방식 타입의 안전성까지 완벽히 취할 있는 방법이 있다. 바로 인라인 함수에 대한 템플릿을 준비하는 것이다.

 

 

 

template 함수 = 함수 형식을 지정해놓고, 다른 곳에서 템플릿 함수를 call 하면 함수를 만들어 사용하는 것이다.

 

inline 함수 = 함수를 호출하는 형식(CALL) 아니라, 컴파일 함수 호출부분을 함수 내용으로 치환해서 함수 호출 오버헤드를 줄이는

소년코딩 - C++ 08.06 - 인라인 함수 (inline function) (tistory.com)

 

매크로 함수의 특징(inline 함수로 단순 치환으로 인한 함수 호출 줄이기, templete 으로 매크로의 효율 가져오기) 모두 가지면서, templete 으로 진짜 함수를 만드는 것이기 때문에 유효범위라는 것도 가져갈 있다.(그에 반해 매크로는 유효범위란게 무엇인지 모른다.) 임의의 클래스 내에서만 있는 매크로 함수 이런게 가능한 것이다.

 

이것만은 잊지 말자!!

 

  • 단순한 상수를 때는, #define 보다는 const 객체 혹은 enum 우선 생각하자
  • 함수처럼 쓰이는 매크로를 만들고 싶으면 #define 보단 인라인 함수, 또는 인라인 함수에 대한 템플릿을 생각하자.