본문 바로가기
프로그래밍언어/C++

[Effective C++] 항목 38~40

by 목가 2018. 4. 10.
반응형

항목 38. "has-s(...는...를 가짐)" 혹은 "is-implemented-in-terms-of(...는..를 써서 구현됨)"를 모형화할 때는 객체 합성을 사용하자


합성이란?

- 포함된 객체들을 모아서 이들을 포함한 다른 객체를 합성한다는 뜻.


class Adress {...};  //누군가의 거주지

class PhoneNumber{...};

class Person{

    public:

    ...

    private:

        std::string name;                         //이 클래스를 이루는 객체 중 하나

        Adress adreess;                         //마찬가지

        PhoneNumber voiceNumber;         //역시 마찬가지

        PhoneNumber faxNumber;            //이것도 마찬가지

};


Person 객체는 string, Address, PhoneNumber 객체로 이루어져 있습니다.


개발자들 사이에서는 합성을 대신해서 레이어링(layering), 통합(aggregation), 내장(embedding) 등으로도 쓰입니다.


응용영역(application domain) - 일상생활에서 볼 수 있는 사물을 본 뜬 것으로 사람, 이동수단, 비디오 프레임등.

                                            has-a의 관계

구현영역(implementation domain) - 응용 영역에 속하지 않는 나머지, 버퍼, 뮤텍스, 탐색 트리 등 순수하게 시스템 구현만을

                                                  위한 인공물.

                                                  is-implemented-in-terms-of의 관계

따라서 위의 예제는 has-a의 관계입니다.


template<typename T>               //Set을 만든답시고 list를 잘못 쓰는 방법

class Set:public std::list<T>{...};


위에서 잘못된 내용은 list 객체는 중복 원소를 가질 수 있는 컨테이너라는 것입니다.

3051이라는 값이 list<int>에 두번 삽입되면, 3051의 사본 두개를 품게 됩니다.

이와 반대로 Set 객체는 원소가 중복되면 안됩니다. 따라서 Set이 list의 일종(is-a)이라는 명제는 참이 아닙니다.


template<class T>                //Set을 만드는데 list를 제대로 쓰는 방법

class Set{

    public:

        bool member(const T& item) const;

        void insert(const T& item);

        void remove(const T& item);

        std::size_t size() const;

    private:

        std::list<T> rep;              //Set  데이터의 내부 표현부

};


실제 구현은 아래와 같습니다.

template<typename T>

bool Set<T>::member(const T& item) const

{

    return std::find(rep.begin(), rep.end(), item) != rep.end();

}


template<typename T>

void Set<T>::insert(const T& item)

{

    if(!member(item)) rep.push_back(item);

}


template<typename T>

void Set<T>::remove(const T& item)

{

    typename std::list<T>::iterator it = std::fine(rep.begin(), rep.end(), item);      //여기에 나온 "typename"에 대한 이야기는

                                                                                                                  항목 42에서 하겠습니다.

    if(it != rep.end()) rep.erase(it);

}


template<typename T>

std::size_t Set<T>::size() const

{

    return rep.size();

}


이 관계는 is-a가 아닌 is-implemented-in-trems-of 입니다.


이것만은 잊지말자!

*객체 합성(composition)의 의미는 public 상속이 가진 의미와 완전히 다릅니다.

*응용 영역에서 객체 합성의 의미는 has-a(...는...를 가짐)입니다. 구현 영역에서는 is-implemented-in-terms-of(...는...를 써서 구현됨)의 의미를 갖습니다.



항목 39. private 상속은 심사숙고해서 구사하자


항목 32에서의 Student와 Person의 public 상속을 private로 변경하면 다음과 같습니다.

class Person {...};

class Student:private Person {...};            //이젠 private 상속입니다.

void eat(const Person& p);                      //누구라도 사람은 먹을 수 있습니다.

void study(const Student& s);                  //공부는 학생만 할 수 있습니다.

Person p;                                              //p는 Person의 일종입니다.

Student s;                                             //s는 Student의 일종입니다.

eat(p);                                                  //좋습니다. p는 Person의 일종이니까요.

eat(s);                                                  //에러!Student는 Person의 일종이 아닙니다.


클래스 상속 관계가 private이면

첫번째로 컴파일러는 일반적으로 파생 클래스 객체(이를테면 Student)를 기본 클래스 개체(그러니까 Person)로 변환하지 않습니다.(eat함수 호출이 s에 대해서 실패했던 이유)

두번째로 기본 클래스로부터 물려받은 멤버는 파생 클래스에서 모조리 private 멤버가 됩니다.


private 상속의 의미는 '구현만 물려받을 수 있다. 인터페이스는 국물도 없다.'


할 수 있으면 객체 합성을 사용하고, 꼭 해야 하면 private 상속을 사용.

(꼭 해야할 때란, 비공개 멤버를 접근할 때 혹은 가상 함수를 재정의할 경우)


class Timer{

    public:

        explict Timer(int tickFrequency);

        virtual void onTick() const;                     //일정 시간이 경과할 때마다 자동으로 이것이 호출됩니다.

        ...

};


class Widget: private Timer{

    private:

        virtual void onTick() const;                //Widget 사용 자료 등을 수집합니다.

        ....

};

private로 선언하였기 때문에 Timer의 onTick함수가 Widget에서는 private 멤버가 되었습니다.

위의 내용을 객체 합성으로 표현하면 아래와 같습니다.


class Widget{

    private:

        class WidgetTimer: public Timer {

            public:

                virtual void onTick() const;

                 ....

         };

    WidgetTimer timer;

    ....

};

위와 같이 private 상속 대신에 public 상속에 객체 합성 조합이 더 자주 사용되는데 이유는 아래와 같습니다.

첫째, Widget 클래스를 설계하는 데 있어서 파생은 가능하게 하되, 파생 클래스에서 onTick 을 재정의 할 수 없도록 설계차원에서 막고 싶을 때 유용.

둘째, Widget의 컴파일 의존성을 최소화하고 싶을 때 유용.


private 상속을 선호할 수밖에 없는 경우는 데이터가 전혀없는 클래스를 사용할 때 입니다.

공백 클래스는 개념적으로 차지하는 메모리 공간이 없는게 맞습니다. 하지만 기술적인 우여곡절 때문에 C++에는 "독립구조 의 객체는 반드시 크기가 0을 넘어야 한다."라는 규칙이 정해져 있습니다.

class Empty{};                      //정의된 데이터가 없으므로, 객체는 메모리를 사용하지 말아야 합니다.

class HoldsAnInt{                 //int를 저장할 공간만 필요해야 합니다.

    private:

        int x;

        Empty e;                      //메모리 요구가 없어야 합니다.

};

하지만 sizeof(HoldsAnInt) > sizeof(int) 가 됩니다. 대부분의 컴파일러에서는 sizeof(Emtpy)가 1로 나오기 때문입니다.

위의 소스를 아래와 같이 바꾸어 봅니다.

class HoldsAnInt: private Empty{

    private:

        int x;

};

이렇게 변경하면 sizeof(HoldsAnInt) == sizeof(int)가 되는데 이러한 기법을 공백 기본 클래스 최적화(empty base optimiztion:EBO)라고 합니다. (EBO는 단일상속에서만 적용)


이것만은 잊지말자!

*private 상속의 의미는 is-implemented-in-terms-of(...는...를 써서 구현됨)입니다. 대개 객체 합성과 비교해서 쓰이는 분야가 많지는 않지만, 파생 클래스 쪽에서 기본 클래스의 protected 멤버에 접근해야 할 경우 혹은 상속받은 가상 함수를 재정의해야 할 경우에는 private 상속이 나름대로 의미가 있습니다.

*객체 합성과 달리, private 상속은 공백 기본 클래스 최적화(EBO)를 활성화 시킬 수 있습니다. 이 점은 객체 크기를 가지고 고민하는 라이브러리 개발자에게 꽤 매력적인 특징이 되기도 합니다.



항목 40. 다중상속은 심사숙고해서 사용하자.


다중상속(multiple inheritance : MI), 단일 상속(single inheritance : SI)

다중상속에서는 둘 이상의 기본 클래스로부터 똑같은 이름(이를테면 함수, typedef 등)을 물려받을 가능성이 생겨 버립니다. 이러한 경우 모호성이 발생하게 됩니다.


class BorrowableItem{                   //라이브러리로부터 여러분이 가져올 수 있는 어떤 것

    public:

        void checkOut();                   //라이브러리로부터 체크아웃합니다.

        ...

};


class ElectronicGadget{

    private:

        bool checkOut() const;          //자체 테스트를 실시하고, 성공여부를 반환합니다.

        ......

};


class MP3Player:                        //여기서 MI가 됩니다. (MP3 플레이어를 위해 몇몇 라이브러리로부터 기능을 가져옵니다.)

    public BorrowableItem,

    public ElectronicGadget

{...};                                         //지금 이클래스가 어떻게 정의됐는가는 중요하지 않습니다.


MP3Player mp;


mp.checkOut();                         //모호성 발생! 대관절 어느 checkOut 이란 말씀?


C++ 컴파일러는 최적 일치 함수를 찾은 후에 함수의 접근 가능성을 점검합니다.

하지만 위의 함수에서 checkOut은 C++ 규칙에 의한 일치도가 같아서 최적 일치 함수가 결정되지 않습니다.


위의 예제에서 모호성을 해소하려면, 호출할 기본 클래스의 함수를 손수 지정해 주어야 합니다.

mp.BorrowableItem::checkOut();  과 같이.


다중상속은 상위 단계의 기본 클래스를 여러 개 갖는 클래스 계통에서 심심치 않게 눈에 띕니다. 이런 구조에서는 소위 "죽음의 마름모꼴" 이라는 모양이 나올 수 있습니다.


class File{...};

class InputFile: public File{...};

class OutputFile: public File{...};

class IOFile: public InputFile, public OutputFile {....};


위의 예제에서 File 클래스 안에 fileName이라는 데이터 멤버가 있을 때 IOFile 클래스에는 이 필드가 몇개가 들어있을까요?

C++에서는 하나가 들어가있을수도있고 두개가 들어가 있을수도 있습니다. (애매모호한 입장 - 기본적으로 중복생성 하는쪽)


만약 중복 생성을 원하는 것이 아니였다면, 해당 데이터를 가진 클래스(이를테면 File)를 가상 기본 클래스로 만들면 해결됩니다.

class File{...};

class InputFile: virtual File{...};

class OutputFile: virtual File{...};

class IOFile: public InputFile, public OutputFile {....};


표준 C++ 라이브러리에 MI모양의 상속 계통이 있습니다. basic_ios, basic_istream, basic_ostream, basic_iostream이고 각각 File, InputFile, OutputFile, IOFile 자리에 들어가면 됩니다.


하지만, 첫째 구태여 쓸 필요가 없으면 가상 기본 클래스를 사용하지 마세요.

           둘째 가상 기본 클래스를 정말 쓰지 않으면 안될 상황이라면, 가상 기본 클래스에는 데이터를 넣지 않는 쪽으로

                  최대한 신경을 써주세요.


C++ 인터페이스 클래스를 써서 사람을 모형화해 보도록 합니다.

class IPerson{

    public:

        virtual ~IPerson();

        virtual std::string() name() const = 0;

        virtual std::string() birthDate() const = 0;

};


IPerson의 구체 파생 클래스를 인스턴스로 만듭니다.


std::tr1::shared_ptr<IPerson> makePerson(DatabaseID personIdentifier);  //사용자로부터 데이터베이스 ID를 얻어내는 함수

DatabaseID askUserForDatabaseID();

DatabaseID id(askUserForDatabaseID());

std::tr1:shared_ptr<IPerson>pp(makePerson(id)); //IPerson 인터페이스를 지원하는 객체를 하나 만들고 pp로 가리키게

                                                                         합니다. 이후에는 *pp의 조작을 위해 IPerson의 멤버 함수를 사용합니다.

makePerson 함수가 인스턴스로 만들 수 있는 구체 클래스가 IPerson으로부터 파생되어 있어야 할 것.


class PersonInfo{

    public:

        explicit PersonInfo(DatabaseID pid);

        virtual ~PersonInfo();

        virtual const char * theName() const;

        virtual const char * theBirthDate() const;

        ....

    private:

        virtual const char * valueDelimOpen() const;

        virtual const char * valueDelimClose() const;

        ...

};


위의 예제를 보면 각 필드값의 시작과 끝을 임의의 무자열로 구분하여 출력할 수 있도록 되어 있는데 아래의 예제를 봐 주시기 바랍니다.


const char * PersonInfo::valueDelimOpen() const

{

    return "[";                  // 기본적으로 지정된 시작 구분자

}

const char * PersonInfo::valueDelimClose() const

{

    return "]";                 // 기본적으로 지정된 끝 구분자

}

const char * PersonInfo::theName() const

{

    //반환 값을 위한 버퍼를 예약해 둡니다. 이 버퍼는 정적 메모리이기 때문에, 자동으로 0으로 초기화 됩니다.

    static char value[Max_Formatted_Field_Value_Length];

   

    std::strcpy(value, valueDelimOpen());  //시작 구분자를 value에 씁니다.

    //value에 들어있는 문자열에 이 객체의 name 필드를 덧붙입니다.(버퍼 오버런이 일어나지 않도록 주의)

    std::strcat(value, valueDelimClose());  //끝 구분자를 value에 씁니다.


    return value;

}


여기서 valueDelimOpen 및 valueDelimClose는 가상함수이기 때문에, theName이 반환하는 결과는 PersonInfo에만 좌우되는 것이 아니라 PersonInfo로부터 파생된 클래스에도 좌우됩니다.


다중상속을 의미있게 써먹는 예 - 인터페이스의 public 상속과 구현의 private 상속을 조합

class IPerson{                   //이 클래스가 나타내는 것은 용도에 따라 구현될 인터페이스 입니다.

    public:

        virtual ~IPerson();

        virtual std::string name() const = 0;

        virtual std::string birthDate() const = 0;

};


class DatabaseID{...};        //아래에서 쓰입니다. 더이상의 자세한 내용은 넘어가도 됩니다.


class PersonInfo{              // 이 클래스에는 IPerson 인터페이스를 구현하는 데 유용한 함수가 들어 있습니다.

   public:

       explicit PersonInfo(DatabaseID pid);

       virtual ~PersonInfo();

       virtual const char * theName() const;

       virtual const char * theBirthDate() const;

       virtual const char * theDelimOpen() const;

       virtual const char * theDelimClose() const;

       ...

};


class CPerson: public IPerson, private PersonInfo{                              //MI가 쓰임

    public:

        explicit CPerson(DatabaseID pid): PersonInfo(pid) {}

        virtual std::string name() const {       //IPerson 클래스의 순수 가상 함수에 대해 파생 클래스의 구현을 제공

            return PersonInfo::theName();

        }

        virtual std::string birthDate() const{

            return PersonInfo::theBirthDate();

        }

    private:       //구분자와 관련된 가상함수들도 상속되므로 이 함수들에 대한 재정의 버전을 만듭니다.

        const char * valueDelimOpen() const {return "";}

        const char * valueDelimClose() const {return "";}

};


결론 : MI 보다는 SI쪽으로 가는것이 좋습니다. 웬만한 경우에는 SI로 할 수 있는 방법이 있습니다.


이것만은 잊지 말자!

* 다중 상속은 단일 상속보다 확실히 복잡해집니다. 새로운 모호성 문제를 일으킬 뿐만 아니라 가상 상속이 필요해질 수도 있습니다.

* 가상 상속을 쓰면 크기 비용, 속도 비용이 늘어나며, 초기화 및 대입 연산의 복잡도가 커집니다. 따라서 가상 기본 클래스에는 데이터를 두지 않는것이 현실적으로 가장 실용적입니다.

* 다중 상속을 적법하게 쓸 수 있는 경우가 있습니다. 여러 시나리오 중 하나는, 인터페이스 클래스로부터 public 상속을 시킴과 동시에 구현을 돕는 클래스로부터 private 상속을 시키는 것입니다.



반응형

'프로그래밍언어 > C++' 카테고리의 다른 글

[Effective C++] 항목 44~46  (0) 2018.04.10
[Effective C++] 항목 41~43  (0) 2018.04.10
[Effective C++] 항목 35~37  (0) 2018.04.10
[Effective C++] 항목 29~31  (0) 2018.04.10
[Effective C++] 항목 26~28  (0) 2018.04.10

댓글