शुद्ध अमूर्त वर्गों और इंटरफेस का कार्यान्वयन


27

यद्यपि यह C ++ मानक में अनिवार्य नहीं है, लेकिन यह उदाहरण के लिए GCC का तरीका लगता है, शुद्ध अमूर्त लोगों सहित माता-पिता वर्गों को लागू करता है, प्रश्न में वर्ग के प्रत्येक तात्कालिकता में उस अमूर्त वर्ग के लिए वी-टेबल के लिए एक सूचक को शामिल करता है ।

स्वाभाविक रूप से यह इस वर्ग के प्रत्येक उदाहरण के आकार को प्रत्येक अभिभावक वर्ग के लिए एक सूचक द्वारा फुलाता है।

लेकिन मैंने देखा है कि कई सी # कक्षाओं और संरचनाओं में बहुत सारे अभिभावक इंटरफेस होते हैं, जो मूल रूप से शुद्ध अमूर्त वर्ग होते हैं। मुझे आश्चर्य होगा कि अगर हर उदाहरण Decimal, 6 पॉइंटर्स के साथ फूला हुआ था, तो यह विभिन्न इंटरफेस है।

इसलिए यदि C # अलग-अलग तरीके से इंटरफेस करता है, तो यह उन्हें कैसे करता है, कम से कम एक विशिष्ट कार्यान्वयन में (मुझे लगता है कि मानक खुद ऐसे कार्यान्वयन को परिभाषित नहीं कर सकता है)? और क्या किसी भी सी ++ कार्यान्वयन के पास कक्षाओं में शुद्ध आभासी माता-पिता को जोड़ने पर ऑब्जेक्ट आकार ब्लोट से बचने का एक तरीका है?


1
C # ऑब्जेक्ट में आमतौर पर काफी मेटाडेटा संलग्न होता है, हो सकता है कि vtables उस की तुलना में बड़े न हों
अधिकतम

आप idl disassembler के साथ संकलित कोड की जांच के साथ शुरू कर सकते हैं
अधिकतम

C ++ इसका एक महत्वपूर्ण अंश सांख्यिकीय रूप से "इंटरफेस" है। तुलना IComparerकरेंCompare
Caleth

4
जीसीसी, उदाहरण के लिए, कई बेस कक्षाओं वाले वर्गों के लिए प्रति तालिका एक vtable टेबल पॉइंटर (vtables की तालिका के लिए एक सूचक, या एक VTT) का उपयोग करता है। इसलिए, प्रत्येक ऑब्जेक्ट में आपके द्वारा संग्रह किए जा रहे संग्रह के बजाय केवल एक अतिरिक्त पॉइंटर है। शायद इसका मतलब यह है कि कोड खराब तरीके से डिजाइन किए जाने पर भी यह कोई समस्या नहीं है और इसमें बड़े पैमाने पर वर्ग पदानुक्रम शामिल है।
स्टीफन एम। वेब

1
@ StephenM.Webb जहाँ तक मुझे इस SO उत्तर से समझा गया है , VTT का उपयोग केवल आभासी विरासत के साथ निर्माण / विनाश के आदेश के लिए किया जाता है। वे विधि प्रेषण में भाग नहीं लेते हैं और न ही वस्तु में कोई स्थान बचाते हैं। चूँकि C ++ upcasts प्रभावी ढंग से ऑब्जेक्ट स्लाइसिंग करता है, इसलिए वॉयटेबल पॉइंटर को कहीं और लगाना संभव नहीं है, लेकिन ऑब्जेक्ट में (जो MI के लिए ऑब्जेक्ट के बीच में vtable पॉइंटर्स जोड़ता है)। मैंने g++-7 -fdump-class-hierarchyआउटपुट को देखकर सत्यापित किया ।
जुआन

जवाबों:


35

C # और जावा कार्यान्वयन में, वस्तुओं में आमतौर पर अपनी कक्षा के लिए एक एकल सूचक होता है। यह संभव है क्योंकि वे एकल-विरासत भाषाएँ हैं। तब वर्ग संरचना में एकल-वंशानुक्रम पदानुक्रम के लिए व्यवहार्य होता है। लेकिन कॉलिंग इंटरफ़ेस विधियों में कई विरासत की सभी समस्याएं भी हैं। यह आमतौर पर वर्ग संरचना में सभी कार्यान्वित इंटरफेस के लिए अतिरिक्त vtables डालकर हल किया जाता है। यह C ++ में विशिष्ट वर्चुअल इनहेरिटेंस कार्यान्वयन की तुलना में स्थान बचाता है, लेकिन इंटरफ़ेस विधि को अधिक जटिल बना देता है - जिसे कैशिंग द्वारा आंशिक रूप से मुआवजा दिया जा सकता है।

उदाहरण के लिए OpenJDK JVM में, हर वर्ग के सभी कार्यान्वित इंटरफेस के लिए vtables की एक सरणी शामिल हैं (एक अंतरफलक vtable एक कहा जाता है itable )। जब एक इंटरफ़ेस विधि कहा जाता है, तो इस सरणी को उस इंटरफ़ेस के चलने योग्य के लिए रेखीय रूप से खोजा जाता है, फिर उस पठनीय के माध्यम से विधि को भेजा जा सकता है। कैशिंग का उपयोग किया जाता है ताकि प्रत्येक कॉल साइट विधि प्रेषण का परिणाम याद रखे, इसलिए यह खोज केवल तब दोहराई जानी चाहिए जब ठोस वस्तु प्रकार बदल जाती है। विधि प्रेषण के लिए स्यूडोकोड:

// Dispatch SomeInterface.method
Method const* resolve_method(
    Object const* instance, Klass const* interface, uint itable_slot) {

  Klass const* klass = instance->klass;

  for (Itable const* itable : klass->itables()) {
    if (itable->klass() == interface)
      return itable[itable_slot];
  }

  throw ...;  // class does not implement required interface
}

(OpenJDK हॉटस्पॉट दुभाषिया या x86 संकलक में वास्तविक कोड की तुलना करें ।)

C # (या अधिक सटीक, CLR) संबंधित दृष्टिकोण का उपयोग करता है। हालाँकि, यहाँ itables में विधियों के संकेत नहीं हैं, लेकिन स्लॉट मैप हैं: वे कक्षा के मुख्य भाग में प्रविष्टियों को इंगित करते हैं। जावा के साथ, सही चलने योग्य की खोज करने के लिए केवल सबसे खराब स्थिति है, और यह उम्मीद की जाती है कि कॉल साइट पर कैशिंग लगभग हमेशा इस खोज से बच सकती है। CLR JIT- संकलित मशीन कोड को अलग-अलग कैशिंग रणनीतियों के साथ पैच करने के लिए वर्चुअल स्टब डिस्पैच नामक तकनीक का उपयोग करता है। स्यूडोकोड:

Method const* resolve_method(
    Object const* instance, Klass const* interface, uint interface_slot) {

  Klass const* klass = instance->klass;

  // Walk all base classes to find slot map
  for (Klass const* base = klass; base != nullptr; base = base->base()) {
    // I think the CLR actually uses hash tables instead of a linear search
    for (SlotMap const* slot_map : base->slot_maps()) {
      if (slot_map->klass() == interface) {
        uint vtable_slot = slot_map[interface_slot];
        return klass->vtable[vtable_slot];
      }
    }
  }

  throw ...;  // class does not implement required interface
}

OpenJDK-pseudocode में मुख्य अंतर यह है कि OpenJDK में प्रत्येक वर्ग में सीधे या परोक्ष रूप से कार्यान्वित किए गए इंटरफेस की एक सरणी होती है, जबकि CLR केवल उन इंटरफेस के लिए स्लॉट मैप का एक सरणी रखता है जो उस वर्ग में सीधे लागू किए गए थे। इसलिए हमें विरासत के पदानुक्रम को ऊपर की ओर चलने की आवश्यकता है जब तक कि एक स्लॉट मानचित्र नहीं मिलता है। गहरी विरासत पदानुक्रम के लिए यह अंतरिक्ष बचत में परिणाम है। सीएलआर में ये विशेष रूप से प्रासंगिक हैं कि जेनेरिक को किस तरह से लागू किया जाता है: जेनेरिक स्पेशलाइजेशन के लिए, क्लास स्ट्रक्चर की नकल की जाती है और मुख्य वाइबेट में तरीकों को स्पेशलाइजेशन द्वारा प्रतिस्थापित किया जा सकता है। स्लॉट के नक्शे सही व्यवहार्य प्रविष्टियों पर इंगित करना जारी रखते हैं और इसलिए उन्हें किसी वर्ग की सभी सामान्य विशेषज्ञता के बीच साझा किया जा सकता है।

अंतिम नोट के रूप में, इंटरफ़ेस प्रेषण को लागू करने की अधिक संभावनाएं हैं। ऑब्जेक्ट में या कक्षा संरचना में vtable / itable पॉइंटर रखने के बजाय, हम ऑब्जेक्ट को वसा बिंदुओं का उपयोग कर सकते हैं , जो मूल रूप से एक (Object*, VTable*)जोड़ी है। दोष यह है कि यह पॉइंटर्स के आकार को दोगुना करता है और यह अपस्टेक (एक कंक्रीट प्रकार से एक इंटरफ़ेस प्रकार तक) मुक्त नहीं है। लेकिन यह अधिक लचीला है, कम अप्रत्यक्ष है, और इसका मतलब यह भी है कि इंटरफेस को एक वर्ग से बाहरी रूप से लागू किया जा सकता है। संबंधित दृष्टिकोणों का उपयोग गो इंटरफेस, रस्ट ट्रैक्ट और हास्केल टाइपकालेज़ द्वारा किया जाता है।

संदर्भ और आगे पढ़ने:

  • विकिपीडिया: इनलाइन कैशिंग । महंगी पद्धति देखने से बचने के लिए उपयोग किए जा सकने वाले कैशिंग दृष्टिकोणों पर चर्चा करता है। आमतौर पर व्यवहार्य-आधारित प्रेषण के लिए आवश्यक नहीं है, लेकिन उपरोक्त इंटरफ़ेस प्रेषण रणनीतियों जैसे अधिक महंगे प्रेषण तंत्र के लिए बहुत वांछनीय है।
  • OpenJDK Wiki (2013): इंटरफ़ेस कॉल । विचार-विमर्श करता है।
  • पोबर, न्यूवर्ड (2009): एसएससीएलआई 2.0 इंटर्नल्स। पुस्तक का अध्याय 5 स्लॉट के नक्शे पर बहुत विस्तार से चर्चा करता है। प्रकाशित नहीं किया गया था लेकिन लेखकों द्वारा अपने ब्लॉग पर उपलब्ध कराया गया थापीडीएफ लिंक के बाद से ले जाया गया है। यह पुस्तक अब CLR की वर्तमान स्थिति को नहीं दर्शाती है।
  • CoreCLR (2006): वर्चुअल स्टब डिस्पैच । में: रनटाइम की किताब। महंगे लुक्स से बचने के लिए स्लॉट मैप्स और कैशिंग पर चर्चा करता है।
  • कैनेडी, साइमे (2001): नेट कॉमन लैंग्वेज रनटाइम के लिए डिजाइन और जेनेरिक्स का कार्यान्वयन । ( पीडीएफ लिंक )। जेनरिक को लागू करने के लिए विभिन्न दृष्टिकोणों पर चर्चा करता है। जेनेरिक विधि प्रेषण के साथ बातचीत करते हैं क्योंकि विधियाँ विशिष्ट हो सकती हैं इसलिए vtables को फिर से लिखना पड़ सकता है।

धन्यवाद @amon महान जवाब दोनों अतिरिक्त विवरण के लिए आगे देख रहे हैं कि जावा और सीएलआर इसे कैसे प्राप्त करते हैं!
क्लिंटन

@ क्लिंटन ने कुछ संदर्भों के साथ पोस्ट को अपडेट किया। आप VMs के स्रोत कोड को भी पढ़ सकते हैं, लेकिन मुझे इसका अनुसरण करना कठिन लगा। मेरे संदर्भ थोड़े पुराने हैं, यदि आप कुछ नया पाते हैं तो मुझे काफी दिलचस्पी होगी। यह उत्तर मूल रूप से उन नोट्स का एक अंश है जो मैंने एक ब्लॉग पोस्ट के लिए इधर-उधर पड़े थे, लेकिन मुझे इसे प्रकाशित करने के लिए कभी नहीं मिला: /
amon

1
callvirtCEE_CALLVIRTCoreCLR में AKA CIL निर्देश है जो कॉलिंग इंटरफ़ेस विधियों को संभालता है, अगर कोई इस बारे में अधिक पढ़ना चाहता है कि रनटाइम इस सेटअप को कैसे संभालता है।
जूनियर

ध्यान दें कि callओपकोड का उपयोग staticविधियों के लिए किया जाता है , दिलचस्प callvirtरूप से तब भी उपयोग किया जाता है जब वर्ग है sealed
jrh

1
पुन:, "[C #] वस्तुओं में आमतौर पर अपनी कक्षा के लिए एक एकल सूचक होता है ... क्योंकि [C # एक एकल-वारित भाषा है।" सी ++ में भी, बहु-विरासत वाले प्रकारों के जटिल जाले के लिए अपनी क्षमता के साथ, आपको अभी भी केवल उस बिंदु पर एक प्रकार निर्दिष्ट करने की अनुमति है जहां आपका कार्यक्रम एक नया उदाहरण बनाता है। यह संभव होना चाहिए, सिद्धांत रूप में, एक C ++ कंपाइलर और एक रन-टाइम सपोर्ट लाइब्रेरी को डिज़ाइन करने के लिए, जैसे कि कोई भी वर्ग उदाहरण RTTI के एक से अधिक पॉइंटर्स-वर्थ को कभी भी कैरी नहीं करता है।
सोलोमन स्लो

2

स्वाभाविक रूप से यह इस वर्ग के प्रत्येक उदाहरण के आकार को प्रत्येक अभिभावक वर्ग के लिए एक सूचक द्वारा फुलाता है।

अगर 'पैरेंट क्लास' से आपका मतलब 'बेस क्लास' है तो जीसीसी में ऐसा नहीं है (न ही मैं किसी दूसरे कंपाइलर से उम्मीद करता हूं)।

C के मामले में B से व्युत्पन्न A से व्युत्पन्न है जहां A एक बहुरूपी वर्ग है, C के उदाहरण में ठीक एक अनुकूलता होगी।

कंपाइलर के पास A की व्यवहार्यता में डेटा को B के और B के C के C में मिलाने की सभी जानकारी है।

यहाँ एक उदाहरण है: https://godbolt.org/g/sfdtNh

आप देखेंगे कि एक विटेबल का केवल एक इनिशियलाइज़ेशन है।

मैंने एनोटेशन के साथ मुख्य समारोह के लिए असेंबली आउटपुट की प्रतिलिपि बनाई है:

main:
        push    rbx

# allocate space for a C on the stack
        sub     rsp, 16

# initialise c's vtable (note: only one)
        mov     QWORD PTR [rsp+8], OFFSET FLAT:vtable for C+16

# use c    
        lea     rdi, [rsp+8]
        call    do_something(C&)

# destruction sequence through virtual destructor
        mov     QWORD PTR [rsp+8], OFFSET FLAT:vtable for B+16
        lea     rdi, [rsp+8]
        call    A::~A() [base object destructor]

        add     rsp, 16
        xor     eax, eax
        pop     rbx
        ret
        mov     rbx, rax
        jmp     .L10

संदर्भ के लिए पूरा स्रोत:

struct A
{
    virtual void foo() = 0;
    virtual ~A();
};

struct B : A {};

struct C : B {

    virtual void extrafoo()
    {
    }

    void foo() override {
        extrafoo();
    }

};

int main()
{
    extern void do_something(C&);
    auto c = C();
    do_something(c);
}

यदि हम एक उदाहरण लेते हैं जहां उपवर्ग दो आधार वर्गों से सीधे विरासत में मिलता है , class Derived : public FirstBase, public SecondBaseतो दो vtables हो सकते हैं। आप g++ -fdump-class-hierarchyक्लास लेआउट (मेरे लिंक्ड ब्लॉग पोस्ट में भी दिखाया गया है) देखने के लिए दौड़ सकते हैं । Godbolt तब कॉल से पहले एक अतिरिक्त पॉइंटर वृद्धि दिखाता है ताकि 2 vtable का चयन किया जा सके।
आमोन
हमारी साइट का प्रयोग करके, आप स्वीकार करते हैं कि आपने हमारी Cookie Policy और निजता नीति को पढ़ और समझा लिया है।
Licensed under cc by-sa 3.0 with attribution required.