ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [Effective C++] 4장(1) - 설계 및 선언
    쾌락없는 책임 (공부)/Effetive C++ 요약본 2022. 3. 17. 13:35
    반응형
    본 카테고리는 프로텍 미디어의 '이펙티브 C++'을 보고 요약하는 카테고리입니다.

    3판을 기준으로 하며 전체 내용이 아닌 간략한 내용만을 요약하고 있습니다.

     

     

    항목 18 : 인터페이스 설계는 제대로 쓰기엔 쉽게, 엉터리로 쓰기엔 어렵게 하자

     C++에서는 함수, 클래스 템플릿 등 다 인터페이스로 이루어져 있다고 보면 되고, 사용자들을 위해서 이런 인터페이스를 잘 쓰기엔 쉽고 잘못된 경우는 경고해주는 인터페이스를 만들어주고 싶습니다.

     

     예를 들어 날짜를 나타내는 클래스가 있다고 합시다.

    class Date{
    public:
    	Date(int month, int day, int year);
    	...
    };

     여기서는 매개변수 순서가 잘못될 여지, 어이없는 수가 들어갈 경우가 있습니다. 그리고 이때 새로운 타입을 들여와 인터페이스를 강화하면 상당수의 실수를 막을 수 있습니다.

    struct Day{
    	explicit Day(int d)
    		: val(d) {}
    	
    	int val;
    };
    
    struct Month{
    	explicit Month(int m)
    		: val(m) {}
    	
    	int val;
    };
    
    struct Year{
    	explicit Year(int y)
    		: val(y) {}
    	
    	int val;
    };
    
    class Date{
    public:
    	Date(const Month& m, const Day& d, const Year& y);
    	...
    };

     일단 이렇게 하면 매개변수 전달이 잘못될 일이 없습니다. 이후 각 타입의 값에 제약을 가하면 괜찮은 경우가 됩니다. 예를 들어 '월'의 경우 12월까지만 있어서 enum을 사용해해 볼 수 있지 않을까? 하는데 이는 타입 안정성에 대해서 믿음직스럽지 못합니다. 그래서 Month의 집합을 만드는 방법이 있습니다.

    class Month{
    public:
    // 얘네가 왜 함수인지는 아래에
    	static Month Jan() {return Month(1);}
    	static Month Feb() {return Month(12);}
    ...
    	static Month Dec() {return Month(12);}
    ...
    private:
    	explicit Month(int m); // 값이 새로 생성되지 않도록 명시 호출 생성자는 private
    };
    
    Date d(Month::Jan(), ...};

     

     그리고 예상되는 사용자들의 문제를 예방하는 방법은 타입에 제약 부여를 해 타입을 통해 할 수 있는 일을 묶어 버리는 것입니다. (예시로 const로 변수들을 묶으면 a*b = c 같은 오타로 인한 오류(원래 == 비교)도 없어질 것입니다)

     

     

     또 하나 더 있는데 이 항목은 기본제공 타입에서도 엿볼 수 있는 부분입니다. 바로 일관된 인터페이스를 보여준다는 것이죠. STL 에서도 각 컨테이너 사이즈들을 size()로 볼 수 있다는 것도 일관성을 보여주는 사례가 됩니다. 사용자 쪽에서 뭔가를 외워야 한다면 잘 못쓰기 쉽습니다. 그 예시로 팩토리 함수를 한번 보겠습니다.

    Investment* createInvestment();

     여기서 문제가 있을 수 있는건 '포인터 삭제'인데 각 팩토리 함수마다 delete를 해 줘야 하고 이걸 계속해야 한다는 것이죠. 이를 위해서 스마트 포인터를 사용해야 하는데 이것조차 까먹으면 어떻게 될까요? 그래서 애초에 팩토리 함수가 스마트 포인터를 반환하게 하는게 좋습니다.

    std::tr1::shared_ptr<Investment> createInvestment();

     여기서 shared_ptr은 자원 해제 관련한 실수들을 봉쇄할 수 있어 인터페이스 설계자에게 좋습니다!

     

     

     

    항목 19 : 클래스 설계는 타입 설계와 똑같이 취급하자

    C++ 에서 객체 정의는 새 타입을 정의하는 것과 같으니 정성스럽게 설계를 해 줘야 합니다. 좋은 클래스는 문법이 자연스럽고, 의미구조가 직관적이며 효율적이어야 하는데 이중 한가지 이상을 하는 게 상당히 어렵습니다.

     

     그래서 [Effective C++] 에서는 여러 질문들을 준비를 했고 그래서 아래 질문들에 따라 제한하는 것들이 생기게 됩니다.

     

    Q : 새로 정의한 타입의 객체 생성 및 소멸은 어떻게 이루어져야 하는가

    A : 함수를 직접 작성할 경우 함수 설계에도 영향을 줍니다.

     

    Q : 객체 초기화는 객체 대입과 어떻게 달라야 하는가

    A : 초기화와 대입을 헷갈리지 않는게 이상적입니다.

     

    Q : 새로운 타입으로 만든 객체가 '값에 의해 전달' 되는 경우 어떤 의미를 줄 것인가

    A : 값에 의한 전달은 복사 생성자쪽입니다. 이 부분을 잘 살펴봐야겠죠.

     

    Q : 새로운 타입이 가질 수 있는 적법한 값에 대한 제약은 무엇으로 잡을 것인가

    A : 전부는 아니지만 클래스 데이터 멤버의 몇 가지 조합은 유지를 해 줘야 하며 이걸 클래스 불변속성이라고 합니다. 이 불변속성에 따라 클래스 멤버 함수 안에서 주어야 할 에러 점검 루틴이 달라집니다. 특히 생성자, 대입 연산자, 각종 쓰기 함수는 불변속성에 의해 좌우됩니다. 또한 이 불변속성은 예외 지정 부분에도 영향을 줍니다!

     

    Q : 기존 클래스 상속 계통망에 맞출 것인가

    A : 이미 가진 클래스로 구현이 된다면 그 클래스의 제약사항을 받게 되며 멤버 함수의 가상/비가상 여부가 가장 큽니다. 그리고 다른 클래스가 상속하는 기본 클래스를 만든다고 하면 이에 대한 제약사항을 가상 함수 여부로 결정해야 합니다. (특히 소멸자)

     

    Q : 어떤 종류의 타입 변환을 허용할 것인가

    A : 기존 타입들과 얽히게 되며 T1에서 T2로 암시적 변환을 하게 만들고 싶은 경우 타입 변환 함수(operator T2)를 넣거나 인자 한개로 호출될 수 있는 비명시 호출 생성자를 T2에 넣어야 합니다. 명시적 변환만 해주고 싶다면 해당 변환을 맡는 별도의 함수를 만들되 타입 변환 연산자, 비명시 호출 생성자는 만들면 안됩니다.

     

    Q : 어떤 연산자와 함수를 두어야 의미가 있을까

    A : 클래스 안에 public, protected, private에 어떤 것들을 둘까 결정해야 하는 것으로 한 클래스를 다른 클래스에 중첩시켜도 되는가? 와 같은 추가 질문이 있을 수 있습니다.

     

    Q : '선언되지 않은 인터페이스'로 무엇을 둘 것인가

    A : 만들 타입이 제공하는 보장은 어떤 종류일까에 대한 질문으로 보장할 수 있는 부분은 수행 성능 및 예외 안전성, 자원 사용 등입니다.

     

    Q : 새로 만드는 타입이 얼마나 일반적인가

    A : 타입 하나가 아닌 동일 계열 타입군을 만들 수도 있으며 만약 그렇다면 클래스보다는 새로운 클래스 템플릿을 만들어야 합니다.

     

    Q : 정말 꼭 필요한가?

    A : 기존 클래스가 아쉬워 파생 클래스를 만들 거라면 차라리 간단하게 비멤버 함수라던가 템플릿 몇 개 더 정의하는 게 좋습니다.

     

    항목 20 : '값에 의한 전달' 보다는 '상수 객체 참조자에 의한 전달' 방식을 택하는 편이 대게 낫다

    C++의 '값에 의한 전달' 방식을 사용하며 특별한 방식을 지정하지 않은 한 인자, 반환 값은 사본을 통해 초기화가 됩니다. 그리고 대게 이런 사본을 만들어 내는 게 '복사 생성자'이며 이로 인해서 값 하나 잘못 전달했다가는 상당한 고비용 연산이 될 수 있습니다. 그럼 생성자, 소멸자가 상당히 많이 불리게 되겠죠.

    bool validateSutdent(const Student& s);

     이렇게 하면 참조로 보내기 때문에 엄청 효율적으로 변하게 됩니다. 또한 const를 붙여 기존 객체가 변할 가능성을 없애 주었죠.

     

     또한 참조 전달의 경우 복사 손실 문제도 없어진다는 장점이 있습니다.(슬라이스 문제라고도 합니다) 만약 값에 의한 전달을 하면 파생 클래스 부분들이 잘려서 오게 될 수 있습니다.

    class Window{...};
    class WindowWithScrollBars: public Window{...};
    
    // 이제 잘리지 않습니다!
    void printNameAndDisplay(const Dindow& w);

     그래서 참조를 통해 전달을 하면 이 부분들이 파생 클래스 부분으로 남게 되죠.

     

     일단 C++에서 참조자 사용 방식은 포인터 사용 방식으로 다른 객체들은 모르겠지만 기본 제공 타입은 값 전달이 더 빠릅니다. 그리고 이 점은 STL의 반복자와 함수 객체에도 마찬가지입니다.

     

     그리고 단순 객체 크기가 작다고 값 전달을 하기는 좀 그런 게 '크기가 작다 != 복사 생성자 호출이 저비용이다'는 아니기 때문이죠. 여기서도 크기도 작고 비용도 저비용이라도 수행 성능이 발목을 잡게 됩니다. 컴파일러 중에서 기본 타입과 사용자 정의 타입을 다르게 취급하는 것들이 있습니다. 예를 들어 double 기본은 레지스터에 넣지만 객체의 double은 넣어주지 않는다는 등 이런 환경에서는 참조에 의한 전달이 좋습니다.

     

     또 값의 크기가 작다고 값에 의한 전달을 하기 꺼려지는 이유가 있습니다. 사용자 정의 타입 크기는 언제나 커질 수 있다로 string 같은걸 생각하면 됩니다. 그래서 '값에 의한 전달'을 하기 좋은 3가지를 모아 보면

     

    - 기본 제공 타입

    - STL 반복자

    - 함수 객체 타입

     

    이 있습니다.

    반응형

    댓글

Designed by Tistory.