본문 바로가기
C++/Effective C++

Effective C++ (36) ~ (40)

by 계발자 망고 2021. 8. 25.

36) 상속받은 비가상 함수를 파생 클래스에서 재정의하는 것은 절대 금물!

비가상 함수는 정적 바인딩, 가상 함수는 동적 바인딩으로 이루어진다.

만약 비 가상 함수를 재정의하고 호출한다면 호출하는 객체의 타입에따라 동작이 달라진다.

따라서 상속받은 비가상 함수는 절대로 재정의하지 말자.

37) 어떤 함수에 대해서도 상속받은 기본 매개변수 값은 절대로 재정의하지 말자

기본값을 정의해둔 가상 함수의 매개변수는 절대로 상속하면서 파생 클래스에서 값이 달라지거나 하지 않도록 주의하자.

가상 함수 자체는 동적으로 바인딩되어 있지만 기본 매개변수는 정적으로 바인딩되어있다. 이는 런타임 효율을 높이기 위해서이다.

따라서 파생 클래스에 정의된 가상 함수를 사용하면서 매개 변수는 기본 클래스의 매개변수를 사용하게 될 수도 있다는 뜻이다.

그렇다고 다음과 같이 구현하는건 좋지 않다.

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 = red) const;
};

코드에 중복과 의존성이 생긴다. 기본 클래스에서 매개변수 기본값이 변하면 모든 파생클래스에도 반영해야 하기 때문이다.

해결책은 NVI를 사용하는 것이다.

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:
  ...
  
private:
  virtual void doDraw(ShapeColor color) const;
};

매개변수 기본값을 가진 함수를 public으로 노출하고, 해당 함수에서 호출하는 가상 함수를 private으로 정의하는 것이다.

38) has-a 혹은 is implement in terms of 를 모형화할 때는 객체 합성을 사용하자

사람 객체가 주소, 이름, 성격 등을 가지는 형태를 합성(composition)이라고 표현한다.

이것이 가지는 의미는 is-a의 의미를 가지는 public 상속과는 절대절대 다르다!

39) private 상속은 조심스럽게

private 상속은 public 상속과 절대로 다르다.

public 상속이 is-a 관계라면 private 상속은 is implemented-in-terms-of의 의미를 가진다.

class Person {...};
class Student : private Person {...};

void eat(const Person& p);
void study(const Student& s);

Person p;
Student s;

eat(p); // ok
eat(s); // error!! student is not a person

상속 관계가 private이면 컴파일러는 파생 클래스 객체를 기본 클래스 객체로 변환하지 않는다.

또한 물려받은 모든 멤버가 private 멤버가 된다.

보통 비공개 멤버를 접근하거나 가상함수를 재정의해야하면서 기본 클래스를 구현에 활용하고 싶을 때 private 상속을 사용한다.

예를 들어 어떤 Widget 클래스의 사용 정보를 수집하기 위해 Timer 클래스를 정의한다.

Widget 클래스에서 Timer에서 private 상속을 받아 Timer의 가상 함수를 재정의해 활용하면 좋다.

이때 public 상속은 적절하지 않다.

하지만 이 또한 객체 합성으로 해결할 수 있는 문제이다.

다음과 같은 구조로 해결하면 좋다.

class Widget{
private:
  class WidgetTimer : public Timer{
  public:
    virtual void onTick() const;
  };
  
  WidgetTimer timer;
};

이렇게 하면 Widget의 파생클래스에서 onTick을 재정의할 수 없다.

또한 Widget이 WidgetTimer의 포인터를 갖게 해두면 Widget의 컴파일 의존성을 최소화할 수 있다.

 

객체 합성이 아닌 반드시 private 상속만으로 해결할 수 있는 문제가 하나 있는데 Empty Base Optimization이다.

 

private 상속이 필요해 보일때 다른 방법으로 해결할 수 있는 경우가 많으므로 조심스럽게 사용하자.

40) 다중 상속은 심사숙고해서

다중 상속의 가장 큰 문제점은 둘 이상의 기본 클래스에서 같은 이름을 물려받을 경우 모호성이 생겨버린다는 것이다.

또한 같은 클래스로부터 상속된 두 클래스를 다중상속 하게되면 문제는 더욱 심각해진다. 이때 데이터 멤버의 중복 생성을 하지 않으려면 virtual 상속을 하면 된다.

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

하지만 가상 상속은 비용이 높은 편이다. 

따라서 기본적으로

  • 가상 기본 클래스는 최대한 사용하지 말자
  • 기존의 클래스 계통에 파생 클래스를 새로 추가할 때 그 파생 클래스가 가상 기본 클래스의 초기화를 해야한다.

단 다중상속이 유용할 때가 있는데, 인터페이스 클래스에서 public 상속을 받고 구현을 돕는 클래스로부터 private 상속을 받는 경우이다.

반응형

'C++ > Effective C++' 카테고리의 다른 글

Effective C++ (46) ~ (50)  (0) 2021.08.28
Effective C++ (41) ~ (45)  (0) 2021.08.26
Effective C++ (31) ~ (35)  (0) 2021.08.24
Effective C++ (26) ~ (30)  (0) 2021.08.22
Effective C++ (21) ~ (25)  (0) 2021.08.16

댓글