การใช้คลาสนามธรรมและอินเทอร์เฟซที่เป็นนามธรรม


27

แม้ว่าสิ่งนี้จะไม่ได้บังคับในมาตรฐาน C ++ แต่ดูเหมือนว่าวิธี GCC เช่นใช้คลาสผู้ปกครองรวมถึงนามธรรมที่บริสุทธิ์คือการรวมตัวชี้ไปยังตาราง v สำหรับคลาสนามธรรมนั้นในทุกอินสแตนซ์ของคำถาม .

โดยปกติแล้วสิ่งนี้จะขยายขนาดของอินสแตนซ์ของคลาสนี้โดยตัวชี้สำหรับคลาสพาเรนต์ทั้งหมดที่มี

แต่ฉันสังเกตเห็นว่าคลาส C และโครงสร้างจำนวนมากมีอินเทอร์เฟซหลักมากมายซึ่งโดยทั่วไปแล้วเป็นคลาสนามธรรมที่บริสุทธิ์ ฉันจะแปลกใจถ้าทุกครั้งที่พูดDecimalถูกป่องด้วย 6 ตัวชี้ไปยังทุกมันเป็นอินเทอร์เฟซต่าง ๆ

ดังนั้นถ้า C # ทำอินเตอร์เฟสแตกต่างกันอย่างน้อยในการใช้งานทั่วไป (ฉันเข้าใจว่ามาตรฐานตัวเองอาจไม่ได้กำหนดการนำไปใช้) และการใช้งาน C ++ มีวิธีหลีกเลี่ยงการขยายขนาดวัตถุเมื่อเพิ่มผู้ปกครองเสมือนบริสุทธิ์ให้กับชั้นเรียนหรือไม่?


1
วัตถุ C # มักจะมีเมตาดาต้าติดอยู่ค่อนข้างมากบางที vtables อาจไม่ใหญ่มากเมื่อเทียบกับที่
max630

คุณสามารถเริ่มต้นด้วยการตรวจสอบโค้ดที่คอมไพล์ด้วย idl disassembler
max630

C ++ ทำส่วนสำคัญของ "อินเตอร์เฟส" แบบคงที่ เปรียบเทียบIComparerกับCompare
Caleth

4
ตัวอย่างเช่น GCC ใช้ตัวชี้ตาราง vtable (ตัวชี้ไปยังตารางของ vtables หรือ VTT) ต่อวัตถุสำหรับคลาสที่มีคลาสพื้นฐานหลายคลาส ดังนั้นแต่ละวัตถุจะมีตัวชี้พิเศษเพียงตัวเดียวแทนที่จะเป็นคอลเล็กชันที่คุณจินตนาการ บางทีนั่นอาจหมายถึงในทางปฏิบัติมันไม่ใช่ปัญหาแม้ว่ารหัสนั้นได้รับการออกแบบมาไม่ดีและมีลำดับชั้นขนาดใหญ่ที่เกี่ยวข้อง
สตีเฟ่นเมตรเวบบ์

1
@ StephenM.Webb เท่าที่ฉันเข้าใจจากคำตอบ SO นี้ VTTs จะใช้สำหรับการสั่งซื้อการก่อสร้าง / ทำลายด้วยมรดกเสมือนเท่านั้น พวกเขาไม่ได้มีส่วนร่วมในวิธีการจัดส่งและไม่ได้จบลงด้วยการประหยัดพื้นที่ในวัตถุเอง เนื่องจากการอัปโหลด C ++ ดำเนินการแบ่งวัตถุอย่างมีประสิทธิภาพจึงไม่สามารถวางตัวชี้ vtable ได้ทุกที่ยกเว้นในวัตถุ (ซึ่งสำหรับ MI จะเพิ่มตัวชี้ vtable ไว้กลางวัตถุ) ฉันตรวจสอบโดยดูที่g++-7 -fdump-class-hierarchyผลลัพธ์
amon

คำตอบ:


35

ในการใช้งาน C # และ Java วัตถุมักจะมีตัวชี้เดียวกับคลาส สิ่งนี้เป็นไปได้เพราะเป็นภาษาที่สืบทอดเดี่ยว โครงสร้างคลาสนั้นมี vtable สำหรับลำดับชั้นการสืบทอดเดี่ยว แต่วิธีการโทรอินเตอร์เฟสมีปัญหาทั้งหมดของการสืบทอดหลายรายการเช่นกัน โดยทั่วไปจะถูกแก้ไขโดยการเพิ่ม vtables เพิ่มเติมสำหรับอินเตอร์เฟสที่นำไปใช้ทั้งหมดลงในโครงสร้างคลาส สิ่งนี้ช่วยประหยัดพื้นที่เมื่อเทียบกับการใช้งานการสืบทอดเสมือนโดยทั่วไปใน C ++ แต่ทำให้วิธีการอินเทอร์เฟซการส่งซับซ้อนมากขึ้น - ซึ่งสามารถชดเชยบางส่วนได้โดยการแคช

เช่นใน OpenJDK JVM แต่ละคลาสจะมีอาร์เรย์ของ vtables สำหรับอินเตอร์เฟสที่ใช้งานทั้งหมด (อินเตอร์เฟส vtable เรียกว่าitable ) เมื่อมีการเรียกวิธีการอินเทอร์เฟซอาร์เรย์นี้ถูกค้นหาเป็นเชิงเส้นเพื่อหาได้ของอินเทอร์เฟซนั้นจากนั้นวิธีสามารถถูกส่งผ่าน itable นั้น การแคชถูกใช้เพื่อให้ไซต์การโทรแต่ละครั้งจดจำผลลัพธ์ของวิธีการแจกจ่ายดังนั้นการค้นหานี้จะต้องทำซ้ำเมื่อการเปลี่ยนแปลงประเภทวัตถุที่เป็นรูปธรรมเท่านั้น Pseudocode สำหรับวิธีการจัดส่ง:

// 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 HotSpot หรือคอมไพเลอร์ x86 )

C # (หรือแม่นยำกว่า CLR) ใช้วิธีการที่เกี่ยวข้อง อย่างไรก็ตามที่นี่ itables ไม่ได้มีตัวชี้ไปยังวิธีการ แต่เป็นแผนที่แมป: พวกเขาชี้ไปที่รายการใน vtable หลักของชั้นเรียน เช่นเดียวกับ Java การค้นหา itable ที่ถูกต้องเป็นเพียงสถานการณ์ที่เลวร้ายที่สุดและคาดว่าการแคชที่ไซต์การโทรสามารถหลีกเลี่ยงการค้นหานี้ได้เกือบตลอดเวลา CLR ใช้เทคนิคที่เรียกว่า Virtual Stub Dispatch เพื่อแก้ไขรหัสเครื่องที่คอมไพล์ด้วย JIT ด้วยกลยุทธ์การแคชที่แตกต่างกัน pseudocode:

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 เก็บเฉพาะอาเรย์ของแผนที่สล็อตสำหรับอินเตอร์เฟสที่นำมาใช้โดยตรงในคลาสนั้น เราต้องเดินตามลำดับชั้นการสืบทอดขึ้นไปจนกว่าจะพบสล็อตแมพ สำหรับลำดับชั้นการสืบทอดที่ลึกส่งผลให้ประหยัดพื้นที่ สิ่งเหล่านี้มีความเกี่ยวข้องเป็นพิเศษใน CLR เนื่องจากวิธีการใช้งาน generics: สำหรับความเชี่ยวชาญทั่วไปโครงสร้างคลาสจะถูกคัดลอกและวิธีการใน vtable หลักอาจถูกแทนที่ด้วยความเชี่ยวชาญ แมปสล็อตจะยังคงชี้ไปที่รายการ vtable ที่ถูกต้องและสามารถแชร์ระหว่างความเชี่ยวชาญทั่วไปทั้งหมดของคลาสได้

ในฐานะที่เป็นบันทึกสิ้นสุดมีความเป็นไปได้มากขึ้นในการใช้การจัดส่งส่วนต่อประสาน แทนที่จะวางตัวชี้ vtable / itable ไว้ในวัตถุหรือในโครงสร้างของคลาสเราสามารถใช้พอยน์เตอร์พอยต์ไปยังวัตถุที่เป็น(Object*, VTable*)คู่ ข้อเสียเปรียบคือสิ่งนี้จะเพิ่มขนาดของพอยน์เตอร์เป็นสองเท่าและอัพคาสต์ (จากประเภทคอนกรีตไปเป็นประเภทอินเตอร์เฟส) นั้นไม่ฟรี แต่มันมีความยืดหยุ่นมากกว่ามีทิศทางที่น้อยกว่าและยังหมายความว่าอินเตอร์เฟสสามารถนำไปใช้ภายนอกจากคลาสได้ วิธีการที่เกี่ยวข้องนั้นถูกใช้โดยส่วนต่อประสานคุณลักษณะลักษณะสนิมและประเภทของ Haskell

การอ้างอิงและการอ่านเพิ่มเติม:

  • : วิกิพีเดียแคช Inline อธิบายถึงวิธีการแคชที่สามารถใช้เพื่อหลีกเลี่ยงวิธีการค้นหาที่แพง โดยทั่วไปแล้วไม่จำเป็นต้องใช้สำหรับการแจกจ่ายแบบอิง vtable แต่เป็นที่ต้องการอย่างมากสำหรับกลไกการจัดส่งที่มีราคาแพงกว่าเช่นกลยุทธ์การส่งต่ออินเตอร์เฟสด้านบน
  • OpenJDK วิกิพีเดีย (2013): โทรอินเตอร์เฟซ กล่าวถึง itables
  • Pobar, Neward (2009): SSCLI 2.0 Internals บทที่ 5 ของหนังสือเล่มนี้กล่าวถึงแผนที่สล็อตโดยละเอียด ก็ไม่เคยตีพิมพ์ แต่ทำใช้ได้โดยผู้เขียนในบล็อกของพวกเขา เชื่อมโยงรูปแบบไฟล์ PDFได้ย้ายตั้งแต่ หนังสือเล่มนี้อาจไม่สะท้อนสถานะปัจจุบันของ CLR อีกต่อไป
  • CoreCLR (2006): เสมือนกุดส่ง ใน: หนังสือของรันไทม์ พูดถึงแผนที่สล็อตและแคชเพื่อหลีกเลี่ยงการค้นหาราคาแพง
  • เคนเนดีไซย์ (2001): การออกแบบและการดำเนินงานของ Generics สำหรับ .NET ทั่วไปรันไทม์ภาษา ( ลิงก์ PDF ) กล่าวถึงวิธีการต่าง ๆ ในการใช้งานข้อมูลทั่วไป Generics โต้ตอบกับวิธีการจัดส่งเพราะวิธีการอาจมีความเชี่ยวชาญดังนั้น vtables อาจจะต้องมีการเขียนใหม่

ขอบคุณ @ คำตอบที่ยอดเยี่ยมรอคอยที่จะมีรายละเอียดเพิ่มเติมทั้งวิธีการที่ Java และ CLR บรรลุเป้าหมาย!
Clinton

@Clinton ฉันอัปเดตโพสต์โดยอ้างอิงบางอย่าง คุณยังสามารถอ่านซอร์สโค้ดของ VMs ได้ แต่ฉันพบว่ามันยากที่จะติดตาม ข้อมูลอ้างอิงของฉันค่อนข้างเก่าถ้าคุณพบสิ่งใหม่ฉันจะค่อนข้างสนใจ คำตอบนี้เป็นข้อความที่ตัดตอนมาจากข้อความที่ฉันโกหกรอบ ๆ โพสต์บล็อก แต่ฉันไม่เคยได้ไปรอบ ๆ เพื่อเผยแพร่: /
amon

1
callvirtAKA CEE_CALLVIRTใน CoreCLRเป็นคำสั่ง CIL ที่จัดการวิธีการโทรติดต่อหากใครต้องการอ่านเพิ่มเติมเกี่ยวกับวิธีรันไทม์จัดการการตั้งค่านี้
jrh

หมายเหตุว่าcallopcodeใช้สำหรับstaticวิธีการที่น่าสนใจจะใช้แม้ว่าระดับคือcallvirt sealed
jrh

1
เรื่องวัตถุ "[C #] โดยทั่วไปจะมีตัวชี้เดียวในระดับ ... เพราะ [C # เป็น] ภาษาสืบทอดเดี่ยว" แม้ใน C ++ ที่มีศักยภาพทั้งหมดสำหรับเว็บที่ซับซ้อนซึ่งมีหลายประเภทที่สืบทอดคุณยังคงได้รับอนุญาตให้ระบุประเภทเดียวที่จุดที่โปรแกรมของคุณสร้างอินสแตนซ์ใหม่ ในทางทฤษฎีแล้วมันควรจะเป็นไปได้ในการออกแบบคอมไพเลอร์ C ++ และไลบรารีการสนับสนุนแบบรันไทม์ซึ่งไม่มีอินสแตนซ์ของคลาสที่เคยมีพอยน์เตอร์พอยต์มากกว่าหนึ่งตัวของ RTTI
โซโลมอนช้า

2

โดยปกติแล้วสิ่งนี้จะขยายขนาดของอินสแตนซ์ของคลาสนี้โดยตัวชี้สำหรับคลาสพาเรนต์ทั้งหมดที่มี

ถ้าโดย 'parent class' คุณหมายถึง 'base class' นี่ไม่ใช่กรณีใน gcc (หรือฉันคาดหวังในคอมไพเลอร์อื่น ๆ )

ในกรณีของ C เกิดขึ้นจาก B มาจาก A โดยที่ A คือคลาส polymorphic อินสแตนซ์ C จะมี vtable เพียงตัวเดียว

คอมไพเลอร์มีข้อมูลทั้งหมดที่จำเป็นในการรวมข้อมูลใน vtable ของ A เข้ากับ B's และ B's เข้า C

นี่คือตัวอย่าง: https://godbolt.org/g/sfdtNh

คุณจะเห็นว่ามีการเริ่มต้นเพียงหนึ่ง vtable

ฉันได้คัดลอกเอาท์พุทการชุมนุมสำหรับฟังก์ชั่นหลักที่นี่พร้อมคำอธิบายประกอบ

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);
}

ถ้าเรานำตัวอย่างที่ subclass สืบทอดโดยตรงจากคลาสพื้นฐานสองคลาสเช่นclass Derived : public FirstBase, public SecondBaseนั้นอาจมี vtables สองอัน คุณสามารถเรียกใช้g++ -fdump-class-hierarchyเพื่อดูเลย์เอาต์ของคลาส (แสดงในบล็อกโพสต์ที่เชื่อมโยงของฉันด้วย) Godbolt จะแสดงการเพิ่มพอยน์เตอร์เพิ่มเติมก่อนการโทรเพื่อเลือก vtable ที่ 2
amon
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.