वर्चुअल हेरिटेज "डायमंड" (मल्टीपल इनहेरिटेंस) अस्पष्टता को कैसे हल करता है?


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 

वर्चुअल इनहेरिटेंस का मतलब है कि बेस Aक्लास 2 का केवल 1 उदाहरण होगा ।

आपके प्रकार Dमें 2 व्यवहार्य बिंदु होंगे (आप उन्हें पहले आरेख में देख सकते हैं), एक के लिए Bऔर एक Cजो वस्तुतः विरासत में मिला है ADऑब्जेक्ट का आकार बढ़ा हुआ है क्योंकि यह अब 2 पॉइंटर्स स्टोर करता है; हालाँकि Aअब केवल एक ही है ।

इसलिए B::Aऔर C::Aसमान हैं और इसलिए इससे कोई अस्पष्ट कॉल नहीं हो सकती है D। यदि आप वर्चुअल इनहेरिटेंस का उपयोग नहीं करते हैं तो आपके पास ऊपर दूसरा आरेख है। और A के सदस्य को कोई भी कॉल अस्पष्ट हो जाता है और आपको यह निर्दिष्ट करने की आवश्यकता होती है कि आप कौन सा रास्ता लेना चाहते हैं।

विकिपीडिया का एक और अच्छा कुंड और उदाहरण है


2
Vtable पॉइंटर एक इम्प्लीमेंटेशन डिटेल है। सभी कंपाइलर इस मामले में व्यवहार्य पॉइंटर्स का परिचय नहीं देंगे।
जिज्ञासु

19
मुझे लगता है कि यह बेहतर होगा अगर रेखांकन लंबवत रूप से चित्रित किया जाएगा। ज्यादातर मामलों में मैंने इस तरह के वंशानुक्रम आरेखों को आधारों के नीचे व्युत्पन्न वर्गों को दिखाने के लिए पाया है। (देखें "डाउनकास्ट", "अपकास्ट")
- मोनिका

मैं कैसे उपयोग करने के लिए अपने कोड को संशोधित कर सकते Bहै या Cकार्यान्वयन के बजाय की? धन्यवाद!
मिन्ह नगूआ

44

व्युत्पन्न वर्गों के उदाहरणों में बेस कक्षाओं के "उदाहरण" होते हैं, इसलिए वे इस तरह स्मृति में दिखते हैं:

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

इस प्रकार, आभासी विरासत के बिना, कक्षा डी का उदाहरण इस तरह दिखेगा:

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

तो, ए डेटा की दो "प्रतियां" नोट करें। वर्चुअल इनहेरिटेंस का अर्थ है कि व्युत्पन्न वर्ग के अंदर रन टाइम पर एक वाइबेटर पॉइंटर होता है जो बेस क्लास के डेटा को इंगित करता है, ताकि बी, सी और डी कक्षाओं के उदाहरण दिखते हैं:

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

@ बालू: कंपाइलटाइम स्टैकओवरफ्लो.com/questions/3849498/when-is-vtable-in- c
रंजन नायक

43

दूसरा जवाब क्यों?

वैसे, SO और लेखों के बाहर के कई पोस्ट कहते हैं, कि हीरे की समस्या Aदो (एक माता-पिता के लिए एक D) के बजाय एक उदाहरण बनाकर हल की जाती है , इस प्रकार अस्पष्टता का समाधान होता है। हालांकि, इसने मुझे प्रक्रिया की व्यापक समझ नहीं दी, मैंने और भी अधिक प्रश्नों के साथ समाप्त किया

  1. क्या होगा Bऔर विभिन्न मापदंडों ( ) के साथ पैराट्राइज्ड कंस्ट्रक्टर को कॉल Cकरने के विभिन्न उदाहरण बनाने की कोशिश करता है ? का हिस्सा बनने के लिए किस उदाहरण को चुना जाएगा ?AD::D(int x, int y): C(x), B(y) {}AD
  2. क्या होगा यदि मैं गैर-आभासी विरासत का उपयोग करता हूं B, लेकिन आभासी एक के लिए C? क्या यह एकल उदाहरण बनाने के लिए पर्याप्त Aहै D?
  3. क्या मुझे हमेशा से अब तक निवारक उपाय के रूप में डिफ़ॉल्ट रूप से आभासी विरासत का उपयोग करना चाहिए क्योंकि यह मामूली प्रदर्शन लागत और अन्य कमियों के साथ संभव हीरे की समस्या को हल करता है?

कोड नमूनों की कोशिश किए बिना व्यवहार की भविष्यवाणी करने में सक्षम नहीं होने का मतलब अवधारणा को समझना नहीं है। नीचे मुझे आभासी विरासत के चारों ओर सिर लपेटने में मदद मिली है।

डबल एक

सबसे पहले, इस कोड के साथ आभासी विरासत के बिना शुरू करने देता है:

#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, इसलिए हमारे पास इसमें दोगुना Aहै d:

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

Virtuality

अब वर्चुअल इनहेरिटेंस जोड़ने देता है। निम्नलिखित परिवर्तनों के साथ समान कोड नमूने का उपयोग करना:

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डिफॉल्ट कंस्ट्रक्टर के साथ बनाया गया है Bऔर कंस्ट्रक्टरों से पारित मापदंडों की अनदेखी कर रहा है C। जैसा कि अस्पष्टता चली गई है, सभी getX()समान मूल्य वापस करने के लिए कॉल करते हैं:

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और न ही Cमापदंडों के लिए जिम्मेदार है A, यह Dकेवल पूरी तरह से है ।
  2. Cके निर्माण के प्रतिनिधि जाएगा Aकरने के लिए D, लेकिन Bके अपने स्वयं के उदाहरण पैदा करेगा Aवापस इस प्रकार लाने हीरा समस्या
  3. प्रत्यक्ष बच्चे के बजाय ग्रैंडचिल्ड क्लास में बेस क्लास मापदंडों को परिभाषित करना एक अच्छा अभ्यास नहीं है, इसलिए हीरे की समस्या होने पर इसे सहन किया जाना चाहिए और यह उपाय अपरिहार्य है।

10

समस्या यह नहीं है कि संकलक को किस मार्ग का अनुसरण करना चाहिए। समस्या उस पथ का समापन बिंदु है: कलाकारों का परिणाम। जब रूपांतरणों की बात आती है, तो कोई फर्क नहीं पड़ता, केवल अंतिम परिणाम होता है।

यदि आप साधारण विरासत का उपयोग करते हैं, तो प्रत्येक पथ का अपना विशिष्ट समापन बिंदु होता है, जिसका अर्थ है कि कलाकारों का परिणाम अस्पष्ट है, जो समस्या है।

यदि आप वर्चुअल इनहेरिटेंस का उपयोग करते हैं, तो आपको हीरे के आकार का पदानुक्रम मिलता है: दोनों पथ एक ही समापन बिंदु की ओर जाते हैं। इस मामले में पथ को चुनने की समस्या मौजूद नहीं है (या, अधिक सटीक, अब मायने नहीं रखता है), क्योंकि दोनों पथ एक ही परिणाम की ओर ले जाते हैं। परिणाम अब अस्पष्ट नहीं है - यही मायने रखता है। सटीक रास्ता नहीं है।


@ ऑन्ड्रे: कंपाइलर इनहेरिटेंस को कैसे लागू करता है ... मेरा मतलब है कि मुझे आपका तर्क मिलता है और मैं इसे स्पष्ट रूप से समझाने के लिए धन्यवाद देना चाहता हूं..लेकिन यह वास्तव में मदद करेगा यदि आप व्याख्या कर सकते हैं (या एक संदर्भ को इंगित करें) कंपाइलर वास्तव में इनहेरिटेंस को कैसे लागू करता है और जब मैं वर्चुअल इनहेरिटेंस करता हूं तो क्या बदल जाता है
ब्रूस

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"

आभासी वंशानुक्रम केवल दादाजी के दोहराव को हल करता है! लेकिन आपको अभी भी तरीकों को निर्दिष्ट करने के लिए वर्चुअल होने की आवश्यकता है ताकि तरीकों को सही ढंग से प्राप्त किया जा सके ...

हमारी साइट का प्रयोग करके, आप स्वीकार करते हैं कि आपने हमारी Cookie Policy और निजता नीति को पढ़ और समझा लिया है।
Licensed under cc by-sa 3.0 with attribution required.