Saf soyut sınıfların ve arayüzlerin uygulanması


27

Bu, C ++ standardında zorunlu olmamasına rağmen, örneğin GCC'nin, saf soyut olanlar da dahil olmak üzere ebeveyn sınıflarını uygulama biçimi, söz konusu sınıfın her örneğine bu soyut sınıf için v-tablosuna bir işaretçi eklemek yoluyla görünmektedir. .

Doğal olarak, bu, bu sınıfın her örneğinin boyutunu, sahip olduğu her ana sınıf için bir işaretçi ile keser.

Fakat birçok C # sınıfının ve yapısının, temelde saf soyut sınıflar olan birçok üst arayüze sahip olduğunu fark ettim. Söyleyeceğim her Decimalşey, çeşitli arayüzlere 6 işaretçi ile şişirilmiş olsaydı şaşırırdım.

Öyleyse, eğer C # farklı arayüzler yapıyorsa, bunları en azından tipik bir uygulamada nasıl yapar (standardın kendisinin böyle bir uygulamayı tanımlayamayacağını anlıyorum)? Ve herhangi bir C ++ uygulamasının, sınıflara saf sanal ebeveynler eklerken nesne boyutu şişmesinden kaçınmanın bir yolu var mı?


1
C # nesneleri genellikle oldukça fazla ekli meta verilere sahiptir, belki değişkenler buna kıyasla o kadar büyük değildir
max630

derlenmiş kodun idl disassembler ile incelenmesi ile başlayabilirsin
max630

C ++ statik olarak "arayüzler" in önemli bir bölümünü yapar. Karşılaştırma ICompareriçinCompare
Caleth

4
Örneğin, GCC, çoklu taban sınıflarına sahip sınıflar için nesne başına vtable tablo işaretçisi (bir tablo tablosuna işaretçi veya VTT) kullanır. Bu nedenle, her nesnenin hayal ettiğiniz koleksiyondan ziyade yalnızca bir tane ekstra gösterici vardır. Belki de bu, pratikte, kodun zayıf tasarlanması ve hatta büyük bir sınıf hiyerarşisi olsa bile sorun olmadığı anlamına gelir.
Stephen M. Webb,

1
@ StephenM.Webb Bu SO cevabından anladığım kadarıyla , VTT'ler yalnızca sanal miras ile inşaat / yıkım siparişi vermek için kullanılır. Yöntem gönderimine katılmazlar ve nesnenin kendisinde herhangi bir yer kazanmazlar. C ++ yukarı kaydırma işlemleri nesne dilimlemeyi etkin bir şekilde gerçekleştirdiğinden, vtable işaretçisini başka bir yere koymak mümkün değildir (MI için nesnenin ortasına seçilebilir işaretçiler ekler). g++-7 -fdump-class-hierarchyÇıkışa bakarak doğruladım .
amon

Yanıtlar:


35

C # ve Java uygulamalarında, nesneler genellikle sınıfında tek bir işaretçiye sahiptir. Bu mümkündür çünkü bunlar tek miras dilleridir. Sınıf yapısı daha sonra tek miras hiyerarşisi için vtable'ı içerir. Ancak, arabirim arama yöntemleri de çoklu kalıtımın tüm sorunlarına sahiptir. Bu tipik olarak, tüm uygulanmış arayüzler için sınıf yapısına ilave boşluklar koyarak çözülür. Bu, C ++ 'daki tipik sanal miras uygulamalarına kıyasla yer kazandırır, ancak arabirim yönteminin daha karmaşık olmasını sağlar; bu, önbelleğe alma ile kısmen telafi edilebilir.

Örneğin, OpenJDK JVM'de, her bir sınıf tüm uygulanan arayüzler için bir dizi değişken içerir (bir arayüz değişkenine bir değişken adı verilir ). Bir arabirim yöntemi çağrıldığında, bu dizi, bu arabirimin yinelemesi için doğrusal olarak aranır, daha sonra yöntem bu yinelemenin içinden gönderilebilir. Önbellekleme, her bir çağrı sitesinin yöntem gönderiminin sonucunu hatırlaması için kullanılır, bu nedenle bu arama yalnızca somut nesne türü değiştiğinde tekrarlanmalıdır. Yöntem gönderme için sözde kod:

// 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 yorumlayıcısındaki veya x86 derleyicisindeki gerçek kodu karşılaştırın .)

C # (veya daha doğrusu CLR) ilgili bir yaklaşım kullanır. Bununla birlikte, buradaki dökülenler yöntemlere işaretçiler içermezler, fakat slot haritalarıdırlar: sınıfın ana oyunda bulunan girişleri gösterirler. Java’da olduğu gibi, doğru itable’ın aranması sadece en kötü durum senaryosudur ve çağrı sitesinde önbelleğe almanın bu aramayı neredeyse her zaman önleyebileceği beklenir. CLR, JIT tarafından derlenen makine kodunu farklı önbellekleme stratejileriyle düzeltmek için Sanal Saplama Gönderme adlı bir teknik kullanır. 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'un temel farkı, OpenJDK'de her bir sınıfın doğrudan veya dolaylı olarak uygulanan tüm arayüzlerin bir dizisine sahip olmasıdır, CLR ise yalnızca bu sınıfta doğrudan uygulanan arayüzler için bir dizi slot haritasını tutar. Bu nedenle, miras hiyerarşisini bir yuva haritası bulunana kadar yukarıya doğru yürümemiz gerekir. Derin kalıtım hiyerarşileri için bu, yerden tasarruf sağlar. Bunlar, jenerik ilaçların nasıl uygulandığına bağlı olarak CLR ile ilgilidir: genel bir uzmanlık için, sınıf yapısı kopyalanır ve ana listedeki yöntemler uzmanlıklarla değiştirilebilir. Slot haritaları doğru oy girişlerini göstermeye devam eder ve bu nedenle bir sınıfın tüm genel uzmanlıkları arasında paylaşılabilir.

Bitiş notu olarak, arayüz gönderimini uygulamak için daha fazla olasılık vardır. Vtable / değişken tabloyu nesneye veya sınıf yapısına yerleştirmek yerine , temelde bir çift olan nesneye yağ işaretçileri kullanabiliriz (Object*, VTable*). Dezavantajı bunun işaretçilerin boyutunu iki katına çıkarması ve üst üste bindirmelerin (somut bir tipten bir arayüz tipine) serbest olmamasıdır. Ancak daha esnektir, daha az dolaylıdır ve ayrıca arayüzlerin bir sınıftan harici olarak uygulanabileceği anlamına gelir. İlgili yaklaşımlar Go arayüzleri, Rust özellikleri ve Haskell sınıfları tarafından kullanılır.

Kaynaklar ve daha fazla okuma:

  • Wikipedia: Satır içi önbellekleme . Pahalı yöntem aramalarını önlemek için kullanılabilecek önbellek yaklaşımlarını tartışır. Tipik olarak vtable tabanlı sevkiyat için gerekli değildir, ancak yukarıdaki arayüz gönderme stratejileri gibi daha pahalı sevkiyat mekanizmaları için çok arzu edilir.
  • OpenJDK Wiki (2013): Arayüz Çağrıları . Buzları tartışır.
  • Pobar, Neward (2009): SSCLI 2.0 Internals. Kitabın 5. bölümü, slot haritalarını ayrıntılı olarak ele alıyor. Hiç bir zaman yayınlanmadı ancak yazarlar tarafından bloglarında yayınlandı . PDF bağlantı beri taşındı. Bu kitap muhtemelen CLR'nin şu anki durumunu yansıtmamaktadır.
  • CoreCLR (2006): Sanal Saplama Gönderimi . In: Çalışma Zamanı Kitabı. Pahalı aramaları önlemek için slot haritalarını ve önbelleklemeyi tartışır.
  • Kennedy, Syme (2001): .NET Ortak Dil Çalışma Zamanı için Jenerik Tasarım ve Uygulama . ( PDF bağlantısı ). Jenerikliği uygulamak için çeşitli yaklaşımları tartışır. Jenerikler metot gönderimi ile etkileşime girer, çünkü metotlar özelleştirilebilir, böylece vtables yeniden yazılabilir.

Thanks @amon harika cevap, hem Java hem de CLR'nin bunu nasıl başardığına dair ekstra ayrıntıları dört gözle bekliyor!
Clinton,

@Clinton Gönderiyi bazı referanslarla güncelleştirdim. VM'lerin kaynak kodunu da okuyabilirsiniz, ancak takip etmekte zorlandım. Referanslarım biraz eski, daha yeni bir şey bulursanız çok ilgilenirim. Bu cevap temelde bir blog yazısı için yattığım notların bir kısmıdır, ancak yayınlamak için hiç
bulamadım

1
callvirtCEE_CALLVIRTCoreCLR'deki AKA , çalışma zamanının bu kurulumu nasıl işlediği hakkında daha fazla bilgi edinmek isteyen, arama arayüzü yöntemlerini kullanan CIL talimatıdır.
jrh

Not callişlemkodu için kullanılan staticyöntemler ilginci callvirtsınıf olsa bile kullanılır sealed.
jrh

1
Re, "[C #] nesneleri genellikle sınıfında tek bir işaretçiye sahiptir ... çünkü [C # bir] tek miras dilidir." C ++ 'da bile, çoklu kalıtsal türlerin karmaşık ağları için tüm potansiyeliyle , programınızın yeni bir örnek oluşturduğu noktada yalnızca bir tür belirtmenize izin verilir . Teorik olarak, bir C ++ derleyicisi ve çalışma zamanı destek kütüphanesi tasarlamak mümkün olmamalı, böylece hiçbir sınıf örneği hiçbir RTTI değerinde işaretçi taşımaz.
Solomon Yavaş

2

Doğal olarak, bu, bu sınıfın her örneğinin boyutunu, sahip olduğu her ana sınıf için bir işaretçi ile keser.

Eğer 'ebeveyn sınıf' ile 'temel sınıf' demek istiyorsan, gcc'de durum böyle değildir (ne de başka bir derleyicide beklenir).

C'nin B'den türetilmesi durumunda, A'nın polimorfik bir sınıf olduğu A'dan türetilirse, C örneği tam olarak bir oylanabilir olacaktır.

Derleyici, A’daki değişkenleri B’ye ve B’ye C’lere eklemek için gereken tüm bilgileri içerir.

İşte bir örnek: https://godbolt.org/g/sfdtNh

Bir vote için yalnızca bir başlangıç ​​olduğunu göreceksiniz.

Ana işlev için derleme çıktısını burada ek açıklamalarla kopyaladım:

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

Referans için komple kaynak:

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

Biz alırsak iki temel sınıflar doğrudan alt sınıf devralır örneği gibi class Derived : public FirstBase, public SecondBasedaha sonra iki vtables olabilir. g++ -fdump-class-hierarchySınıf düzenini görmek için koşabilirsiniz (ayrıca bağlantılı blog yazımda da gösterilir). Godbolt daha sonra 2. vtable'ı seçmek için çağrıdan önce ilave bir işaretçi artışı gösterir .
amon
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.