[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);
}
```
댓글
댓글 쓰기