Patrick's Devlog

[Effective C++] Chapter 1 정리 - 1 본문

Study/C++

[Effective C++] Chapter 1 정리 - 1

Patrick_ 2022. 5. 9. 18:08

개요

C++에 관련하여 조금 더 효과적이고 심도있게 공부하기 위해 관련 서적을 구매하여 공부할 예정이다. 서적은 C++를 조금 더 잘 설계하고 활용하고자 스콧 마이어스의 <Effective C++>를 선정하였다. 

앞으로 정리해야할 부분은 블로그에 차근차근 작성할 예정이다. 


Item 1 : C++를 언어들의 연합체로 바라보는 안목은 필수

C에서 파생된 C++는 초기와 다르게 점차 발전하여 다중패러다임 프로그래밍 언어(multiparadigm programming language)라고 불린다. 절차적, 객체지향적, 함수식, 일반화 프로그래밍을 포함해 메타프로그래밍 개념까지 지원한다. 

위와 같은 유연함 덕분에 C++는 소프트웨어 개발에 매우 적절하다. 하지만 사용하기까지 C++의 이해도는 높아야 한다. C++를 조금 더 잘 이해하기 위해 단일 언어로 바라보는 것 뿐만 아니라, 여러 언어들의 연합체로 보면 된다. 여러 언어들은 아래의 네 가지라고 말할 수 있다.

 

 - C

 - 객체지향 개념의 C++

 - 템플릿 C++

 - STL

 

여기서 효과적인 프로그램 개발을 위해 한 하위 언어에서 다른 하위 언어로 옮겨가면서 대응 전략을 바꾸어야 하는 상황에도 당황하지 말아야 한다. 

그리고 공부를 시작하기 전 C++는 하위 언어들로 구성되어 있다는 점을 새기고 진행해야 한다. 

Item 2 : #define을 쓰려거든 const, enum, inline을 떠올리자

#define ASPECT_RATIO 1.653

우리는 ASPECT_RATIO가 symbolic name으로 보이지만 컴파일러에겐 전혀 보이지 않는다. 소스 코드가 컴파일러에게 넘어가기 전에 선행 처리자가 밀어버리고 숫자 상수로 바꾸어버리기 때문이다. 그 결과 위의 이름은 컴파일러가 쓰는 기호테이블에 들어가지 않는다. 따라서 오류가 발생했을 때 위의 정의된 이름이 아닌, 1.653으로 나와 있을 것이다. 

 

위의 문제의 해결법은 아래의 상수를 대신 사용하는 것이다.

const double AspectRatio = 1.653;

AspectRatio는 언어 차원에서 지원하는 상수타입 데이터이므로 당연히 컴파일러 눈에도 보이고 기호 테이블에도 저장된다. #define을 썼을 때보다 최종 코드의 크기가 더 적게나온다. #define은 여러번 쓰였을 때 1.653의 사본이 여러 번 들어가지만, 상수타입은 여러번 쓰였을 때 사본은 딱 하나만 생긴다. 

 

위의 과정과 같이 #define을 상수로 교체할 때 두 가지 경우만 조심하면 된다.

 

 1. 상수 포인터(constant pointer)를 정의

상수 정의는 대게 헤더 파일에 넣는 것이 상례이므로 포인터는 꼭 const로 선언해주고, 포인터가 가리키는 대상까지 const로 선언하는 것이 보통이다. 

const char * const authorName = "Scott Meyers";

헤더 파일 안에 char * 기반 문자열 상수를 정의하면 위와 같이 작성해야 한다. char * 기반의 문자열 보다는 string 객체가 대체적으로 사용하기 좋으므로 아래의 방법처럼 정의하는 것이 더 좋다.

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

 

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

어떤 상수의 유효 범위를 클래스로 한정하고자 할때 멤버로 만들 상수의 사본 개수가 한 개를 초과하지 않게 하고 싶다면 static으로 생성해야 한다.

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

이 때 NumTurns는 선언(declaration)된 것이다. 보통 사용하고자 하는 것에 대해서는 정의가 필요하지만 정적 멤버로 만들어지는 정수 타입의 클래스 내부 상수는 예외이다. 주소를 취하지 않는 한 선언만 진행해도 문제없다.

단, 클래스 상수의 주소를 구하거나 주소를 구하지 않는데도 컴파일러가 잘못 만들어진 관계로 정의를 달라고 하는 경우에는 별도로 정의를 제공해야 한다.

const int GamePlayer::NumTurns; //NumTurns의 정의

이 때 클래스 상수 정의는 구현 파일에 둬야한다. 정의에는 상수 초기값이 있으면 안되는데, 클래스 상수의 초기값은 해당 상수가 선언된 시점에서 바로 주어지기 때문이다. 

 

조금 오래된 컴파일러는 위 문법을 받아들이지 않는 경우가 존재한다. 정적 클래스 멤버가 선언된 시점에 초기값을 주는 것이 대개 맞지 않다고 판단하기 때문이다. 

class CostEstimate{
private:
    static const double FudgeFactor; //정적 클래스 상수의 선언, 헤더 파일에 두면 됨
    ...
};

const double CostEstimate::FudgeFactor = 1.35f; //정적 클래스 상수의 정의, 구현 파일에 두면 됨

그럴 경우는 위의 코드로 대처하면 된다. 한 가지 예외가 있다면 해당 클래스를 컴파일하는 도중 클래스 상수의 값이 필요할 때이다. 정수 타입의 정적 클래스 상수에 대한 클래스 내 초기화를 금지하는 구식 컴파일러에 대한 배려로서 방법을 추천한다면 나열자 둔갑술(enum hack)이 있다. 이 기법의 원리는 enumerator 타입의 값은 int가 놓일 곳에도 쓸수 있다는 C++의 진실을 적극 활용하는 것이다. 

class GamePlayer{
private:
    enum { NumTurns = 5 };
    
    int scores[NumTurns];
    ...
};

이 나열자 둔갑술을 알아두는 것이 여러 가지 이유로 도움이 될 것이다.

 

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

선언한 정수 상수를 가지고 다른 사람이 주소를 얻는다든지, 참조자를 쓴다든지 하는 것이 싫으면 enum은 좋은 방법이 될 것이다. 또한 제대로 만들어진 컴파일러는 const 객체에 대해 저장공간을 준비하지 않겠지만, 덜 만들어진 컴파일러는 반대로 동작할 수 있으므로 양쪽 모두에 대해 안전하게 const 객체에 대한 메모리를 만들지 않는 방법을 쓰고싶을 것이다. enum은 #define처럼 어떤 형태의 쓸데없는 메모리 할당도 절대 저지르지 않는다.

 

 2. 상당히 많은 코드에 이 기법이 쓰이고 있으므로, 혹시 발견하면 쉽게 알아보도록 눈을 단련 시켜야 한다.

 상당히 많은 경우에서 발견되는 #define 지시자의 다른 오용 사례는 매크로 함수이다. 함수처럼 보이지만, 함수 호출 오버헤드를 일으키지 않는 매크로를 구현하는 것이다. 

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

위의 매크로는 단점이 많은 코드이다. 이러한 매크로를 작성할 때는 매크로 본문에 들어있는 인자마자 반드시 괄호를 씌워주어야 한다. 이게 안되어 있으면, 표현식을 매크로로 넘길 때 문제가 발생할 수 있다. 

int a = 5, b = 0;

CALL_WITH_MAX(++a ,b);
CALL_WITH_MAX(++a, b+10);

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

C++에서는 함수 호출을 없애준다는 명목에 진행되는 이러한 상황을 굳이 만들 필요가 없다. 기존 매크로 효율을 그대로 유지하고, 정규 함수의 모든 동작 방식 및 타입 안전성까지 완벽히 취할 수 있는 방법이 존재한다. 아래의 코드처럼 인라인 함수에 대한 템플릿을 준비하면 된다. 

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

이 함수는 템플릿이므로 동일 계열 함수군(family of functions)을 만들어 낸다. 동일한 타입의 객체 두 개를 인자로 받고 둘 중 큰 것을 f에 넘겨 호출하는 구조이다. 함수 본문에 괄호로 할 필요 없으며, 인자를 여러번 평가하지 않는다. 그 뿐만 아니라 위의 코드는 진짜 함수이므로 유효범위 및 접근 규칙을 그대로 따라간다. 

 

 ★ 단순한 상수를 쓸 때 #define보다 const or enum을 우선 생각하자

 ★ 함수처럼 쓰이는 매크로를 만드려면, #define보다 인라인 함수를 우선으로 생각하자

'Study > C++' 카테고리의 다른 글

[Effective C++] Chapter 2 정리 - 1  (0) 2022.05.26
[Modern C++] Lvalue 및 Rvalue  (0) 2022.05.20
[Modern C++] Smart Pointer  (0) 2022.05.17
[Modern C++] 시작하기  (0) 2022.05.16
[Effective C++] Chapter 1 정리 - 2  (0) 2022.05.11