Sanal işlevler ve vtable nasıl uygulanır?


110

C ++ 'da hangi sanal işlevlerin olduğunu hepimiz biliyoruz, ancak bunlar derin bir düzeyde nasıl uygulanır?

Vtable değiştirilebilir veya çalışma zamanında doğrudan erişilebilir mi?

Vtable tüm sınıflar için mi yoksa yalnızca en az bir sanal işlevi olanlarda mı var?

Soyut sınıflar, en az bir girdinin işlev işaretçisi için basitçe bir NULL'a sahip mi?

Tek bir sanal işleve sahip olmak tüm sınıfı yavaşlatır mı? Yoksa sadece sanal olan işleve çağrı mı? Ve sanal işlev gerçekten üzerine yazılırsa veya yazılmazsa hız etkilenir mi, yoksa sanal olduğu sürece bunun bir etkisi olmaz mı?


2
Başyapıt okuyarak öner Inside the C++ Object Modeltarafından Stanley B. Lippman. (Bölüm 4.2, sayfa 124-131)
smwikipedia

Yanıtlar:


123

Sanal işlevler derin bir düzeyde nasıl uygulanır?

Gönderen "C ++ Sanal Fonksiyonlar" :

Bir programın bildirilmiş sanal bir işlevi olduğunda, sınıf için av - table oluşturulur. V-tablosu, bir veya daha fazla sanal işlev içeren sınıflar için sanal işlevlerin adreslerinden oluşur. Sanal işlevi içeren sınıfın nesnesi, bellekteki sanal tablonun temel adresine işaret eden bir sanal işaretçi içerir. Sanal bir işlev çağrısı olduğunda, v-tablosu işlev adresini çözümlemek için kullanılır. Bir veya daha fazla sanal işlev içeren sınıfın bir nesnesi, bellekteki nesnenin en başında vptr adı verilen bir sanal işaretçi içerir. Dolayısıyla, bu durumda nesnenin boyutu, işaretçinin boyutu ile artar. Bu vptr, bellekteki sanal tablonun temel adresini içerir. Sanal tabloların sınıfa özgü olduğuna dikkat edin. İçerdiği sanal işlevlerin sayısına bakılmaksızın bir sınıf için yalnızca bir sanal tablo vardır. Bu sanal tablo da, sınıfın bir veya daha fazla sanal işlevinin temel adreslerini içerir. Bir nesnede sanal bir işlev çağrıldığında, o nesnenin vptr'si bellekteki bu sınıf için sanal tablonun temel adresini sağlar. Bu tablo, o sınıfın tüm sanal işlevlerinin adreslerini içerdiği için işlev çağrısını çözmek için kullanılır. Bir sanal işlev çağrısı sırasında dinamik bağlama bu şekilde çözülür. bu nesnenin vptr'si, bellekteki bu sınıf için sanal tablonun temel adresini sağlar. Bu tablo, o sınıfın tüm sanal işlevlerinin adreslerini içerdiği için işlev çağrısını çözmek için kullanılır. Bir sanal işlev çağrısı sırasında dinamik bağlama bu şekilde çözülür. bu nesnenin vptr'si, bellekteki bu sınıf için sanal tablonun temel adresini sağlar. Bu tablo, o sınıfın tüm sanal işlevlerinin adreslerini içerdiği için işlev çağrısını çözmek için kullanılır. Bir sanal işlev çağrısı sırasında dinamik bağlama bu şekilde çözülür.

Vtable değiştirilebilir veya çalışma zamanında doğrudan erişilebilir mi?

Evrensel olarak cevabın "hayır" olduğuna inanıyorum. Vtable'ı bulmak için biraz bellek karıştırabilirsiniz, ancak yine de işlev imzasının onu adlandırmak için neye benzediğini bilemezsiniz. Bu yetenekle elde etmek isteyeceğiniz her şey (dilin desteklediği), vtable'a doğrudan erişmeden veya çalışma zamanında onu değiştirmeden mümkün olmalıdır. Ayrıca not, C ++ dil spec yok vtables gerekli olduğunu belirtmek - ancak çoğu derleyiciler sanal fonksiyonları uygulamak nasıl olduğunu.

Vtable tüm nesneler için mi yoksa yalnızca en az bir sanal işlevi olanlarda mı var?

Ben inanıyorum burada cevap Spec ilk etapta vtables gerektirmediğinden "o uygulanmasına bağlı" dır. Bununla birlikte, pratikte, tüm modern derleyicilerin yalnızca bir sınıfın en az 1 sanal işlevi varsa bir vtable oluşturduğuna inanıyorum. Vtable ile ilişkili bir alan ek yükü ve sanal olmayan bir işlevi yerine sanal bir işlevi çağırmakla ilişkili bir zaman ek yükü vardır.

Soyut sınıflar, en az bir girdinin işlev işaretçisi için basitçe bir NULL'a sahip mi?

Cevap, dil spesifikasyonu tarafından belirtilmediği için uygulamaya bağlı olmasıdır. Saf sanal işlevi çağırmak, tanımlanmamışsa (genellikle değildir) tanımsız davranışla sonuçlanır (ISO / IEC 14882: 2003 10.4-2). Pratikte, işlev için vtable'da bir yuva tahsis eder, ancak ona bir adres atamaz. Bu, türetilmiş sınıfların işlevi uygulamak ve vtable'ı tamamlamasını gerektiren vtable'ı eksik bırakır. Bazı uygulamalar vtable girişine basitçe bir NULL gösterici yerleştirir; diğer uygulamalar, bir iddiaya benzer bir şey yapan kukla bir yönteme bir işaretçi yerleştirir.

Soyut bir sınıfın, saf bir sanal işlev için bir uygulama tanımlayabileceğini, ancak bu işlevin yalnızca nitelikli kimlik sözdizimi ile çağrılabileceğini unutmayın (yani, bir temel sınıf yönteminden bir temel sınıf yöntemini çağırmaya benzer şekilde, yöntem adında sınıfı tam olarak belirtmek) Türetilmiş sınıf). Bu, kullanımı kolay bir varsayılan uygulama sağlamak için yapılırken, yine de türetilmiş bir sınıfın bir geçersiz kılma sağlamasını gerektirir.

Tek bir sanal işleve sahip olmak tüm sınıfı mı yoksa yalnızca sanal işlev çağrısını mı yavaşlatır?

Bu benim bilgimin sınırına geliyor, bu yüzden eğer yanılıyorsam biri bana yardım etsin!

Ben inanıyorum zamanlı performans sanal işlevi vs olmayan bir sanal fonksiyon çağırarak ilişkin isabet sınıf deneyimi sanal olduğunu sadece fonksiyonları. Sınıf için ek alan her iki şekilde de oradadır. Bir vtable varsa, nesne başına bir değil, sınıf başına yalnızca 1 tane olduğunu unutmayın .

Sanal işlev gerçekten geçersiz kılınırsa veya geçersiz kılınırsa hız etkilenir mi, yoksa sanal olduğu sürece bunun bir etkisi olmaz mı?

Geçersiz kılınan bir sanal işlevin yürütme süresinin, temel sanal işlevi çağırmaya kıyasla azaldığına inanmıyorum. Bununla birlikte, türetilmiş sınıf için temel sınıfa karşı başka bir vtable tanımlamakla ilişkili sınıf için ek bir alan yükü vardır.

Ek kaynaklar:

http://www.codersource.net/published/view/325/virtual_functions_in.aspx (geri dönüş makinesi aracılığıyla)
http://en.wikipedia.org/wiki/Virtual_table
http://www.codesourcery.com/public/ cxx-abi / abi.html # vtable


2
Bir derleyicinin ihtiyaç duymayan bir nesneye gereksiz bir vtable işaretçisi koyması Stroustrup'ın C ++ felsefesine uygun olmaz. Kural, siz istemedikçe C'de olmayan ek yükü almamanızdır ve derleyicilerin bunu bozması kabalıktır.
Steve Jessop

3
Sanal işlevler olmadığında kendini ciddiye alan herhangi bir derleyicinin bir vtable kullanmasının aptalca olacağını kabul ediyorum. Bununla birlikte, bildiğim kadarıyla, C ++ standardının gerektirmediğini / gerektirmediğini, bu yüzden ona bağlı kalmadan önce uyarılmanın önemli olduğunu hissettim.
Zach Burlingame

8
Sanal işlevler bile sanal olmayan olarak adlandırılabilir. Bu aslında oldukça yaygındır: nesne yığın üzerindeyse, kapsam dahilinde derleyici tam türü bilir ve vtable aramasını optimize eder. Bu özellikle aynı yığın kapsamında çağrılması gereken dtor için geçerlidir.
MSalters

1
En az bir sanal işlevi olan bir sınıf, her nesnenin bir vtable'a sahip olduğuna ve tüm sınıf için bir vtable olmadığına inanıyorum.
Asaf R

3
Ortak uygulama: Her nesnenin bir vtable'a bir işaretçisi vardır; sınıf masanın sahibidir. Yapım büyüsü, temel ctor bittikten sonra türetilmiş ctor'daki vtable işaretçisini güncellemekten ibarettir.
MSalters

31
  • Vtable değiştirilebilir veya çalışma zamanında doğrudan erişilebilir mi?

Taşınabilir değil, ama kirli numaralara aldırmazsanız, elbette!

UYARI : Bu tekniğin çocuklar, 969 yaşın altındaki yetişkinler veya Alpha Centauri'den küçük tüylü yaratıklar tarafından kullanılması tavsiye edilmez . Yan etkiler arasında burnunuzdan fırlayan iblisler , Yog-Sothoth'un sonraki tüm kod incelemelerinde gerekli bir onaylayıcı olarak aniden ortaya çıkması IHuman::PlayPiano()veya tüm mevcut örneklere geriye dönük olarak eklenmesi yer alabilir ]

Gördüğüm çoğu derleyicide, vtbl * nesnenin ilk 4 baytıdır ve vtbl içerikleri basitçe oradaki üye işaretçilerinden oluşan bir dizidir (genellikle bildirildikleri sırayla, temel sınıfın ilkiyle). Elbette başka olası düzenler de var, ancak genel olarak gözlemlediğim şey bu.

class A {
  public:
  virtual int f1() = 0;
};
class B : public A {
  public:
  virtual int f1() { return 1; }
  virtual int f2() { return 2; }
};
class C : public A {
  public:
  virtual int f1() { return -1; }
  virtual int f2() { return -2; }
};

A *x = new B;
A *y = new C;
A *z = new C;

Şimdi biraz saçmalığa bakalım ...

Çalışma zamanında sınıf değiştirme:

std::swap(*(void **)x, *(void **)y);
// Now x is a C, and y is a B! Hope they used the same layout of members!

Tüm örnekler için bir yöntemi değiştirme (bir sınıfı maymun ekleyerek)

Bu biraz daha yanıltıcıdır, çünkü vtbl'nin kendisi muhtemelen salt okunur bellekte.

int f3(A*) { return 0; }

mprotect(*(void **)x,8,PROT_READ|PROT_WRITE|PROT_EXEC);
// Or VirtualProtect on win32; this part's very OS-specific
(*(int (***)(A *)x)[0] = f3;
// Now C::f1() returns 0 (remember we made x into a C above)
// so x->f1() and z->f1() both return 0

İkincisi, muhtemelen virüs denetleyicileri yapar ve bağlantı, mprotect manipülasyonları nedeniyle uyanır ve dikkat çeker. NX bit kullanan bir işlemde başarısız olabilir.


6
Hmm. Bunun bir ödül alması uğursuz geliyor. Umarım bu, @Mobilewits'in bu tür maskaralıkların aslında iyi bir fikir olduğunu düşündüğü anlamına gelmez ...
puetzk

1
Lütfen bu tekniğin "göz kırpmak" yerine açıkça ve kuvvetle kullanılmasını engellemeyi düşünün.
einpoklum

" vtbl içerikleri basitçe üye işaretçilerinden oluşan bir dizidir " aslında farklı girişlere sahip, eşit aralıklarla yerleştirilmiş bir kayıt (yapı)
meraklı adam

1
Her iki şekilde de bakabilirsiniz; işlev işaretçilerinin farklı imzaları ve dolayısıyla farklı işaretçi türleri vardır; bu anlamda aslında yapıya benzer. Ancak diğer bağlamlarda, ancak vtbl indeksi fikri kullanışlıdır (örneğin, ActiveX onu typelibs'de ikili arabirimleri tanımladığı şekilde kullanır), bu daha dizi benzeri bir görünümdür.
puetzk

17

Tek bir sanal işleve sahip olmak tüm sınıfı yavaşlatır mı?

Yoksa sadece sanal olan işleve çağrı mı? Ve sanal işlev gerçekten üzerine yazılırsa veya yazılmazsa hız etkilenir mi, yoksa sanal olduğu sürece bunun bir etkisi olmaz mı?

Sanal işlevlere sahip olmak, bir veri öğesinin daha başlatılması, kopyalanması,… böyle bir sınıftaki bir nesneyle uğraşırken, tüm sınıfı yavaşlatır. Yarım düzine kadar üyesi olan bir sınıf için fark önemsiz olmalıdır. Sadece tek bir charüye içeren veya hiç üye içermeyen bir sınıf için fark dikkate değer olabilir.

Bunun dışında, bir sanal işleve yapılan her çağrının sanal bir işlev çağrısı olmadığına dikkat etmek önemlidir. Bilinen türde bir nesneniz varsa, derleyici normal bir işlev çağrısı için kod yayınlayabilir ve hatta böyle hissediyorsa söz konusu işlevi satır içi yapabilir. Yalnızca, temel sınıfın bir nesnesine veya türetilmiş bir sınıfın bir nesnesine işaret edebilecek bir işaretçi veya referans aracılığıyla polimorfik çağrılar yaptığınızda, vtable indireksiyonuna ihtiyaç duyarsınız ve bunun için performans açısından ödeme yaparsınız.

struct Foo { virtual ~Foo(); virtual int a() { return 1; } };
struct Bar: public Foo { int a() { return 2; } };
void f(Foo& arg) {
  Foo x; x.a(); // non-virtual: always calls Foo::a()
  Bar y; y.a(); // non-virtual: always calls Bar::a()
  arg.a();      // virtual: must dispatch via vtable
  Foo z = arg;  // copy constructor Foo::Foo(const Foo&) will convert to Foo
  z.a();        // non-virtual Foo::a, since z is a Foo, even if arg was not
}

Donanımın atması gereken adımlar, işlevin üzerine yazılsa da yazılmasa da esasen aynıdır. Vtable'ın adresi nesneden, uygun yuvadan alınan işlev işaretçisi ve işaretçi tarafından çağrılan işlevden okunur. Gerçek performans açısından, şube tahminlerinin bazı etkileri olabilir. Örneğin, nesnelerinizin çoğu belirli bir sanal işlevin aynı uygulamasına atıfta bulunuyorsa, dal tahmincisinin, işaretçi alınmadan önce hangi işlevi çağıracağını doğru bir şekilde tahmin etme şansı vardır. Ancak hangi işlevin ortak olduğu önemli değildir: üzerine yazılmamış temel duruma delege eden çoğu nesne veya aynı alt sınıfa ait olan ve dolayısıyla aynı üzerine yazılmış duruma delege eden çoğu nesne olabilir.

nasıl derin bir düzeyde uygulanıyor?

Bunu sahte bir uygulama kullanarak göstermek için jheriko fikrini seviyorum. Ancak yukarıdaki koda benzer bir şey uygulamak için C'yi kullanırdım, böylece düşük seviye daha kolay görülebilir.

ebeveyn sınıfı Foo

typedef struct Foo_t Foo;   // forward declaration
struct slotsFoo {           // list all virtual functions of Foo
  const void *parentVtable; // (single) inheritance
  void (*destructor)(Foo*); // virtual destructor Foo::~Foo
  int (*a)(Foo*);           // virtual function Foo::a
};
struct Foo_t {                      // class Foo
  const struct slotsFoo* vtable;    // each instance points to vtable
};
void destructFoo(Foo* self) { }     // Foo::~Foo
int aFoo(Foo* self) { return 1; }   // Foo::a()
const struct slotsFoo vtableFoo = { // only one constant table
  0,                                // no parent class
  destructFoo,
  aFoo
};
void constructFoo(Foo* self) {      // Foo::Foo()
  self->vtable = &vtableFoo;        // object points to class vtable
}
void copyConstructFoo(Foo* self,
                      Foo* other) { // Foo::Foo(const Foo&)
  self->vtable = &vtableFoo;        // don't copy from other!
}

türetilmiş sınıf Bar

typedef struct Bar_t {              // class Bar
  Foo base;                         // inherit all members of Foo
} Bar;
void destructBar(Bar* self) { }     // Bar::~Bar
int aBar(Bar* self) { return 2; }   // Bar::a()
const struct slotsFoo vtableBar = { // one more constant table
  &vtableFoo,                       // can dynamic_cast to Foo
  (void(*)(Foo*)) destructBar,      // must cast type to avoid errors
  (int(*)(Foo*)) aBar
};
void constructBar(Bar* self) {      // Bar::Bar()
  self->base.vtable = &vtableBar;   // point to Bar vtable
}

işlev f sanal işlev çağrısı gerçekleştirme

void f(Foo* arg) {                  // same functionality as above
  Foo x; constructFoo(&x); aFoo(&x);
  Bar y; constructBar(&y); aBar(&y);
  arg->vtable->a(arg);              // virtual function call
  Foo z; copyConstructFoo(&z, arg);
  aFoo(&z);
  destructFoo(&z);
  destructBar(&y);
  destructFoo(&x);
}

Gördüğünüz gibi, bir vtable hafızadaki statik bir bloktur ve çoğunlukla işlev işaretçileri içerir. Bir polimorfik sınıfın her nesnesi, dinamik türüne karşılık gelen vtable'a işaret edecektir. Bu aynı zamanda RTTI ile sanal işlevler arasındaki bağlantıyı daha net hale getirir: Bir sınıfın hangi tür olduğunu basitçe hangi vtable'a işaret ettiğine bakarak kontrol edebilirsiniz. Yukarıdakiler, örneğin çoklu kalıtım gibi birçok yönden basitleştirilmiştir, ancak genel kavram sağlamdır.

Eğer argtürdeyse Foo*ve alırsanız arg->vtable, ancak aslında bir tür nesnesiyse Bar, o zaman yine de vtable. Bunun nedeni, vtableçağrılmış vtableveya base.vtabledoğru yazılmış bir ifadede her zaman nesnenin adresindeki ilk öğedir .


"Bir polimorfik sınıfın her nesnesi kendi vtable'ına işaret edecektir." Her nesnenin kendine ait bir tablosu olduğunu mu söylüyorsunuz? AFAIK vtable, aynı sınıftaki tüm nesneler arasında paylaşılır. Yanılıyorsam haberim olsun.
Bhuwan

1
@Bhuwan: Hayır, haklısınız: tür başına yalnızca bir vtable var (bu, şablonlar durumunda şablon somutlaştırması başına olabilir). Bir polimorfik sınıfın her nesnesinin kendisine uygulanan vtable'a işaret ettiğini, yani her nesnenin böyle bir işaretçiye sahip olduğunu, ancak aynı türdeki nesneler için aynı tabloyu göstereceğini söylemek istedim. Muhtemelen bunu yeniden ifade etmeliyim.
MvG

1
@MvG " aynı türdeki nesneler aynı tabloyu gösterecektir ", sanal temel sınıflarla temel sınıfların oluşturulması sırasında değil! (çok özel bir durum)
wonderguy

1
@curiousguy: "Yukarıdakilerin birçok yönden basitleştirildiğini" dosyalardım, özellikle de sanal tabanların ana uygulaması çoklu kalıtım olduğundan ben de modellemedim. Ancak yorum için teşekkürler, daha fazla derinliğe ihtiyaç duyan insanlar için bunu burada bulundurmak faydalı.
MvG


2

Bu yanıt, Topluluk Wiki yanıtına dahil edilmiştir.

  • Soyut sınıflar, en az bir girdinin işlev işaretçisi için basitçe bir NULL'a sahip mi?

Bunun yanıtı, belirtilmemiş olmasıdır - saf sanal işlevi çağırmak, tanımlanmamışsa (genellikle değildir) tanımsız davranışla sonuçlanır (ISO / IEC 14882: 2003 10.4-2). Bazı uygulamalar vtable girişine basitçe bir NULL gösterici yerleştirir; diğer uygulamalar, bir iddiaya benzer bir şey yapan kukla bir yönteme bir işaretçi yerleştirir.

Soyut bir sınıfın, saf bir sanal işlev için bir uygulama tanımlayabileceğini, ancak bu işlevin yalnızca nitelikli kimlik sözdizimi ile çağrılabileceğini unutmayın (yani, bir temel sınıf yönteminden bir temel sınıf yöntemini çağırmaya benzer şekilde, yöntem adında sınıfı tam olarak belirtmek) Türetilmiş sınıf). Bu, kullanımı kolay bir varsayılan uygulama sağlamak için yapılırken, yine de türetilmiş bir sınıfın bir geçersiz kılma sağlamasını gerektirir.


Ayrıca, soyut bir sınıfın saf bir sanal işlev için bir uygulama tanımlayabileceğini düşünmüyorum. Tanım olarak, saf bir sanal işlevin gövdesi yoktur (örn. Bool my_func () = 0;). Bununla birlikte, normal sanal işlevler için uygulamalar sağlayabilirsiniz.
Zach Burlingame

Saf bir sanal işlevin bir tanımı olabilir. Scott Meyers'in "Etkili C ++, 3. Baskı" Öğe # 34, ISO 14882-2003 10.4-2 veya bytes.com/forum/thread572745.html
Michael Burr

2

C ++ 'da sanal işlevlerin işlevselliğini bir sınıfın üyeleri olarak işlev işaretçileri ve uygulamalar olarak statik işlevler kullanarak veya uygulamalar için üye işlevlere ve üye işlevlerine işaretçi kullanarak yeniden oluşturabilirsiniz. İki yöntem arasında yalnızca temsili avantajlar vardır ... aslında sanal işlev çağrıları kendileri için sadece bir gösterimsel kolaylıktır. Aslında kalıtım sadece bir gösterimsel kolaylıktır ... hepsi kalıtım için dil özellikleri kullanılmadan uygulanabilir. :)

Aşağıdakiler, test edilmemiş, muhtemelen hatalı koddur, ancak umarım fikri gösterir.

Örneğin

class Foo
{
protected:
 void(*)(Foo*) MyFunc;
public:
 Foo() { MyFunc = 0; }
 void ReplciatedVirtualFunctionCall()
 {
  MyFunc(*this);
 }
...
};

class Bar : public Foo
{
private:
 static void impl1(Foo* f)
 {
  ...
 }
public:
 Bar() { MyFunc = impl1; }
...
};

class Baz : public Foo
{
private:
 static void impl2(Foo* f)
 {
  ...
 }
public:
 Baz() { MyFunc = impl2; }
...
};

void(*)(Foo*) MyFunc;bu biraz Java sözdizimi mi?
wonderguy

hayır, işlev işaretçileri için C / C ++ sözdizimi. Kendimden alıntı yapacak olursak, "C ++ 'da sanal işlevlerin işlevselliğini işlev işaretçileri kullanarak yeniden oluşturabilirsiniz". kötü bir sözdizimi ama kendinizi bir C programcısı olarak görüyorsanız aşina olmanız gereken bir şey.
jheriko

ac işlev göstericisi daha çok şuna benzer: int ( PROC) (); ve bir sınıf üyesi işlevine bir işaretçi şöyle görünür: int (ClassName :: MPROC) ();
Menace

1
@menace, orada bazı sözdizimini unuttunuz ... typedef'i düşünüyorsunuz belki? typedef int (* PROC) (); yani PROC foo'yu int (* foo) () yerine daha sonra yapabilirsiniz?
jheriko

2

Basitleştirmeye çalışacağım :)

C ++ 'da hangi sanal işlevlerin olduğunu hepimiz biliyoruz, ancak bunlar derin bir düzeyde nasıl uygulanır?

Bu, belirli bir sanal işlevin gerçeklemeleri olan işlevlere işaret eden bir dizidir. Bu dizideki bir dizin, bir sınıf için tanımlanan sanal bir işlevin belirli dizinini temsil eder. Bu, saf sanal işlevleri içerir.

Bir polimorfik sınıf, başka bir polimorfik sınıftan türediğinde, aşağıdaki durumlara sahip olabiliriz:

  • Türetme sınıfı yeni sanal işlevler eklemez veya herhangi birini geçersiz kılmaz. Bu durumda bu sınıf, vtable'ı temel sınıfla paylaşır.
  • Deriving sınıfı, sanal yöntemleri ekler ve geçersiz kılar. Bu durumda, eklenen sanal işlevlerin son türetilmiş olanı geçen dizine sahip olduğu kendi vtable'ını alır.
  • Kalıtımda çoklu polimorfik sınıflar. Bu durumda, ikinci ve sonraki tabanlar ve bunun türetilmiş sınıftaki indeksi arasında bir dizin kayması var.

Vtable değiştirilebilir veya çalışma zamanında doğrudan erişilebilir mi?

Standart bir yol değil - bunlara erişmek için API yok. Derleyicilerin bunlara erişmek için bazı uzantıları veya özel API'leri olabilir, ancak bu yalnızca bir uzantı olabilir.

Vtable tüm sınıflar için mi yoksa yalnızca en az bir sanal işlevi olanlarda mı var?

Yalnızca en az bir sanal işleve sahip olanlar (yıkıcı bile olabilir) veya vtable'ına sahip en az bir sınıf türetenler ("polimorfiktir").

Soyut sınıflar, en az bir girdinin işlev işaretçisi için basitçe bir NULL'a sahip mi?

Bu olası bir uygulama, ancak uygulanmadı. Bunun yerine, genellikle "saf sanal işlev adı verilen" gibi bir şey yazdıran ve yapan bir işlev vardır abort(). Yapıcı veya yıkıcıda soyut yöntemi çağırmaya çalışırsanız, buna çağrı gerçekleşebilir.

Tek bir sanal işleve sahip olmak tüm sınıfı yavaşlatır mı? Yoksa sadece sanal olan işleve çağrı mı? Ve sanal işlev gerçekten üzerine yazılırsa veya yazılmazsa hız etkilenir mi, yoksa sanal olduğu sürece bunun bir etkisi olmaz mı?

Yavaşlama, yalnızca aramanın doğrudan arama olarak mı yoksa sanal arama olarak mı çözümlendiğine bağlıdır. Ve başka hiçbir şeyin önemi yok. :)

Bir işaretçi veya bir nesneye başvuru yoluyla sanal bir işlevi çağırırsanız, bu her zaman sanal çağrı olarak uygulanır - çünkü derleyici, çalışma zamanında bu işaretçiye ne tür bir nesnenin atanacağını ve bunun bir nesne olup olmadığını asla bilemez. Bu yöntemin geçersiz kılındığı veya olmadığı sınıf. Yalnızca iki durumda derleyici, sanal bir işleve yönelik çağrıyı doğrudan çağrı olarak çözebilir:

  • Yöntemi bir değer aracılığıyla çağırırsanız (bir değer döndüren bir değişken veya bir işlevin sonucu) - bu durumda derleyicinin nesnenin gerçek sınıfının ne olduğu konusunda hiçbir şüphesi yoktur ve derleme zamanında onu "zor çözebilir" .
  • Sanal yöntem, finalonu çağırdığınız bir işaretçiniz veya başvurunuzun olduğu sınıfta bildirilirse ( yalnızca C ++ 11'de ). Bu durumda derleyici, bu yöntemin daha fazla geçersiz kılmaya tabi tutulamayacağını ve yalnızca bu sınıfın yöntemi olabileceğini bilir.

Sanal çağrıların yalnızca iki işaretleyiciyi referans alma ek yüküne sahip olduğunu unutmayın. RTTI kullanmak (yalnızca polimorfik sınıflar için mevcut olsa da), sanal yöntemleri çağırmaktan daha yavaştır, aynı şeyi iki şekilde uygulamak için bir vaka bulursanız. Örneğin, tanımlama virtual bool HasHoof() { return false; }ve sonra geçersiz kılma, yalnızca bool Horse::HasHoof() { return true; }arama yeteneğini if (anim->HasHoof())sağlayacak şekilde, denemekten daha hızlı olacaktır if(dynamic_cast<Horse*>(anim)). Bunun nedeni dynamic_cast, gerçek işaretçi türünden ve istenen sınıf türünden yolun oluşturulup oluşturulamayacağını görmek için bazı durumlarda sınıf hiyerarşisini özyinelemeli olarak geçmek zorunda olmasıdır. Sanal arama her zaman aynı olsa da - iki işaretleyiciden yararlanma.


2

İşte modern C ++ 'da sanal tablonun çalıştırılabilir bir manuel uygulaması. İyi tanımlanmış bir semantiğe sahiptir, kesmeleri yoktur ve hayır void*.

Not: .*ve ->*farklı operatörler vardır *ve ->. Üye işlevi işaretçileri farklı şekilde çalışır.

#include <iostream>
#include <vector>
#include <memory>

struct vtable; // forward declare, we need just name

class animal
{
public:
    const std::string& get_name() const { return name; }

    // these will be abstract
    bool has_tail() const;
    bool has_wings() const;
    void sound() const;

protected: // we do not want animals to be created directly
    animal(const vtable* vtable_ptr, std::string name)
    : vtable_ptr(vtable_ptr), name(std::move(name)) { }

private:
    friend vtable; // just in case for non-public methods

    const vtable* const vtable_ptr;
    std::string name;
};

class cat : public animal
{
public:
    cat(std::string name);

    // functions to bind dynamically
    bool has_tail() const { return true; }
    bool has_wings() const { return false; }
    void sound() const
    {
        std::cout << get_name() << " does meow\n"; 
    }
};

class dog : public animal
{
public:
    dog(std::string name);

    // functions to bind dynamically
    bool has_tail() const { return true; }
    bool has_wings() const { return false; }
    void sound() const
    {
        std::cout << get_name() << " does whoof\n"; 
    }
};

class parrot : public animal
{
public:
    parrot(std::string name);

    // functions to bind dynamically
    bool has_tail() const { return false; }
    bool has_wings() const { return true; }
    void sound() const
    {
        std::cout << get_name() << " does crrra\n"; 
    }
};

// now the magic - pointers to member functions!
struct vtable
{
    bool (animal::* const has_tail)() const;
    bool (animal::* const has_wings)() const;
    void (animal::* const sound)() const;

    // constructor
    vtable (
        bool (animal::* const has_tail)() const,
        bool (animal::* const has_wings)() const,
        void (animal::* const sound)() const
    ) : has_tail(has_tail), has_wings(has_wings), sound(sound) { }
};

// global vtable objects
const vtable vtable_cat(
    static_cast<bool (animal::*)() const>(&cat::has_tail),
    static_cast<bool (animal::*)() const>(&cat::has_wings),
    static_cast<void (animal::*)() const>(&cat::sound));
const vtable vtable_dog(
    static_cast<bool (animal::*)() const>(&dog::has_tail),
    static_cast<bool (animal::*)() const>(&dog::has_wings),
    static_cast<void (animal::*)() const>(&dog::sound));
const vtable vtable_parrot(
    static_cast<bool (animal::*)() const>(&parrot::has_tail),
    static_cast<bool (animal::*)() const>(&parrot::has_wings),
    static_cast<void (animal::*)() const>(&parrot::sound));

// set vtable pointers in constructors
cat::cat(std::string name) : animal(&vtable_cat, std::move(name)) { }
dog::dog(std::string name) : animal(&vtable_dog, std::move(name)) { }
parrot::parrot(std::string name) : animal(&vtable_parrot, std::move(name)) { }

// implement dynamic dispatch
bool animal::has_tail() const
{
    return (this->*(vtable_ptr->has_tail))();
}

bool animal::has_wings() const
{
    return (this->*(vtable_ptr->has_wings))();
}

void animal::sound() const
{
    (this->*(vtable_ptr->sound))();
}

int main()
{
    std::vector<std::unique_ptr<animal>> animals;
    animals.push_back(std::make_unique<cat>("grumpy"));
    animals.push_back(std::make_unique<cat>("nyan"));
    animals.push_back(std::make_unique<dog>("doge"));
    animals.push_back(std::make_unique<parrot>("party"));

    for (const auto& a : animals)
        a->sound();

    // note: destructors are not dispatched virtually
}

1

Her nesnenin bir dizi üye işlevine işaret eden bir vtable işaretçisi vardır.


1

Tüm bu cevaplarda burada bahsedilmeyen bir şey, temel sınıfların hepsinin sanal yöntemlere sahip olduğu çoklu kalıtım durumunda. Miras alan sınıfın bir vmt'ye birden çok işaretçisi vardır. Sonuç, böyle bir nesnenin her bir örneğinin boyutunun daha büyük olmasıdır. Sanal yöntemlere sahip bir sınıfın vmt için fazladan 4 bayta sahip olduğunu herkes bilir, ancak çoklu kalıtım durumunda, sanal yöntemlere sahip olan her temel sınıf için 4 kat, işaretçinin boyutudur.


0

Burly'nin cevapları şu soru haricinde doğrudur:

Soyut sınıflar, en az bir girdinin işlev işaretçisi için basitçe bir NULL'a sahip mi?

Cevap, soyut sınıflar için hiçbir sanal tablonun oluşturulmamasıdır. Bu sınıfların hiçbir nesnesi oluşturulamadığı için buna gerek yoktur!

Başka bir deyişle, eğer sahipsek:

class B { ~B() = 0; }; // Abstract Base class
class D : public B { ~D() {} }; // Concrete Derived class

D* pD = new D();
B* pB = pD;

PB aracılığıyla erişilen vtbl işaretçisi, D sınıfının vtbl'si olacaktır. Bu, çok biçimliliğin tam olarak nasıl uygulandığıdır. Yani, D yöntemlerine pB aracılığıyla erişilir. B sınıfı için vtbl'ye gerek yoktur.

Mike'ın aşağıdaki yorumuna cevaben ...

Açıklamamdaki B sınıfı, D tarafından geçersiz kılınmayan sanal bir foo () yöntemine ve geçersiz kılınmış bir sanal yöntem çubuğuna () sahipse, D'nin vtbl'si B'nin foo () ve kendi çubuğuna () bir göstericiye sahip olacaktır. . B için hala vtbl oluşturulmamıştır.


Bu, 2 nedenden dolayı doğru değildir: 1) soyut bir sınıf, saf sanal yöntemlere ek olarak normal sanal yöntemlere sahip olabilir ve 2) saf sanal yöntemlerin isteğe bağlı olarak tam nitelikli bir adla çağrılabilen bir tanımı olabilir.
Michael Burr

Doğru, ikinci olarak düşündüm de, eğer tüm sanal yöntemler saf sanal ise, derleyicinin vtable'ı optimize edebileceğini (hiçbir tanım olmadığından emin olmak için bağlayıcıdan yardıma ihtiyacı olacaktır).
Michael Burr

1
" Cevap, soyut sınıflar için hiçbir sanal tablo oluşturulmamış olmasıdır. " Yanlış. " Bu sınıfların hiçbir nesnesi oluşturulamadığı için gerek yoktur! " Yanlış.
wonderguy

Hiçbir vtable'a ihtiyaç duyulmaması B konusundaki gerekçenizi takip edebilirim . Bazı yöntemlerinin (varsayılan) uygulamaları olması, bunların bir vtable'da depolanmaları gerektiği anlamına gelmez. Ama kodunuzu (derlemek için bazı düzeltmeleri modulo) gcc -Stakip ederek çalıştırdım c++filtve açıkça Boraya dahil edilecek bir vtable var. Sanırım bunun nedeni vtable'ın sınıf adları ve kalıtım gibi RTTI verilerini de depolaması olabilir. Bir için gerekli olabilir dynamic_cast<B*>. Hatta -fno-rttivtable'ın kaybolmasına neden olmaz. Onun clang -O3yerine gccaniden gitti.
MvG

@MvG " Bazı yöntemlerinin (varsayılan) uygulamaları olması, bunların bir vtable'da depolanması gerektiği anlamına gelmez " Evet, sadece bu demek.
wonderguy

0

biraz önce yaptığım çok sevimli kavram kanıtı (kalıtım sırasının önemli olup olmadığını görmek için); C ++ uygulamanız gerçekten reddederse bana bildirin (benim gcc sürümüm sadece anonim yapılar atamak için bir uyarı veriyor, ancak bu bir hata), merak ediyorum.

CCPolite.h :

#ifndef CCPOLITE_H
#define CCPOLITE_H

/* the vtable or interface */
typedef struct {
    void (*Greet)(void *);
    void (*Thank)(void *);
} ICCPolite;

/**
 * the actual "object" literal as C++ sees it; public variables be here too 
 * all CPolite objects use(are instances of) this struct's structure.
 */
typedef struct {
    ICCPolite *vtbl;
} CPolite;

#endif /* CCPOLITE_H */

CCPolite_constructor.h :

/** 
 * unconventionally include me after defining OBJECT_NAME to automate
 * static(allocation-less) construction.
 *
 * note: I assume CPOLITE_H is included; since if I use anonymous structs
 *     for each object, they become incompatible and cause compile time errors
 *     when trying to do stuff like assign, or pass functions.
 *     this is similar to how you can't pass void * to windows functions that
 *         take handles; these handles use anonymous structs to make 
 *         HWND/HANDLE/HINSTANCE/void*/etc not automatically convertible, and
 *         require a cast.
 */
#ifndef OBJECT_NAME
    #error CCPolite> constructor requires object name.
#endif

CPolite OBJECT_NAME = {
    &CCPolite_Vtbl
};

/* ensure no global scope pollution */
#undef OBJECT_NAME

main.c :

#include <stdio.h>
#include "CCPolite.h"

// | A Greeter is capable of greeting; nothing else.
struct IGreeter
{
    virtual void Greet() = 0;
};

// | A Thanker is capable of thanking; nothing else.
struct IThanker
{
    virtual void Thank() = 0;
};

// | A Polite is something that implements both IGreeter and IThanker
// | Note that order of implementation DOES MATTER.
struct IPolite1 : public IGreeter, public IThanker{};
struct IPolite2 : public IThanker, public IGreeter{};

// | implementation if IPolite1; implements IGreeter BEFORE IThanker
struct CPolite1 : public IPolite1
{
    void Greet()
    {
        puts("hello!");
    }

    void Thank()
    {
        puts("thank you!");
    }
};

// | implementation if IPolite1; implements IThanker BEFORE IGreeter
struct CPolite2 : public IPolite2
{
    void Greet()
    {
        puts("hi!");
    }

    void Thank()
    {
        puts("ty!");
    }
};

// | imposter Polite's Greet implementation.
static void CCPolite_Greet(void *)
{
    puts("HI I AM C!!!!");
}

// | imposter Polite's Thank implementation.
static void CCPolite_Thank(void *)
{
    puts("THANK YOU, I AM C!!");
}

// | vtable of the imposter Polite.
ICCPolite CCPolite_Vtbl = {
    CCPolite_Thank,
    CCPolite_Greet    
};

CPolite CCPoliteObj = {
    &CCPolite_Vtbl
};

int main(int argc, char **argv)
{
    puts("\npart 1");
    CPolite1 o1;
    o1.Greet();
    o1.Thank();

    puts("\npart 2");    
    CPolite2 o2;    
    o2.Greet();
    o2.Thank();    

    puts("\npart 3");    
    CPolite1 *not1 = (CPolite1 *)&o2;
    CPolite2 *not2 = (CPolite2 *)&o1;
    not1->Greet();
    not1->Thank();
    not2->Greet();
    not2->Thank();

    puts("\npart 4");        
    CPolite1 *fake = (CPolite1 *)&CCPoliteObj;
    fake->Thank();
    fake->Greet();

    puts("\npart 5");        
    CPolite2 *fake2 = (CPolite2 *)fake;
    fake2->Thank();
    fake2->Greet();

    puts("\npart 6");        
    #define OBJECT_NAME fake3
    #include "CCPolite_constructor.h"
    fake = (CPolite1 *)&fake3;
    fake->Thank();
    fake->Greet();

    puts("\npart 7");        
    #define OBJECT_NAME fake4
    #include "CCPolite_constructor.h"
    fake2 = (CPolite2 *)&fake4;
    fake2->Thank();
    fake2->Greet();    

    return 0;
}

çıktı:

part 1
hello!
thank you!

part 2
hi!
ty!

part 3
ty!
hi!
thank you!
hello!

part 4
HI I AM C!!!!
THANK YOU, I AM C!!

part 5
THANK YOU, I AM C!!
HI I AM C!!!!

part 6
HI I AM C!!!!
THANK YOU, I AM C!!

part 7
THANK YOU, I AM C!!
HI I AM C!!!!

sahte nesnemi asla tahsis etmediğim için, herhangi bir imha etmeye gerek olmadığını unutmayın; yıkıcılar, nesnenin kendisinin ve vtable işaretçisinin belleğini geri kazanmak için dinamik olarak ayrılmış nesnelerin kapsamının sonuna otomatik olarak yerleştirilir.

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.