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