c++ - C ++에서 다중 상속을 피하는 이유는 무엇입니까?


다중 상속을 사용하는 것이 좋은 개념입니까, 아니면 대신 다른 작업을 수행 할 수 있습니까?


Answers



다중 상속 (MI로 약칭 함)은 냄새를 맡습니다 . 이는 대개 악의적 인 이유로 완료되었으며 유지 보수 자의 얼굴로 다시 날아가 버릴 것임을 의미합니다.

개요

  1. 상속 대신 기능 구성 고려
  2. Diamond of Dread를 조심하십시오.
  3. 객체 대신 여러 인터페이스의 상속 고려
  4. 때로는 다중 상속이 올바른 것입니다. 그럴 경우 사용하십시오.
  5. 코드 리뷰에서 다중 상속 아키텍처를 방어 할 준비를하십시오.

1. 아마도 구성?

이것은 상속에 적용됩니다. 따라서 다중 상속의 경우 더욱 그렇습니다.

당신의 물건이 정말로 다른 사람에게서 상속 받아야합니까? CarEngine 이나 Wheel 에서 상속 할 필요가 없습니다. Car 에는 Engine 과 4 개의 Wheel 있습니다.

컴포지션 대신 이러한 상속을 해결하기 위해 다중 상속을 사용하면 잘못된 결과가 발생합니다.

2. 공포의 다이아몬드

일반적으로 클래스 A 있고 BC 모두 A 상속받습니다. 그리고 (왜 그런지 묻지 마십시오.) 그러면 누군가 DBC 모두를 상속해야한다고 결정합니다.

나는이 문제를 8 년 동안 두 번이나 두 번이나 겪었습니다.

  1. 처음부터 실수가 얼마나 되었습니까 (두 경우 모두 DBC 모두에서 상속되어서는 안됩니다). 왜냐하면 이것이 나쁜 아키텍처 였기 때문입니다 (실제로 C 가 존재해서는 안됩니다 ...)
  2. C ++에서 부모 클래스 A 가 손자 클래스 D 두 번 나타나기 때문에 하나의 부모 필드 A::field 를 업데이트하는 것은 B::fieldC::field 통해 두 번 업데이트하는 것을 의미했기 때문에 얼마나 많은 유지 보수 담당자가 비용을 지불 했습니까? C::field ), 또는 무언가를 조용히 잘못하고 충돌 (나중에 B::field 새 포인터 및 C::field 삭제 ...)

상속을 한정하기 위해 C ++에서 키워드 virtual을 사용하면 위와 같은 이중 레이아웃을 피할 수 있습니다. 그러나 이것이 원하는 것이 아니라면 어쨌든, 내 경험상, 당신은 아마 뭔가 잘못하고있는 것입니다 ...

객체 계층 구조에서는 그래프가 아닌 계층 구조를 트리 (노드에는 하나의 부모가 있음)로 유지해야합니다.

The Diamond에 대한 자세한 정보 (편집 : 2017-05-03)

C ++의 Dread of Diamond의 실제 문제 ( 설계가 건전하다고 가정하고 코드 검토가 필요하다고 가정 할 때 ) 는 선택을해야합니다 .

  • 클래스 A 가 레이아웃에 두 번 존재하는 것이 바람직합니까? 그리고 그 의미는 무엇입니까? 그렇다면 꼭 모든 것을 두 번 상속하십시오.
  • 그것이 단 한 번만 존재해야한다면, 사실상 그것으로부터 상속받습니다.

이 선택은 문제에 내재되어 있으며 C ++에서는 다른 언어와 달리 실제로 언어 수준에서 디자인을 강요하지 않고도 할 수 있습니다.

그러나 모든 힘과 마찬가지로 그 힘은 책임감을 가져옵니다. 디자인을 검토하십시오.

3. 인터페이스

0 또는 하나의 구체적인 클래스, 그리고 0 개 이상의 인터페이스의 다중 상속은 일반적으로 좋습니다. 위에서 설명한 Dread of Diamond가 발생하지 않기 때문입니다. 사실, 이것은 Java에서 일이 이루어지는 방식입니다.

일반적으로 C가 AB 로부터 상속받을 때 사용자가 C 를 마치 A 처럼 사용하거나 B 를 사용하는 것처럼 사용할 수 있습니다.

C ++에서 인터페이스는 다음과 같은 추상 클래스입니다.

  1. 모든 메소드가 순수 가상 (선언 자 = 0) (2017-05-03 삭제)
  2. 멤버 변수 없음

0에서 1 개의 실제 객체와 0 이상의 인터페이스의 다중 상속은 "smelly"로 간주되지 않습니다 (적어도 많지는 않음).

C ++ 추상 인터페이스에 대한 추가 정보 (edit 2017-05-03)

첫째, NVI 패턴은 인터페이스를 생성하는 데 사용될 수 있습니다 . 실제 조건은 상태가 없기 때문입니다 (즉,이 경우를 제외하고는 멤버 변수가 없습니다). 추상적 인 인터페이스의 요점은 계약을 발표하는 것입니다 ( "이쪽으로, 그리고 이쪽이라고 부를 수 있습니다"). 추상 가상 메소드 만 갖는 한계는 의무가 아닌 설계상의 선택입니다.

둘째, C ++에서는 추상적 인 인터페이스 (추가 비용 / 간접 지정 포함)로부터 사실상 상속하는 것이 좋습니다. 인터페이스 상속이 계층 구조에 여러 번 나타나는 경우 모호한 점이 있습니다.

셋째, 객체 오리엔테이션은 훌륭하지만, C ++에서는 The Only Truth Out TM 이 아닙니다. 올바른 도구를 사용하고 C ++에서 다른 종류의 솔루션을 제공하는 다른 패러다임을 항상 기억하십시오.

4. 다중 상속이 정말로 필요합니까?

어쩔 땐 그래.

일반적으로 C 클래스는 AB 상속하며 AB 는 서로 관련이없는 두 개체 (즉, 동일한 계층 구조가 아니거나 공통적 인 개념이나 다른 개념 등이 아님)입니다.

예를 들어 X, Y, Z 좌표의 Nodes 시스템을 사용하여 많은 기하학적 계산 (점, 기하학 객체의 일부)을 수행 할 수 있으며 각 노드는 자동 에이전트이며 다른 에이전트와 통신 할 수 있습니다 .

어쩌면 이미 네 개의 네임 스페이스가있는 두 개의 라이브러리에 액세스 할 수 있습니다 (네임 스페이스를 사용하는 또 다른 이유 ...하지만 네임 스페이스를 사용합니까?), 하나는 geo 이고 다른 하나는 ai

그래서 당신은 자신 own::Nodeai::Agentgeo::Point 에서 파생됩니다.

이제는 대신 작곡을 사용하지 말고 스스로에게 질문해야하는 순간입니다. own::Node 가 정말로 ai::Agentgeo::Point 인 경우 구성이 수행되지 않습니다.

그런 다음 own::Node 다중 상속이 필요합니다 own::Node 는 3D 공간에서의 위치에 따라 다른 에이전트와 통신합니다.

(당신은 ai::Agentgeo::Point 가 완전히 완전하고 완전하며 ... 이것은 다중 상속의 위험을 획기적으로 줄임)

기타 사례 (2017-05-03 편집)

다른 경우가 있습니다.

  • 구현 정보로 (잘하면 개인적인) 상속 사용하기
  • 정책과 같은 일부 C ++ 관용구는 다중 상속을 사용할 수 있습니다 (각 부분 this 부분을 ​​통해 다른 부분과 통신해야하는 경우)
  • std :: exception의 가상 상속 ( 예외에 대해 가상 상속이 필요합니까? )
  • 기타

때때로 당신은 작곡을 사용할 수 있으며 때로는 MI가 더 좋을 수도 있습니다. 요점은 : 선택의 여지가 있습니다. 책임감있게 수행하십시오 (코드를 검토하십시오).

5. 그래서 다중 상속을해야합니까?

대부분의 시간, 내 경험에, 아니. MI는 결과를 실현하지 않고 게으른 기능을 함께 누적하여 사용할 수 있기 때문에 (마치 Car EngineWheel 만드는 것과 같이) 작동한다고해도 올바른 도구는 아닙니다.

하지만 때로는 그렇습니다. 그리고 그 당시에는 MI보다 더 좋은 것은 없을 것입니다.

그러나 MI는 냄새가 심하기 때문에 코드 검토에서 아키텍처를 방어 할 준비를하십시오 (방어하는 것은 좋은 일입니다. 방어 할 수 없으면 수행하지 않아야하기 때문입니다).




Bjarne Stroustrup과인터뷰에서 :

사람들은 다중 상속을 필요로하지 않는다고 말합니다. 다중 상속으로 할 수있는 일은 단일 상속으로도 할 수 있기 때문입니다. 방금 언급 한 위임 트릭을 사용합니다. 또한 상속을 전혀 필요로하지 않습니다. 단일 상속으로 수행하는 작업은 상속없이 클래스를 통해 전달할 수 있기 때문에 상속이 필요하지 않습니다. 실제로 포인터와 데이터 구조로 모든 것을 할 수 있기 때문에 어떤 클래스도 필요하지 않습니다. 그런데 왜 그걸하고 싶니? 언어 시설을 이용하는 것이 언제 편리합니까? 해결 방법을 언제 선호합니까? 다중 상속이 유용한 경우를 보았고, 다중 상속이 매우 복잡한 경우도 보았습니다. 일반적으로 언어에서 제공하는 기능을 사용하여 대안을 취하는 것을 선호합니다.




그것을 피할 이유가 없으며 상황에서 매우 유용 할 수 있습니다. 당신은 잠재적 인 문제를 알고 있어야합니다.

죽음의 다이아몬드 인 가장 큰 것 :

class GrandParent;
class Parent1 : public GrandParent;
class Parent2 : public GrandParent;
class Child : public Parent1, public Parent2;

이제 Child 내에 GrandParent라는 두 개의 "사본"이 생겼습니다.

C ++은 이것에 대해서 생각해 왔으며 당신이 가상의 상속을 통해이 문제를 해결할 수있게 해줍니다.

class GrandParent;
class Parent1 : public virtual GrandParent;
class Parent2 : public virtual GrandParent;
class Child : public Parent1, public Parent2;

디자인을 항상 검토하고 데이터 재사용을 위해 상속을 사용하지 않도록하십시오. 당신이 작곡을 통해 똑같은 것을 표현할 수 있다면 (그리고 일반적으로 할 수있다) 이것은 훨씬 더 나은 접근법이다.




w : 다중 상속 참조.

다중 상속은 비판을 받았으며, 따라서 여러 언어로 구현되지 않았습니다. 비판에는 다음이 포함됩니다.

  • 복잡성 증가
  • 시맨틱 모호성은 종종 다이아몬드 문제 로 요약됩니다.
  • 단일 클래스에서 여러 번 명시 적으로 상속 할 수 없음
  • 상속의 클래스 의미를 변경하는 순서.

C ++ / Java 스타일 생성자를 사용하는 언어의 다중 상속은 생성자 및 생성자 체인의 상속 문제를 악화 시키므로 이러한 언어에서 유지 관리 및 확장 성 문제가 발생합니다. 커스터마이징 패러다임 (constructor chaining paradigm) 하에서는 매우 다양한 구성 메소드를 사용하여 상속 관계에있는 객체를 구현하기가 어렵습니다.

COM 및 Java 인터페이스와 같은 인터페이스 (순수 추상 클래스)를 사용하여이를 해결하는 현대적인 방법.

나는 이것 대신에 다른 일을 할 수 있습니까?

그래 넌 할수있어. 나는 GoF 에서 훔칠거야.

  • 구현이 아닌 인터페이스에 프로그램하십시오.
  • 상속에 대한 컴포지션 선호



공개 상속은 IS-A 관계이며 때로는 클래스가 여러 클래스의 한 유형이되며 때로는이를 반영하는 것이 중요합니다.

"믹스 인"은 때로는 유용합니다. 이들은 일반적으로 작은 클래스이며, 일반적으로 어떤 것도 상속하지 않으며 유용한 기능을 제공합니다.

상속 계층 구조가 매우 얕고 (거의 항상 그렇듯이) 관리가 잘되어 있다면, 두려운 다이아몬드 상속을받을 가능성은 희박합니다. 다이아몬드는 다중 상속을 사용하는 모든 언어에서 문제가되지 않지만 C ++의 처리는 종종 어색하고 때로는 혼란 스럽습니다.

다중 상속이 매우 편리한 경우를 실행했지만 실제로는 거의 사용되지 않습니다. 왜냐하면 내가 다중 상속을 필요로하지 않을 때 다른 디자인 방법을 선호하기 때문입니다. 혼란스러운 언어 구조를 피하는 것이 더 좋으며 어떤 일이 일어나고 있는지 파악하기 위해 매뉴얼을 잘 읽어야하는 상속 사례를 만드는 것은 쉽습니다.




다중 상속을 피할 수는 없지만 '다이아몬드 문제'( http://en.wikipedia.org/wiki/Diamond_problem )와 같이 발생할 수있는 문제를 알아야하며 사용자에게 주어진 힘을주의 깊게 다루어야합니다 , 당신은 모든 힘으로해야한다.







모든 프로그래밍 언어는 장단점이있는 객체 지향 프로그래밍의 처리 방법이 약간 다릅니다. C ++의 버전은 성능에 중점을두고 있으며 잘못된 코드를 작성하기가 쉽지 않은 부수적 인 단점이 있습니다. 이는 다중 상속에 해당합니다. 따라서 프로그래머가이 기능을 사용하지 못하도록하는 경향이 있습니다.

다른 사람들은 다중 상속이 어째서 좋지 않은가에 대한 문제를 다루었습니다. 그러나 우리는 그것이 안전하지 않기 때문에 그것을 피하는 이유가 더 많거나 적은 것을 암시하는 꽤 많은 의견을 보았습니다. 네, 그렇습니다.

C ++에서 흔히있는 것처럼, 기본 지침을 따르면 "어깨 너머로"보지 않아도 안전하게 사용할 수 있습니다. 핵심 아이디어는 "혼합"이라고 불리는 특별한 종류의 클래스 정의를 구별하는 것입니다. 클래스는 모든 멤버 함수가 가상 (또는 순수 가상) 인 경우 믹스 인입니다. 그런 다음 하나의 메인 클래스와 원하는만큼 많은 "믹스 인"을 상속받을 수 있지만 "가상"키워드로 믹스 인을 상속해야합니다. 예

class CounterMixin {
    int count;
public:
    CounterMixin() : count( 0 ) {}
    virtual ~CounterMixin() {}
    virtual void increment() { count += 1; }
    virtual int getCount() { return count; }
};

class Foo : public Bar, virtual public CounterMixin { ..... };

제 제안은 클래스를 믹스 인 클래스로 사용하려는 경우 명명 규칙을 사용하여 코드를 읽는 사람이 무슨 일이 일어나고 있는지를 쉽게 확인하고 기본 가이드 라인의 규칙에 따라 놀고 있는지 확인하는 것입니다. . 또한 가상베이스 클래스의 작동 방식 때문에 믹스 인에 기본 생성자가있는 경우 훨씬 더 효율적으로 작동합니다. 모든 소멸자를 가상으로 만드는 것을 잊지 마십시오.

여기서 "믹스 인"이라는 단어를 사용하는 것은 매개 변수화 된 템플릿 클래스와 다릅니다 (좋은 설명을 보려면 이 링크 참조). 그러나 용어의 정당한 사용이라고 생각합니다.

이제는 이것이 다중 상속을 안전하게 사용하는 유일한 방법이라는 인상을주고 싶지 않습니다. 확인하기 쉬운 방법 중 하나입니다.




조금 추상화 될 위험에 처해서 카테고리 이론의 틀 내에서 상속에 대해 생각하는 것이 밝혀졌습니다.

상속 관계를 나타내는 모든 클래스와 화살표를 생각하면 다음과 같은 것입니다.

A --> B

class Bclass B 에서 파생됨을 의미합니다. 주어진

A --> B, B --> C

우리는 C가 A에서 파생 된 B에서 유래한다고 말합니다. 그래서 C는 또한 A에서 파생된다고 말합니다.

A --> C

게다가, 우리는 모든 A 클래스가 A 가 trivially A 로부터 파생되므로, 따라서 상속 모델은 카테고리의 정의를 충족한다고 말합니다. 보다 전통적인 언어로, 우리는 객체를 가진 Class 를 가지며 모든 클래스와 형태는 상속 관계입니다.

그건 약간의 설정이지만, 그걸로 다이아몬드의 운명을 살펴 봅시다.

C --> D
^     ^
|     |
A --> B

그늘지는보고있는 도표이다, 그러나 그것은 할 것이다. 그래서 DA , B , C 모두를 상속받습니다. 또한 OP의 질문을 다루는 데 더 가까워지면서 DA 수퍼 클래스도 상속받습니다. 우리는 다이어그램을 그릴 수 있습니다.

C --> D --> R
^     ^
|     |
A --> B
^ 
|
Q

이제 다이아몬드 오브 데스와 관련된 문제는 CB 가 몇 가지 속성 / 메서드 이름을 공유하고 모호한 경우입니다. 그러나 공유 행동을 A 로 옮기면 모호성이 사라집니다.

범주적인 용어를 넣으면, AB , CBCQ 로부터 상속 받으면 AQ 서브 클래스로 재 작성 될 수있는 것으로 생각합니다. 이것은 푸시 아웃 (pushout) 이라고하는 것을 만듭니다 .

D 에는 철수 라고하는 대칭 구조가 있습니다. 이것은 본질적으로 BC 모두에서 상속하는 가장 일반적으로 유용한 클래스입니다. 즉, 다른 클래스 RBC 에서 상속 받으면 DRD 하위 클래스로 다시 쓰여질 수있는 클래스입니다.

다이아몬드 팁이 철수와 푸시 아웃인지 확인하면 달리 발생할 수있는 이름 충돌 또는 유지 관리 문제를 일반적으로 처리 할 수 ​​있습니다.

참고 Paercebal 의 대답 은 우리가 모든 범주의 모든 클래스에서 작업한다는 점을 감안할 때 위의 모델에 대한 그의 경고가 암시하기 때문에이 점에 영감을 받았습니다.

저는 다중 상속 관계가 얼마나 복잡하고 비 문제가 될 수 있는지 보여주는 그의 주장을 일반화하고 싶었습니다.

TL; DR 프로그램에서 상속 관계를 카테고리로 생각하십시오. 그렇다면 다중 상속 클래스 푸시 아웃과 대칭 적으로 푸어 버튼 인 공통 상위 클래스를 만들어 Doom의 다이아몬드 문제를 피할 수 있습니다.







다이아몬드 패턴 외에도 다중 상속은 객체 모델을 이해하기 어렵게 만들고 유지 비용을 증가시킵니다.

구성은 본질적으로 이해하기 쉽고, 이해하고, 설명하기 쉽습니다. 코드를 작성하는 것은 지루할 수 있지만, 좋은 IDE (Visual Studio로 작업 한 지 수 년이되었지만, 확실히 Java IDE에는 모두 멋진 단축키 자동화 도구가 있습니다)는 장애물을 극복해야합니다.

또한 유지 보수면에서 비 문자 상속 인스턴스에서도 "다이아몬드 문제"가 발생합니다. 예를 들어, A와 B가 있고 C 클래스를 모두 확장하면 A는 오렌지 주스를 만드는 'makeJuice'방법을 사용하며 석회로 오렌지 주스를 만드는 방법을 확장합니다. 디자이너가 ' B '는 전류를 생성하고 생성하는'makeJuice '방법을 추가합니까? 'A'와 'B'는 지금 "부모"와 호환 될 수 있지만, 그렇다고해서 항상 그렇게 될 것은 아닙니다.

전반적으로 상속을 피하기위한 경향의 극대화, 특히 다중 상속은 건전합니다. 모든 격언에는 예외가 있지만 코드를 작성하는 예외를 가리키는 깜박이는 녹색 네온 사인이 있는지 확인해야합니다 (뇌가 열리면 상속 나무가 보이기 때문에 언제든지 그린 네온이 깜박입니다) sign), 그리고 당신은 모든 것이 한 번씩 의미가 있는지 확인해야합니다.




구체적인 객체에 대한 MI의 핵심 문제는 합법적으로 "A가되고 B"가되어야하는 객체가 거의 없다는 것입니다. 따라서 논리적 근거에 대한 올바른 해결책은 거의 없습니다. 훨씬 더 자주, 당신은 C가 A 나 B의 역할을 할 수있는 객체 C를 가지고 있습니다. 이것은 인터페이스 상속과 합성을 통해 얻을 수 있습니다. 그러나 여러 개의 인터페이스를 상속하는 것은 여전히 ​​실수입니다. MI의 일부분입니다.

특히 C ++의 경우 기능의 주요 약점은 다중 상속의 실제 존재가 아니라 거의 항상 잘못된 형식의 허용되는 일부 구문입니다. 예를 들어, 다음과 같은 동일한 객체의 여러 사본을 상속 :

class B : public A, public A {};

DEFINITION에 의해 ​​조작됩니다. 영어로 번역 된 것은 "B는 A와 A"입니다. 따라서 인간 언어에서도 심각한 모호성이 있습니다. "B가 2 As"또는 "B is A"를 의미 했습니까? 그러한 병적 인 코드를 허용하고, 사용 예제를 만들기가 더 어려워서 후속 언어로 기능을 유지하는 경우를 만들 때 C ++은 아무런 도움이되지 못했습니다.




상속에 우선하여 composition을 사용할 수 있습니다.

일반적인 느낌은 구성이 더 좋고, 매우 잘 논의 된 것입니다.




그것은 관련된 클래스 당 4/8 바이트를 필요로합니다. (클래스 당이 포인터 하나).

이것은 결코 문제가되지 않을 수도 있지만 언젠가는 당신이 수십억 시간의 인스턴스가되는 마이크로 데이터 구조를 가지고 있다면 그렇게 될 것입니다.




우리는 에펠을 사용합니다. 우리는 뛰어난 MI를 가지고 있습니다. 걱정 마. 문제 없음. 쉽게 관리 할 수 ​​있습니다. MI를 사용하지 않는 경우가 있습니다. 그러나 사람들이 깨닫는 것보다 더 유용합니다 : A) 잘 관리하지 못하는 위험한 언어로 - 또는 - B) 그들이 수년간 MI에서 어떻게 일했는지에 만족했습니다. - 또는 - C) 다른 이유들 ( 목록이 너무 많아서 나는 확실히 확신합니다 - 위의 답변을보십시오).

우리에게는 Eiffel을 사용하여 MI가 다른 것보다 자연 스러우며 도구 상자에 다른 훌륭한 도구가 있습니다. 솔직히, 우리는 에펠을 사용하는 사람이 아무도 없다는 사실에 전혀 관심이 없습니다. 걱정 마. 우리는 우리가 가지고있는 것에 만족하며 당신에게 한 번 보라고 권합니다.

찾고있는 동안 : 무효화 및 Null 포인터 참조 해제의 제거에 특히주의하십시오. 우리가 모두 MI에서 춤을 추는 동안, 당신의 조언은 길을 잃고 있습니다! :-)