가상 상속은 "다이아몬드"(다중 상속) 모호성을 어떻게 해결합니까?


95
class A                     { public: void eat(){ cout<<"A";} }; 
class B: virtual public A   { public: void eat(){ cout<<"B";} }; 
class C: virtual public A   { public: void eat(){ cout<<"C";} }; 
class D: public         B,C { public: void eat(){ cout<<"D";} }; 

int main(){ 
    A *a = new D(); 
    a->eat(); 
} 

나는 다이아몬드 문제를 이해하고 위의 코드에는 그 문제가 없습니다.

가상 상속이 문제를 정확히 어떻게 해결합니까?

내가 이해하는 것 : 내가 말할 때 A *a = new D();컴파일러는 유형의 객체가 유형 D의 포인터에 할당 될 수 있는지 알고 싶어 A하지만 따라갈 수 있지만 스스로 결정할 수는없는 두 개의 경로가 있습니다.

그렇다면 가상 상속은 어떻게 문제를 해결합니까 (컴파일러가 결정을 내리는 데 도움이 됨)?

답변:


109

원하는 사항 : (가상 상속으로 달성 가능)

  A  
 / \  
B   C  
 \ /  
  D 

그리고 아닙니다 : (가상 상속없이 일어나는 일)

A   A  
|   |
B   C  
 \ /  
  D 

가상 상속은 A2가 아닌 기본 클래스의 인스턴스가 1 개뿐임을 의미합니다 .

당신의 유형은 D, (첫 번째 그림에서 볼 수있는) 하나 2의 vtable 포인터있을 것입니다 B및 하나 C실질적으로 상속 사람을 A. D의 개체 크기는 이제 2 개의 포인터를 저장하기 때문에 증가합니다. 그러나 지금은 하나뿐입니다 A.

그래서 B::AC::A가입일 모호한 전화 같은 등이있을 수 있습니다 D. 가상 상속을 사용하지 않는 경우 위의 두 번째 다이어그램이 있습니다. 그러면 A 멤버에 대한 모든 호출이 모호해지며 원하는 경로를 지정해야합니다.

Wikipedia에는 ​​또 다른 좋은 요약과 예가 있습니다.


2
Vtable 포인터는 구현 세부 사항입니다. 이 경우 모든 컴파일러가 vtable 포인터를 도입하는 것은 아닙니다.
curiousguy

19
그래프를 세로로 미러링하면 더 좋을 것 같아요. 대부분의 경우 이러한 상속 다이어그램을 발견하여 기본 아래에 파생 클래스를 표시했습니다. ( "downcast", "upcast"참조)
peterh-Monica 복원

대신 B의 또는 C의 구현 을 사용하도록 코드를 수정하려면 어떻게해야합니까? 감사!
Minh Nghĩa

44

파생 클래스의 인스턴스는 기본 클래스의 인스턴스를 "포함"하므로 메모리에서 다음과 같이 보입니다.

class A: [A fields]
class B: [A fields | B fields]
class C: [A fields | C fields]

따라서 가상 상속이 없으면 클래스 D의 인스턴스는 다음과 같습니다.

class D: [A fields | B fields | A fields | C fields | D fields]
          '- derived from B -' '- derived from C -'

따라서 A 데이터의 두 "사본"에 유의하십시오. 가상 상속은 파생 클래스 내부에 기본 클래스의 데이터를 가리키는 런타임에 설정된 vtable 포인터가 있으므로 B, C 및 D 클래스의 인스턴스가 다음과 같이 보입니다.

class B: [A fields | B fields]
          ^---------- pointer to A

class C: [A fields | C fields]
          ^---------- pointer to A

class D: [A fields | B fields | C fields | D fields]
          ^---------- pointer to B::A
          ^--------------------- pointer to C::A


43

왜 또 다른 대답입니까?

글쎄요, SO에 대한 많은 게시물과 외부 기사에 따르면 다이아몬드 문제는 A두 개 (의 각 부모에 대해 하나씩) 대신 단일 인스턴스를 생성 D하여 모호성을 해결함으로써 해결됩니다. 그러나 이것은 프로세스에 대한 포괄적 인 이해를 제공하지 못했으며 결국 다음과 같은 더 많은 질문을 받게되었습니다.

  1. 만약에 BC시도는 다른 인스턴스 생성하는 A다른 매개 변수를 매개 변수화 된 생성자를 호출 예를 ( D::D(int x, int y): C(x), B(y) {})? 의 A일부가 될의 인스턴스 는 D무엇입니까?
  2. 제가 아닌 가상에 대한 상속 사용하는 경우 B에, 그러나 가상 하나 C? Ain의 단일 인스턴스를 만드는 데 충분 D합니까?
  3. 약간의 성능 비용과 다른 단점없이 가능한 다이아몬드 문제를 해결하기 때문에 지금부터 기본적으로 가상 상속을 항상 예방 조치로 사용해야합니까?

코드 샘플을 시도하지 않고 행동을 예측할 수 없다는 것은 개념을 이해하지 못한다는 것을 의미합니다. 아래는 가상 상속에 대해 머리를 감싸는 데 도움이 된 것입니다.

더블 A

먼저 가상 상속없이 다음 코드로 시작해 보겠습니다.

#include<iostream>
using namespace std;
class A {
public:
    A()                { cout << "A::A() "; }
    A(int x) : m_x(x)  { cout << "A::A(" << x << ") "; }
    int getX() const   { return m_x; }
private:
    int m_x = 42;
};

class B : public A {
public:
    B(int x):A(x)   { cout << "B::B(" << x << ") "; }
};

class C : public A {
public:
    C(int x):A(x) { cout << "C::C(" << x << ") "; }
};

class D : public C, public B  {
public:
    D(int x, int y): C(x), B(y)   {
        cout << "D::D(" << x << ", " << y << ") "; }
};

int main()  {
    cout << "Create b(2): " << endl;
    B b(2); cout << endl << endl;

    cout << "Create c(3): " << endl;
    C c(3); cout << endl << endl;

    cout << "Create d(2,3): " << endl;
    D d(2, 3); cout << endl << endl;

    // error: request for member 'getX' is ambiguous
    //cout << "d.getX() = " << d.getX() << endl;

    // error: 'A' is an ambiguous base of 'D'
    //cout << "d.A::getX() = " << d.A::getX() << endl;

    cout << "d.B::getX() = " << d.B::getX() << endl;
    cout << "d.C::getX() = " << d.C::getX() << endl;
}

출력을 살펴 보겠습니다. 예상대로 실행 B b(2);하면 다음 A(2)C c(3);같습니다.

Create b(2): 
A::A(2) B::B(2) 

Create c(3): 
A::A(3) C::C(3) 

D d(2, 3);모두 필요 B하고 C그들 각각의 자신을 만드는, A우리는 이중 그래서, Ad:

Create d(2,3): 
A::A(2) C::C(2) A::A(3) B::B(3) D::D(2, 3) 

이것이 d.getX()컴파일러가 A메서드를 호출해야하는 인스턴스를 선택할 수 없기 때문에 컴파일 오류가 발생 하는 이유 입니다. 여전히 선택한 부모 클래스에 대해 직접 메서드를 호출 할 수 있습니다.

d.B::getX() = 3
d.C::getX() = 2

가상

이제 가상 상속을 추가하겠습니다. 다음과 같이 변경된 동일한 코드 샘플 사용 :

class B : virtual public A
...
class C : virtual public A
...
cout << "d.getX() = " << d.getX() << endl; //uncommented
cout << "d.A::getX() = " << d.A::getX() << endl; //uncommented
...

다음 생성으로 이동합니다 d.

Create d(2,3): 
A::A() C::C(2) B::B(3) D::D(2, 3) 

및의 A생성자에서 전달 된 매개 변수를 무시하고 기본 생성자로 생성 된 것을 볼 수 있습니다 . 모호성이 사라지면 모든 호출 이 동일한 값 을 반환합니다.BCgetX()

d.getX() = 42
d.A::getX() = 42
d.B::getX() = 42
d.C::getX() = 42

그러나 매개 변수화 된 생성자를 호출하려면 어떻게해야 A합니까? 의 생성자에서 명시 적으로 호출하여 수행 할 수 있습니다 D.

D(int x, int y, int z): A(x), C(y), B(z)

일반적으로 클래스는 직접 부모의 생성자 만 명시 적으로 사용할 수 있지만 가상 상속의 경우 제외됩니다. 이 규칙을 발견 한 것은 저에게 "클릭"되었고 가상 인터페이스를 이해하는 데 많은 도움이되었습니다.

코드 class B: virtual A는 상속 된 모든 클래스 B가 이제 자동으로 수행되지 않기 A때문에 자체적으로 생성 하는 책임이 있음을 의미 B합니다.

이 진술을 염두에두면 내가 가진 모든 질문에 쉽게 답할 수 있습니다.

  1. D생성 하는 동안의 매개 변수에 대한 책임 도 B없고 , 전적으로 유일한 것입니다.CAD
  2. C의 생성을 A에 위임 D하지만 다이아몬드 문제를 다시 가져 오는 B자체 인스턴스를 생성합니다.A
  3. 직계 하위가 아닌 손자 클래스에서 기본 클래스 매개 변수를 정의하는 것은 좋은 습관이 아니므로 다이아몬드 문제가 존재하고이 조치가 불가피 할 때 허용되어야합니다.

10

문제는 컴파일러가 따라야 하는 경로 가 아닙니다 . 문제는 해당 경로 의 종점 , 즉 캐스트의 결과입니다. 유형 변환과 관련하여 경로는 중요하지 않으며 최종 결과 만 중요합니다.

일반 상속을 사용하는 경우 각 경로에는 고유 한 끝 점이 있습니다. 즉, 캐스트 결과가 모호한 것이 문제입니다.

가상 상속을 사용하는 경우 다이아몬드 모양의 계층 구조가 생성됩니다. 두 경로 모두 동일한 끝점으로 연결됩니다. 이 경우 경로 선택 문제는 더 이상 존재하지 않습니다 (또는 더 정확하게는 더 이상 중요하지 않음). 두 경로가 동일한 결과를 가져 오기 때문입니다. 결과는 더 이상 모호하지 않습니다. 그것이 중요한 것입니다. 정확한 경로는 그렇지 않습니다.


@Andrey : 컴파일러가 상속을 어떻게 구현합니까 ... 나는 당신의 주장을 이해하고 그것을 명쾌하게 설명 해주셔서 감사하고 싶습니다.하지만 당신이 다음에 대해 설명 할 수 있다면 정말 도움이 될 것입니다. 컴파일러가 실제로 상속을 구현하는 방법과 가상 상속을 수행 할 때 변경되는 사항
Bruce

8

실제로 예제는 다음과 같아야합니다.

#include <iostream>

//THE DIAMOND PROBLEM SOLVED!!!
class A                     { public: virtual ~A(){ } virtual void eat(){ std::cout<<"EAT=>A";} }; 
class B: virtual public A   { public: virtual ~B(){ } virtual void eat(){ std::cout<<"EAT=>B";} }; 
class C: virtual public A   { public: virtual ~C(){ } virtual void eat(){ std::cout<<"EAT=>C";} }; 
class D: public         B,C { public: virtual ~D(){ } virtual void eat(){ std::cout<<"EAT=>D";} }; 

int main(int argc, char ** argv){
    A *a = new D(); 
    a->eat(); 
    delete a;
}

... 그러면 출력이 올바른 결과가됩니다. "EAT => D"

가상 상속은 할아버지의 중복 만 해결합니다! 그러나 메서드를 올바르게 재정의하려면 여전히 가상 메서드를 지정해야합니다.

당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.