{{ label!='' ? 'Label : ' : (q!='' ? '검색 : ' : '전체 게시글') }} {{ label }} {{ q }} {{ ('('+(pubs|date:'yyyy-MM')+')') }}

[C++] 스터디 CPPALOM 15주차: Effective C++ Item 36~45

파워 포인트 파일(.pptx)을 Markdown으로 변환하여 업로드하였음)

# <br>CPPALOM

# <br>15주차 – Effective C++ item 36~45
한수빈

# <br>항목36: 상속받은 비가상 함수를 파생 클래스에서 재정의하는 것은 절대 금물!

* 비가상 함수는 정적으로 바인딩된다.
  * 다음 코드는 같은 이름임에도 불구하고 다른 함수가 호출된다!
* 비가상 함수를 오버라이딩하는 것은 객체지향적으로 적절하지도 않으며, 오류가 날 여지가 많다!
  * 따라서 상속받은 비가상 함수를 재정의하는 일은 절대로 없도록 하자.

```C++
class D : public B
{
public:
    void mf();
    ...
};
pB->mf();
pD->mf();
```

# <br>항목37: 어떤 함수에 대해서도 상속받은 기본 매개변수 값은 절대로 재정의하지 말자

* 다음 코드는 문제의 여지가 많다!
  * 어떤 타입의 포인터인지에 따라서 기본 매개변수의 값이 변하게 된다.
* 따라서 상속받은 기본 매개변수 값은 절대로 재정의해서는 안 된다.
  * 기본 매개변수 값은 정적으로 바인딩되기 때문이다.

```C++
class Shape
{
public:
    enum ShapeColor { Red, Green, Blue };
    virtual void draw(ShapeColor color = Red) const = 0;
};
class Rectangle : public Shape 
{
public:
    virtual void draw(ShapeColor color = Green) const;
};
Shape* pr = new Rectangle();
pr->draw() // Calls Rectangle::draw(Shape::Red)!
```

* 기본 매개변수가 있는 가상 함수를 만드는 방법?
  * NVI 관용구

```C++
class Shape
{
public:
    enum ShapeColor { Red, Green, Blue };
    void draw(ShapeColor color = Red) const
    {
        doDraw(color);
    }
private:
    virtual void doDraw(ShapeColor color) const = 0;
};
class Rectangle : public Shape 
{
public:
    virtual void doDraw(ShapeColor color) const;
};
```

# <br>항목38: “has-a” 혹은 “is-implemented-in-terms-of”를 모형화할 때는 객체 합성을 사용하자

* 합성이란: A 객체가 B 객체를 갖는 것.
  * Set을 구현하기 위해서 List를 사용하고 싶다.
  * 이 때, 상속을 이용해서 구현한다면 리스코프 치환 원칙에 위배된다.
* 따라서, 객체 합성을 이용해서 구현한다!

```C++
template<class T>
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;
};
```

# <br>항목39: private 상속은 심사숙고해서 구사하자

* private 상속은 is-a를 의미하지 않는다.
  * 자식 클래스는 private 상속한 객체로 변환될 수도 없다.
* 대신, is-implemented-in-terms-of를 의미한다!
* 상황
  * Timer 라이브러리를 활용해 Widget이 특정 주기마다 행위하게 하고 싶다.
  * private 상속을 이용해서 onTick()을 오버라이딩할 수 있다.
  * 혹은 객체 합성을 이용해서 구현한다.
* 객체 합성의 장점
  * 상속으로 인한 의도치 않은 오버라이딩을 막을 수 있다.
    * onTick()이 재정의될 가능성을 막을 수 있다.
  * 컴파일 의존성이 최소화된다.

```C++
class Timer
{
public:
    explicit Timer(int tickFrequency);
    virtual void onTick() const;
};
class Widget : private Timer
{
private:
    virtual void onTick() const;
}
class Widget 
{
private:
    class WidgetTimer : public Timer 
    {
    public:
        virtual void onTick() const;
    };
    WidgetTimer timer;
}
```

* 공백 기본 클래스 최적화
  * 기본적으로 객체는 최소한의 메모리를 할당받는다.
  * 심지어, 변수가 없고 함수만 존재해도 그렇다!
* 위의 HoldsAnInt 클래스는 함수만 존재하는 Empty 객체를 소유하기 위해 추가적인 메모리를 소모한다.
* 반면, Empty를 private 상속한 경우 별도의 메모리 공간을 차지하지 않아 최적화를 할 수 있다.

```C++
class HoldsAnInt
{
private:
    int x;
    Empty e;
};
class HoldsAnInt : private Empty
{
private:
    int x;
};
```

# <br>항목40: 다중 상속은 심사숙고해서 사용하자

* 다중 상속은 단일 상속보다 복잡하며, 새로운 모호성 문제를 일으킬 여지가 있다.
  * “죽음의 MI 마름모꼴"이 나타나는 경우 별로 좋지 않다.
  * 다만, 가상 상속으로 어느정도 해결할 수 있다.
  * 가상 상속 사용 시 크기 비용, 속도 비용이 늘어나며 초기화 및 대입 연산의 복잡도가 커진다.
* 다중 상속을 적법하게 쓸 수 있는 경우가 있다.
  * 비가상 함수 및 멤버 변수가 없는 인터페이스 클래스로부터 상속을 받는 경우이다.
    * 여러 인터페이스를 public 상속하여도 문제가 발생할 여지가 적다.
  * 인터페이스를 public 상속 받으면서, 동시에 구현을 위한 private 상속을 받는 경우 적절할 수 있다.

# <br>항목41: 템플릿 프로그래밍의 천릿길도 암시적 인터페이스와 컴파일 타임 다형성부터

* 템플릿 프로그래밍의 경우 암시적 인터페이스와 컴파일 타임 다형성이 중심이 된다.
* w가 지원해야 하는 인터페이스는 템플릿 안에서 w에 대해 실행되는 연산이 결정한다.
  * w는 size, normalize, swap, 복사 생성자, 부등 비교를 위한 연산을 제공해야 한다.
* w가 수반되는 함수 호출이 일어날 때, 템플릿의 인스턴스화가 일어나며 이는 컴파일 시기에 이루어진다.
  * 즉, T타입 w로부터 호출되는 함수의 결정은 컴파일 시기에 이미 전부 결정 된다.

```C++
template<typename T>
void doProcessing(T& w)
{
    if (w.size() > 10 && w != someNastyWidget)
    {
        T temp(w);
        temp.normalize();
        temp.swap(w);
    }
}
```

* 극단적으로, 다음과 같은 경우도 가능하다.
* w.size()는 정수 값을 반환할 필요가 없다.
  * X라는 타입을 반환하고, operator>가 X와 정수 사이의 연산을 지원하거나, X가 적절히 암시적 변환이 가능하기만 하면 된다.
* 표현식 w.size() > 10 && w != someNastyWidget이 bool 값을 반환하기만 하면 된다.
* 수빈의 첨언:
  * 컴파일 시기에 T 타입을 X 타입으로 대입했을 때, 말그대로 컴파일 에러만 나지 않으면 된다.

```C++
template<typename T>
void doProcessing(T& w)
{
    if (w.size() > 10 && w != someNastyWidget)
    {
        T temp(w);
        temp.normalize();
        temp.swap(w);
    }
}
```

# <br>항목42: typename의 두 가지 의미를 제대로 파악하자

* template에서 class와 typename 키워드는 차이가 없다.
  * 하지만 중첩 의존 타입 이름을 활용할 때에는 반드시 typename 키워드를 붙여주어야 한다.
  * 템플릿 매개변수에 따라서 달라지는 타입을 중첩 의존 타입 이름이라고 한다.
* 문제는, 중첩 의존 이름은 모호하다.
  * C::const_iterator가 C 클래스의 정적 데이터 멤버일 수도 있다.
  * x는 전역 변수의 이름일 수도 있다.
  * 위 코드는 C::const_iterator와 x를 곱하는 코드일 수도 있다.
* 이러한 모호성에 의해, 컴파일러는 중첩 의존 이름이 나온다면 타입이 아니라고 가정하게 된다.

```C++
template<typename C>
void print2nd(const C& container)
{
    C::const_iterator* x;
    ...
}
```

따라서 중첩 이름을 활용할 때에는 반드시 typename을 붙이자!

그 외의 경우에는 typename을 사용하면 안 된다.

```C++
template<typename C>
void print2nd(const C& container)
{
    if(container.size() >= 2)
    {
        typename C::const_iterator iter(container.begin());
        ++iter;
        int value = *iter;
        std::cout << value;
    }
}
```

```C++
template<typeenamea C>          // class와 같은 의미
void f(const C& container,      // typename 쓰면 안 됨
    typename C::iterator iter); // typename 써야 함
```

* 단, 예외가 있다.
* 수빈의 첨언:
  * 중첩 이름 타입을 통해 변수를 선언하는 경우에만 typename을 선언해주도록 하자.

```C++
template<typename T>
class Derived : public Base<T>::Nested  // 상속되는 기본 클래스 리스트:
{                                       // typename 쓰면 안 됨
public:
    explicit Derived(int x)             // 멤버 초기화 리스트에 있는 기본
        : Base<T>::Nested(x)            // 클래스 식별자: typename 쓰면 안 됨
    {
        typename Base<T>::Nested temp;  // 중첩 의존 타입 이름
    }
}
```

# <br>항목43: 템플릿으로 만들어진 기본 클래스 안의 이름에 접근하는 방법을 알아두자

* 템플릿으로 만들어진 기본 클래스 안의 이름에는 일반적으로 접근할 수 없다.
* 템플릿으로 만들어진 클래스를 상속받을 때, 해당 클래스가 어떤 함수를 갖고 있을지는 전혀 예측할 수 없다.
  * 특수화 때문에 언제든지 바뀔 수 있다!
* 따라서, 컴파일러는 템플릿으로 만들어진 클래스로부터 어떤 함수도 있다고 가정하지 않는다.

```C++
template<typename Company>
class MsgSender
{
public:
    ...
    void sendClear(const MsgInfo& info)
    {
        std::string msg;
        Company c;
        c.sendCleartext(msg);
    }
    ...
}
```

```C++
template<typename Company>
class LoggingMsgSender : public MsgSender<Company>
{
public:
    void sendClearMsg(const MsgInfo& info)
    {
        // log
        sendClear(info);    // error
        // log
    }
}
```

다음과 같은 방식으로 해결할 수 있다:

마지막 방법은 추천하지 않는데, sendClear()가 가상 함수일 시 제대로 동작하지 않을 여지가 존재하기 때문이다.

```C++
template<typename Company>
class LoggingMsgSender : public MsgSender<Company>
{
public:
    void sendClearMsg(const MsgInfo& info)
    {
        // log
        this->sendClear(info);    // OK.
        // log
    }
}
```

```C++
template<typename Company>
class LoggingMsgSender : public MsgSender<Company>
{
public:
    using MsgSender<Company>::sendClear;
    void sendClearMsg(const MsgInfo& info)
    {
        // log
        sendClear(info);    // OK.
        // log
    }
}
```

```C++
template<typename Company>
class LoggingMsgSender : public MsgSender<Company>
{
public:
    void sendClearMsg(const MsgInfo& info)
    {
        // log
        MsgSender<Company>::sendClear(info); // OK.
        // log
    }
}
```

# <br>항목44: 매개변수에 독립적인 코드는 템플릿으로부터 분리시키자

* 템플릿 매개변수가 다르면 전부 다른 타입으로 인식한다.
  * 만약 같은 행위를 하는 함수가 있더라도, 별도의 함수로 생성되며 이는 코드 비대화를 유발한다.
* sm1.invert()는 5x5 행렬에서 동작하는 invert() 함수가 생성되며, sm2.invert()는 10x10 행렬에서 동작하는 invert() 함수가 생성된다.
* 이러한 이유로, 코드 비대화가 이루어진다.
* 수빈의 첨언:
  * i<5까지 도는 for문과 i<10까지 도는 for문이 각각 invert() 함수에 하드코딩 된다고 상상하자!

```C++
template<typename T, std::size_t n>
class SquareMatrix
{
public:
    ...
    void invert();
};
SquareMatrix<double, 5> sm1;
sm1.invert();
SquareMatrix<double 10> sm2;
sm2.invert();
```

* 따라서 템플릿 매개변수로 받은 n과 인라인 호출을 이용해 부모에게 넘길 수 있다.
  * 부모는 같은 타입이라면(가령 double) 서로 다른 크기에서도 단 하나의 함수로 잘 동작할 수 있다.
* 하지만 이 경우 부모가 자식의 데이터를 알아야 한다는 문제가 발생한다.
  * 부모는 invert를 수행하기 위해서 자식의 데이터에 접근해야 한다.

```C++
template<typename T>
class SquareMatrixBase
{
protected:
    void invert(std::size_t matrixSize);
};
template<typename T, std::size_t n>
class SquareMatrix : private SquareMatrixBase<T>
{
public:
    ...
    void invert() { this->invert(n); } 
};
```

* 따라서 부모에게 해당하는 데이터 포인터를 넘기는 식으로 해결할 수 있다.
* 이러한 방식은 분명 비용이 존재한다!
  * 포인터에 대한 비용, 캡슐화, 자원 관리 등 다양한 문제가 발생할 수 있으며 이러한 설계 결정은 오롯이 개발자의 몫이다.

```C++
template<typename T>
class SquareMatrixBase
{
protected:
    SquareMatrixBase(std::size_t n, T* pMem)
        : size(n), pData(pMem) {}
    void invert(std::size_t matrixSize);
private:
    std::size_t size;
    T* pData;
};
```

```C++
template<typename T, std::size_t n>
class SquareMatrix : private SquareMatrixBase<T>
{
public:
    SquareMatrix()
        : SquareMatrixBase<T>(n, data) {}
    ...
    void invert() { this->invert(n); } 
private:
    T data[n*n];
};
```

# <br>항목45: “호환되는 모든 타입”을 받아들이는 데는 멤버 함수 템플릿이 직방!

* 스마트 포인터의 암시적 변환은 어떻게 구현할 수 있을까?
  * 어려운 이유: 템플릿 타입이 서로 상속 구조에 있든 없든, 스마트 포인터 입장에서는 서로 다른 타입일 뿐이다.
* 멤버 함수 템플릿을 활용하면 가능하다.
* 하지만 이는 Base로부터 Derived로 가는 것 역시 가능하게 한다.
  * 심지어 double로부터 int로 가는 것 역시 가능하다.

```C++
SmartPtr<Base> pt1 = SmartPtr<Derived>(new Derived); // error.
// SmartPtr<Base>와 SmartPtr<Derived>는 완전히 다른 타입이다.
```

```C++
template<typename T>
class SmartPtr
{
public:
    template<typename U>
    SmartPtr(const SmartPtr<U>& other);
}
```

따라서, 포인터 값 자체를 갖고 있는 멤버 변수를 이용해 영리하게 해결할 수 있다.

복사 생성자를 사용할 때, heldPtr에 other의 원본 포인터가 대입되지 않으면 컴파일 에러가 난다.

```C++
template<typename T>
class SmartPtr
{
public:
    template<typename U>
    SmartPtr(const SmartPtr<U>& other)
        : heldPtr(otoher.get()) { ... };
    T* get() const { return heldPtr; }
private:
    T* heldPtr;
}
```

기본 복사 생성자와 복사 대입 연산자는 템플릿 멤버 함수로 선언되더라도 여전히 직접 선언해야 한다.

```C++
template<class T> 
class shared_ptr
{
public:
    shared_ptr(shared_ptr const& r);
    template<class Y>
    shared_ptr(shared_ptr<Y> const& r);
    shared_ptr& operator=(shared_ptr const& r);
    template<class Y>
    shared_ptr& operator=(shared_ptr<Y> const& r);
}
```

댓글

이 블로그의 인기 게시물

[코딩의탑] 4층: 툰 쉐이딩

[코딩의탑] 3층: 바다 렌더링

[코딩의탑] 5층: 포탈(Portal), 더 나아가기