콘텐츠로 이동

상속 (Inheritance)

구성(composition)이 상속보다 적절한 경우가 많습니다. 상속을 사용할 때는 public으로 하세요.

정의:

하위 클래스가 기반 클래스를 상속하면, 기반 클래스가 정의한 모든 데이터와 연산의 정의를 포함하게 됩니다. "인터페이스 상속"은 순수 추상 기반 클래스(상태도 정의된 메서드도 없는 클래스)로부터의 상속이고, 그 밖의 모든 상속은 "구현 상속"입니다.

장점:

구현 상속은 기존 타입을 특수화하면서 기반 클래스의 코드를 재사용하므로 코드 크기를 줄여 줍니다. 상속은 컴파일 타임 선언이므로 여러분과 컴파일러 모두 그 동작을 이해하고 오류를 잡아낼 수 있습니다. 인터페이스 상속은 클래스가 특정 API를 노출하도록 프로그램적으로 강제하는 데 쓸 수 있습니다. 이 경우에도 클래스가 API에 필요한 메서드를 정의하지 않으면 컴파일러가 오류를 잡아냅니다.

단점:

구현 상속에서는 하위 클래스를 구현하는 코드가 기반 클래스와 하위 클래스에 나뉘어 있으므로 구현을 이해하기가 더 어려울 수 있습니다. 하위 클래스는 가상 함수가 아닌 함수를 재정의할 수 없으므로 구현을 바꿀 수 없습니다.

특히 다중 상속이 문제가 됩니다. 성능 부담이 더 큰 경우가 많고(실제로 단일 상속에서 다중 상속으로 갈 때의 성능 저하가 일반 디스패치에서 가상 디스패치로 갈 때의 저하보다 클 수 있습니다), 모호성과 혼란과 명백한 버그를 낳기 쉬운 "다이아몬드" 상속 형태로 이어질 위험이 있기 때문입니다.

결정:

모든 상속은 public이어야 합니다. private 상속을 하고 싶다면 대신 기반 클래스의 인스턴스를 멤버로 포함해야 합니다. 어떤 클래스를 기반 클래스로 사용하는 것을 지원할 생각이 없다면 그 클래스에 final을 쓸 수 있습니다.

구현 상속을 과용하지 마세요. 구성이 더 적절한 경우가 많습니다. 상속은 "is-a" 관계로 제한하세요. Bar가 Foo의 "일종"이라고 말하는 것이 합리적일 때 Bar가 Foo를 상속합니다.

protected는 하위 클래스에서 접근해야 할 수 있는 멤버 함수로만 제한하세요. 데이터 멤버는 private이어야 한다는 점에 유의하세요.

가상 함수나 가상 소멸자의 재정의에는 override 또는 (드물게) final 지정자 중 정확히 하나를 명시적으로 붙이세요. 재정의를 선언할 때 virtual을 쓰지 마세요. 근거: 기반 클래스의 가상 함수를 재정의하지 않는데도 override나 final이 붙은 함수나 소멸자는 컴파일되지 않으며, 이는 흔한 오류를 잡는 데 도움이 됩니다. 또한 이 지정자들은 문서 역할을 합니다. 지정자가 없으면 읽는 사람이 해당 클래스의 모든 조상을 확인해야 그 함수나 소멸자가 가상인지 알 수 있습니다.

다중 상속은 허용되지만, 다중 구현 상속은 강력히 권장하지 않습니다.


옮긴이 풀이

핵심: 상속보다 구성(composition)을 먼저 고려하라

"is-a" 관계가 분명할 때만 상속하세요. Bar가 Foo의 "일종"이라고 자연스럽게 말할 수 있어야 합니다. 그저 코드를 재사용하려는 것이라면 상속이 아니라 멤버로 포함(구성)하는 편이 대개 낫습니다.

인터페이스 상속 vs 구현 상속

  • 인터페이스 상속: 순수 추상 기반 클래스(상태·구현 없음)로부터의 상속. 특정 API를 강제하고 컴파일러가 누락을 잡아줍니다.
  • 구현 상속: 그 외. 구현이 기반 클래스와 하위 클래스에 흩어져 이해하기 어려워지므로 과용하지 마세요.

모든 상속은 public

private 상속이 필요하다면 그건 사실 "포함" 관계입니다 — 기반 클래스 인스턴스를 멤버로 두세요. 하위 클래스를 막고 싶으면 클래스에 final을 쓸 수 있습니다.

override / final 로 명시

가상 함수를 재정의할 때는 override(또는 드물게 final)를 정확히 하나 붙이고, virtual은 다시 쓰지 마세요. 재정의가 아닌데 override를 붙이면 컴파일 오류가 나서 흔한 실수를 잡아줍니다. 또한 독자가 조상 클래스를 다 뒤지지 않아도 가상 여부를 알 수 있는 문서 역할을 합니다.

class Base {
 public:
  virtual ~Base() = default;
  virtual void Foo();
};

class Derived : public Base {
 public:
  void Foo() override;  // virtual 반복 X, override O
};

그 밖에

  • protected는 하위 클래스가 접근해야 하는 멤버 함수로만 제한하세요. 데이터 멤버는 private이어야 합니다.
  • 다중 상속은 허용되지만 다중 구현 상속은 권장되지 않습니다(다이아몬드 상속의 모호성·버그 위험).