Bagaimana fungsi virtual dan vtable diimplementasikan?

109

Kita semua tahu apa fungsi virtual di C ++, tetapi bagaimana mereka diterapkan pada level yang dalam?

Bisakah vtable diubah atau bahkan diakses langsung saat runtime?

Apakah vtable ada untuk semua kelas, atau hanya yang memiliki setidaknya satu fungsi virtual?

Apakah kelas abstrak hanya memiliki NULL untuk penunjuk fungsi setidaknya satu entri?

Apakah memiliki satu fungsi virtual memperlambat seluruh kelas? Atau hanya panggilan ke fungsi yang bersifat virtual? Dan apakah kecepatan akan terpengaruh jika fungsi virtual benar-benar ditimpa atau tidak, atau apakah ini tidak berpengaruh selama itu virtual.

Brian R. Bondy
sumber
2
Sarankan untuk membaca karya Inside the C++ Object Modeldengan Stanley B. Lippman. (Bagian 4.2, halaman 124-131)
smwikipedia

Jawaban:

123

Bagaimana fungsi virtual diterapkan pada level yang dalam?

Dari "Fungsi Virtual di C ++" :

Kapanpun program memiliki fungsi virtual yang dideklarasikan, av - table dibuat untuk kelas tersebut. V-table terdiri dari alamat ke fungsi virtual untuk kelas yang berisi satu atau lebih fungsi virtual. Objek kelas yang berisi fungsi virtual berisi penunjuk virtual yang menunjuk ke alamat dasar tabel virtual dalam memori. Setiap kali ada panggilan fungsi virtual, v-table digunakan untuk menyelesaikan ke alamat fungsi. Objek kelas yang berisi satu atau lebih fungsi virtual berisi penunjuk virtual yang disebut vptr di bagian paling awal objek dalam memori. Karenanya ukuran objek dalam hal ini meningkat dengan ukuran penunjuk. Vptr ini berisi alamat dasar dari tabel virtual di memori. Perhatikan bahwa tabel virtual adalah kelas khusus, yaitu, hanya ada satu tabel virtual untuk kelas terlepas dari jumlah fungsi virtual yang dikandungnya. Tabel virtual ini pada gilirannya berisi alamat dasar dari satu atau lebih fungsi virtual kelas. Pada saat fungsi virtual dipanggil pada sebuah objek, vptr dari objek tersebut memberikan alamat dasar dari tabel virtual untuk kelas tersebut dalam memori. Tabel ini digunakan untuk menyelesaikan panggilan fungsi karena berisi alamat semua fungsi virtual kelas itu. Beginilah cara pengikatan dinamis diselesaikan selama panggilan fungsi virtual. vptr dari objek tersebut memberikan alamat dasar dari tabel virtual untuk kelas tersebut dalam memori. Tabel ini digunakan untuk menyelesaikan panggilan fungsi karena berisi alamat semua fungsi virtual kelas itu. Beginilah cara pengikatan dinamis diselesaikan selama panggilan fungsi virtual. vptr dari objek tersebut memberikan alamat dasar dari tabel virtual untuk kelas tersebut dalam memori. Tabel ini digunakan untuk menyelesaikan panggilan fungsi karena berisi alamat semua fungsi virtual kelas itu. Beginilah cara pengikatan dinamis diselesaikan selama panggilan fungsi virtual.

Bisakah vtable diubah atau bahkan diakses langsung saat runtime?

Secara universal, saya yakin jawabannya adalah "tidak". Anda bisa melakukan beberapa memory mangling untuk menemukan vtable tetapi Anda masih tidak tahu seperti apa fungsi signature untuk memanggilnya. Apa pun yang ingin Anda capai dengan kemampuan ini (yang didukung bahasa) harus dimungkinkan tanpa akses ke vtable secara langsung atau mengubahnya saat runtime. Perhatikan juga, spesifikasi bahasa C ++ tidak menentukan bahwa vtables diperlukan - namun begitulah cara sebagian besar compiler mengimplementasikan fungsi virtual.

Apakah vtable ada untuk semua objek, atau hanya yang memiliki setidaknya satu fungsi virtual?

Saya percaya jawabannya di sini adalah "itu tergantung pada implementasinya" karena spesifikasi tidak memerlukan vtables di tempat pertama. Namun, dalam praktiknya, saya percaya semua kompiler modern hanya membuat vtable jika sebuah kelas memiliki setidaknya 1 fungsi virtual. Ada overhead ruang yang terkait dengan vtable dan overhead waktu yang terkait dengan pemanggilan fungsi virtual vs fungsi non-virtual.

Apakah kelas abstrak hanya memiliki NULL untuk penunjuk fungsi setidaknya satu entri?

Jawabannya tidak ditentukan oleh spesifikasi bahasa sehingga tergantung pada implementasinya. Memanggil fungsi virtual murni menghasilkan perilaku tidak terdefinisi jika tidak ditentukan (yang biasanya tidak) (ISO / IEC 14882: 2003 10.4-2). Dalam praktiknya, ia mengalokasikan slot di vtable untuk fungsi tersebut tetapi tidak menetapkan alamat untuk itu. Hal ini membuat vtable tidak lengkap yang membutuhkan kelas turunan untuk mengimplementasikan fungsi dan menyelesaikan vtable. Beberapa implementasi hanya menempatkan pointer NULL di entri vtable; implementasi lain menempatkan pointer ke metode dummy yang melakukan sesuatu yang mirip dengan sebuah pernyataan.

Perhatikan bahwa kelas abstrak dapat mendefinisikan implementasi untuk fungsi virtual murni, tetapi fungsi itu hanya dapat dipanggil dengan sintaks id yang memenuhi syarat (yaitu, sepenuhnya menentukan kelas dalam nama metode, mirip dengan memanggil metode kelas dasar dari kelas turunan). Ini dilakukan untuk menyediakan implementasi default yang mudah digunakan, sambil tetap mensyaratkan bahwa kelas turunan menyediakan penggantian.

Apakah memiliki satu fungsi virtual memperlambat seluruh kelas atau hanya panggilan ke fungsi virtual?

Ini semakin mendekati pengetahuan saya, jadi seseorang tolong bantu saya jika saya salah!

Saya percaya bahwa hanya fungsi yang virtual di kelas yang mengalami kinerja waktu yang terkait dengan pemanggilan fungsi virtual vs. fungsi non-virtual. Ruang overhead untuk kelas ada di sana. Perhatikan bahwa jika ada vtabel, hanya ada 1 per kelas , bukan satu per objek .

Apakah kecepatan terpengaruh jika fungsi virtual benar-benar diganti atau tidak, atau apakah ini tidak berpengaruh selama itu virtual?

Saya tidak percaya waktu pelaksanaan fungsi virtual yang diganti berkurang dibandingkan dengan memanggil fungsi virtual dasar. Namun, ada overhead ruang tambahan untuk kelas yang terkait dengan mendefinisikan vtabel lain untuk kelas turunan vs kelas dasar.

Sumber daya tambahan:

http://www.codersource.net/published/view/325/virtual_functions_in.aspx (melalui mesin jalan kembali)
http://en.wikipedia.org/wiki/Virtual_table
http://www.codesourcery.com/public/ cxx-abi / abi.html # vtable

Zach Burlingame
sumber
2
Ini tidak akan sejalan dengan filosofi Stroustrup tentang C ++ bagi kompilator untuk meletakkan penunjuk vtable yang tidak perlu di objek yang tidak membutuhkannya. Aturannya adalah Anda tidak mendapatkan overhead yang tidak ada di C kecuali Anda memintanya, dan tidak sopan bagi kompiler untuk melanggarnya.
Steve Jessop
3
Saya setuju bahwa akan bodoh untuk setiap kompiler yang menganggap dirinya serius untuk menggunakan vtable ketika tidak ada fungsi virtual. Namun, saya merasa penting untuk menunjukkan bahwa, sepengetahuan saya, standar C ++ tidak / memerlukan / itu, jadi berhati-hatilah sebelum bergantung padanya.
Zach Burlingame
8
Bahkan fungsi virtual bisa disebut non-virtual. Ini sebenarnya cukup umum: jika objek ada di tumpukan, dalam cakupan kompilator akan mengetahui tipe persisnya dan mengoptimalkan pencarian vtable. Ini terutama berlaku untuk dtor, yang harus dipanggil dalam lingkup tumpukan yang sama.
MSalters
1
Saya percaya ketika kelas yang memiliki setidaknya satu fungsi virtual, setiap objek memiliki vtable, dan bukan satu untuk seluruh kelas.
Asaf R
3
Implementasi umum: Setiap objek memiliki pointer ke vtable; kelas memiliki meja. Sihir konstruksi hanya terdiri dari memperbarui penunjuk vtable di ctor turunan, setelah ctor dasar selesai.
MSalters
31
  • Bisakah vtable diubah atau bahkan diakses langsung saat runtime?

Tidak portabel, tetapi jika Anda tidak keberatan dengan trik kotor, tentu!

PERINGATAN : Teknik ini tidak disarankan untuk digunakan oleh anak-anak, orang dewasa di bawah usia 969 , atau makhluk berbulu kecil dari Alpha Centauri. Efek samping mungkin termasuk setan yang terbang keluar dari hidung Anda , kemunculan tiba-tiba Yog-Sothoth sebagai pemberi persetujuan yang diperlukan pada semua tinjauan kode berikutnya, atau penambahan retroaktif IHuman::PlayPiano()ke semua contoh yang ada]

Di sebagian besar kompiler yang pernah saya lihat, vtbl * adalah 4 byte pertama dari objek, dan konten vtbl hanyalah sebuah array pointer anggota di sana (umumnya dalam urutan mereka dideklarasikan, dengan kelas dasar yang pertama). Tentu saja ada tata letak lain yang memungkinkan, tetapi itulah yang biasanya saya amati.

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;

Sekarang untuk menarik beberapa kejahatan ...

Mengubah kelas saat runtime:

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

Mengganti metode untuk semua instance (monkeypatching kelas)

Yang ini sedikit lebih rumit, karena vtbl itu sendiri mungkin ada dalam memori hanya-baca.

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

Yang terakhir ini cenderung membuat pemeriksa virus dan tautan terbangun dan memperhatikan, karena manipulasi mprotect. Dalam proses menggunakan bit NX, ini mungkin gagal.

puetzk.dll
sumber
6
Hmm. Rasanya tidak menyenangkan bahwa ini menerima hadiah. Saya harap itu tidak berarti @Mobilewits menganggap kejahatan seperti itu sebenarnya ide yang bagus ...
puetzk
1
Harap pertimbangkan untuk mencegah penggunaan teknik ini, dengan jelas dan kuat, daripada "mengedipkan mata".
einpoklum
" konten vtbl hanyalah sebuah array dari petunjuk anggota " sebenarnya ini adalah sebuah catatan (sebuah struct) dengan entri yang berbeda, yang kebetulan berjarak sama
penasaran
1
Anda dapat melihatnya dengan cara apa pun; penunjuk fungsi memiliki tanda tangan yang berbeda, dan dengan demikian jenis penunjuk yang berbeda; dalam hal ini, memang seperti struktur. Tetapi dalam konteks lain, tetapi gagasan indeks vtbl berguna (misalnya ActiveX menggunakannya dengan cara menggambarkan antarmuka ganda di typelibs), yang merupakan tampilan yang lebih mirip larik.
puetzk
17

Apakah memiliki satu fungsi virtual memperlambat seluruh kelas?

Atau hanya panggilan ke fungsi yang bersifat virtual? Dan apakah kecepatan akan terpengaruh jika fungsi virtual benar-benar ditimpa atau tidak, atau apakah ini tidak berpengaruh selama itu virtual.

Memiliki fungsi virtual memperlambat seluruh kelas sejauh satu item data lagi harus diinisialisasi, disalin,… saat berhadapan dengan objek dari kelas semacam itu. Untuk kelas dengan setengah lusin anggota atau lebih, perbedaannya harus diabaikan. Untuk kelas yang hanya berisi satu charanggota, atau tanpa anggota sama sekali, perbedaannya mungkin terlihat.

Selain itu, penting untuk diperhatikan bahwa tidak setiap panggilan ke fungsi virtual adalah panggilan fungsi virtual. Jika Anda memiliki objek dengan tipe yang diketahui, kompilator dapat memancarkan kode untuk pemanggilan fungsi normal, dan bahkan dapat menyebariskan fungsi tersebut jika diinginkan. Hanya ketika Anda melakukan panggilan polimorfik, melalui pointer atau referensi yang mungkin menunjuk ke objek kelas dasar atau objek dari beberapa kelas turunan, Anda memerlukan tipuan vtable dan membayarnya dalam hal kinerja.

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
}

Langkah-langkah yang harus diambil perangkat keras pada dasarnya sama, tidak peduli apakah fungsinya ditimpa atau tidak. Alamat vtable dibaca dari objek, penunjuk fungsi diambil dari slot yang sesuai, dan fungsi dipanggil oleh penunjuk. Dalam hal kinerja aktual, prediksi cabang mungkin berdampak. Jadi misalnya, jika sebagian besar objek Anda merujuk ke implementasi yang sama dari fungsi virtual tertentu, maka ada beberapa kemungkinan bahwa prediktor cabang akan memprediksi dengan benar fungsi mana yang akan dipanggil bahkan sebelum pointer diambil. Tapi tidak masalah fungsi mana yang umum: bisa jadi sebagian besar objek didelegasikan ke kasus dasar yang tidak ditimpa, atau sebagian besar objek milik subkelas yang sama dan oleh karena itu mendelegasikan ke kasus yang ditimpa sama.

bagaimana penerapannya di tingkat yang dalam?

Saya suka ide jheriko untuk mendemonstrasikan ini menggunakan implementasi tiruan. Tetapi saya akan menggunakan C untuk mengimplementasikan sesuatu yang mirip dengan kode di atas, sehingga level rendah lebih mudah dilihat.

kelas orang tua 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!
}

kelas turunan 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
}

fungsi f melakukan panggilan fungsi virtual

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

Jadi Anda bisa lihat, vtable hanyalah blok statis dalam memori, sebagian besar berisi pointer fungsi. Setiap objek dari kelas polimorfik akan mengarah ke vtabel yang sesuai dengan tipe dinamisnya. Ini juga membuat koneksi antara RTTI dan fungsi virtual lebih jelas: Anda dapat memeriksa tipe kelas apa hanya dengan melihat apa yang ditunjuk oleh vtable. Hal di atas disederhanakan dalam banyak hal, seperti misalnya pewarisan ganda, tetapi konsep umumnya masuk akal.

Jika argbertipe Foo*dan Anda ambil arg->vtable, tetapi sebenarnya merupakan objek bertipe Bar, maka Anda masih mendapatkan alamat yang benar dari vtable. Itu karena vtableselalu merupakan elemen pertama di alamat objek, tidak peduli apakah itu dipanggil vtableatau base.vtabledalam ekspresi yang diketik dengan benar.

MvG
sumber
"Setiap objek dari kelas polimorfik akan mengarah ke vtabelnya sendiri." Apakah Anda mengatakan setiap objek memiliki vtabelnya sendiri? Vtable AFAIK dibagikan antara semua objek dari kelas yang sama. Beri tahu saya jika saya salah.
Bhuwan
1
@Bhuwan: Tidak, Anda benar: hanya ada satu vtable per jenis (yang mungkin per contoh template dalam kasus template). Saya bermaksud mengatakan bahwa setiap objek dari kelas polimorfik dengan titik ke vtable yang berlaku untuk itu, jadi setiap objek memiliki penunjuk seperti itu, tetapi untuk objek dengan tipe yang sama itu akan menunjuk ke tabel yang sama. Mungkin saya harus mengubah ini.
MvG
1
@MvG " objek dari jenis yang sama itu akan menunjuk ke tabel yang sama " tidak selama pembangunan kelas dasar dengan kelas basis virtual! (kasus yang sangat khusus)
penasaran
1
@curiousguy: Saya akan mengajukan bahwa di bawah "hal di atas disederhanakan dalam banyak cara", terutama karena aplikasi utama basis virtual adalah multiple inheritance, yang juga tidak saya modelkan. Tapi terima kasih atas komentarnya, ada gunanya memiliki ini di sini untuk orang-orang yang mungkin membutuhkan lebih banyak kedalaman.
MvG
3

Biasanya dengan VTable, serangkaian pointer ke fungsi.

Lou Franco
sumber
2

Jawaban ini telah dimasukkan ke dalam jawaban Wiki Komunitas

  • Apakah kelas abstrak hanya memiliki NULL untuk penunjuk fungsi setidaknya satu entri?

Jawaban untuk itu adalah tidak ditentukan - memanggil fungsi virtual murni menghasilkan perilaku yang tidak ditentukan jika tidak ditentukan (yang biasanya tidak) (ISO / IEC 14882: 2003 10.4-2). Beberapa implementasi hanya menempatkan pointer NULL di entri vtable; implementasi lain menempatkan pointer ke metode dummy yang melakukan sesuatu yang mirip dengan sebuah pernyataan.

Perhatikan bahwa kelas abstrak dapat mendefinisikan implementasi untuk fungsi virtual murni, tetapi fungsi itu hanya dapat dipanggil dengan sintaks id yang memenuhi syarat (yaitu, sepenuhnya menentukan kelas dalam nama metode, mirip dengan memanggil metode kelas dasar dari kelas turunan). Ini dilakukan untuk menyediakan implementasi default yang mudah digunakan, sambil tetap mensyaratkan bahwa kelas turunan menyediakan penggantian.

Michael Burr
sumber
Juga, saya tidak berpikir bahwa kelas abstrak dapat mendefinisikan implementasi untuk fungsi virtual murni. Menurut definisi, fungsi virtual murni tidak memiliki tubuh (misalnya bool my_func () = 0;). Namun, Anda dapat menyediakan implementasi untuk fungsi virtual biasa.
Zach Burlingame
Fungsi virtual murni dapat memiliki definisi. Lihat "Effective C ++, 3rd Ed" dari Scott Meyers Item # 34, ISO 14882-2003 10.4-2, atau bytes.com/forum/thread572745.html
Michael Burr
2

Anda dapat membuat ulang fungsionalitas fungsi virtual di C ++ menggunakan pointer fungsi sebagai anggota kelas dan fungsi statis sebagai implementasinya, atau menggunakan pointer ke fungsi anggota dan fungsi anggota untuk implementasi. Hanya ada keuntungan notasi antara kedua metode ... pada kenyataannya pemanggilan fungsi virtual hanyalah kenyamanan notasi itu sendiri. Faktanya, pewarisan hanyalah kenyamanan notasional ... itu semua dapat diterapkan tanpa menggunakan fitur bahasa untuk pewarisan. :)

Di bawah ini adalah omong kosong yang belum teruji, mungkin kode buggy, tapi semoga bisa menunjukkan idenya.

misalnya

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; }
...
};
jheriko
sumber
void(*)(Foo*) MyFunc;apakah ini beberapa sintaks Java?
wonderguy
tidak, sintaks C / C ++ untuk pointer fungsi. Mengutip diri saya sendiri, "Anda dapat membuat ulang fungsionalitas fungsi virtual di C ++ menggunakan pointer fungsi". ini sedikit sintaks yang buruk, tetapi sesuatu yang harus dikenal jika Anda menganggap diri Anda seorang programmer C.
jheriko
penunjuk fungsi ac akan terlihat lebih seperti: int ( PROC) (); dan penunjuk ke fungsi anggota kelas akan terlihat seperti: int (ClassName :: MPROC) ();
Menace
1
@menace, Anda lupa beberapa sintaks di sana ... Anda berpikir tentang typedef mungkin? typedef int (* PROC) (); jadi Anda bisa melakukan PROC foo nanti daripada int (* foo) ()?
jheriko
2

Saya akan mencoba membuatnya sederhana :)

Kita semua tahu apa fungsi virtual di C ++, tetapi bagaimana mereka diterapkan pada level yang dalam?

Ini adalah larik dengan pointer ke fungsi, yang merupakan implementasi dari fungsi virtual tertentu. Indeks dalam larik ini mewakili indeks tertentu dari fungsi virtual yang ditentukan untuk sebuah kelas. Ini termasuk fungsi virtual murni.

Ketika kelas polimorfik diturunkan dari kelas polimorfik lain, kita mungkin mengalami situasi berikut:

  • Kelas turunan tidak menambahkan fungsi virtual baru atau menimpa apa pun. Dalam hal ini kelas ini berbagi vtabel dengan kelas dasar.
  • Kelas turunan menambahkan dan mengganti metode virtual. Dalam hal ini ia mendapatkan vtabelnya sendiri, di mana fungsi virtual yang ditambahkan memiliki indeks yang dimulai setelah yang terakhir diturunkan.
  • Beberapa kelas polimorfik dalam pewarisan. Dalam hal ini kita memiliki pergeseran indeks antara basis kedua dan berikutnya dan indeksnya di kelas turunan

Bisakah vtable diubah atau bahkan diakses langsung saat runtime?

Bukan cara standar - tidak ada API untuk mengaksesnya. Penyusun mungkin memiliki beberapa ekstensi atau API pribadi untuk mengaksesnya, tetapi itu mungkin hanya ekstensi.

Apakah vtable ada untuk semua kelas, atau hanya yang memiliki setidaknya satu fungsi virtual?

Hanya mereka yang memiliki setidaknya satu fungsi virtual (bahkan destruktor) atau mendapatkan setidaknya satu kelas yang memiliki vtabelnya ("bersifat polimorfik").

Apakah kelas abstrak hanya memiliki NULL untuk penunjuk fungsi setidaknya satu entri?

Itu adalah implementasi yang mungkin, tetapi tidak dipraktikkan. Sebaliknya biasanya ada fungsi yang mencetak sesuatu seperti "fungsi virtual murni yang disebut" dan melakukannya abort(). Panggilan ke itu mungkin terjadi jika Anda mencoba memanggil metode abstrak di konstruktor atau destruktor.

Apakah memiliki satu fungsi virtual memperlambat seluruh kelas? Atau hanya panggilan ke fungsi yang bersifat virtual? Dan apakah kecepatan akan terpengaruh jika fungsi virtual benar-benar ditimpa atau tidak, atau apakah ini tidak berpengaruh selama itu virtual.

Perlambatan hanya bergantung pada apakah panggilan diselesaikan sebagai panggilan langsung atau panggilan virtual. Dan tidak ada lagi yang penting. :)

Jika Anda memanggil fungsi virtual melalui pointer atau referensi ke suatu objek, maka itu akan selalu diimplementasikan sebagai panggilan virtual - karena compiler tidak akan pernah tahu jenis objek apa yang akan ditugaskan ke pointer ini dalam runtime, dan apakah itu dari sebuah kelas di mana metode ini diganti atau tidak. Hanya dalam dua kasus, compiler dapat menyelesaikan panggilan ke fungsi virtual sebagai panggilan langsung:

  • Jika Anda memanggil metode melalui nilai (variabel atau hasil dari fungsi yang mengembalikan nilai) - dalam hal ini compiler tidak memiliki keraguan tentang kelas sebenarnya dari objek tersebut, dan dapat "menyelesaikannya" pada waktu kompilasi .
  • Jika metode virtual dideklarasikan finaldi kelas tempat Anda memiliki pointer atau referensi yang Anda gunakan untuk memanggilnya ( hanya di C ++ 11 ). Dalam hal ini kompilator mengetahui bahwa metode ini tidak dapat menjalani penggantian lebih lanjut dan hanya dapat menjadi metode dari kelas ini.

Perhatikan bahwa panggilan virtual hanya memiliki overhead dereferencing dua petunjuk. Menggunakan RTTI (meskipun hanya tersedia untuk kelas polimorfik) lebih lambat daripada memanggil metode virtual, jika Anda menemukan kasus untuk mengimplementasikan hal yang sama dengan dua cara. Misalnya, menentukan virtual bool HasHoof() { return false; }dan mengganti hanya sebagaimana bool Horse::HasHoof() { return true; }akan memberi Anda kemampuan untuk menelepon if (anim->HasHoof())yang akan lebih cepat daripada mencoba if(dynamic_cast<Horse*>(anim)). Ini karena dynamic_castharus berjalan melalui hierarki kelas dalam beberapa kasus bahkan secara rekursif untuk melihat apakah ada jalur yang dibangun dari tipe pointer aktual dan tipe kelas yang diinginkan. Sementara panggilan virtual selalu sama - dereferensi dua petunjuk.

Ethouris
sumber
2

Berikut ini adalah runnable implementasi manual tabel virtual di C ++ modern. Ini memiliki semantik yang terdefinisi dengan baik, tidak ada peretasan dan tidak void*.

Catatan: .*dan ->*merupakan operator berbeda dari *dan ->. Pointer fungsi anggota bekerja secara berbeda.

#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
}
Xeverous
sumber
1

Setiap objek memiliki penunjuk vtable yang menunjuk ke array fungsi anggota.


sumber
1

Sesuatu yang tidak disebutkan di sini dalam semua jawaban ini adalah dalam kasus warisan berganda, di mana semua kelas dasar memiliki metode virtual. Kelas mewarisi memiliki beberapa pointer ke vmt. Hasilnya adalah ukuran setiap instance dari objek tersebut lebih besar. Semua orang tahu bahwa kelas dengan metode virtual memiliki 4 byte ekstra untuk vmt, tetapi dalam kasus warisan berganda untuk setiap kelas dasar yang memiliki metode virtual dikalikan 4. 4 adalah ukuran pointer.

Philip Stuyck
sumber
0

Jawaban Burly benar di sini kecuali untuk pertanyaannya:

Apakah kelas abstrak hanya memiliki NULL untuk penunjuk fungsi setidaknya satu entri?

Jawabannya adalah tidak ada tabel virtual yang dibuat sama sekali untuk kelas abstrak. Tidak diperlukan karena tidak ada objek kelas ini yang dapat dibuat!

Dengan kata lain jika kita memiliki:

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

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

Pointer vtbl yang diakses melalui pB akan menjadi vtbl kelas D. Ini persis bagaimana polimorfisme diimplementasikan. Artinya, bagaimana metode D diakses melalui pB. Tidak perlu vtbl untuk kelas B.

Menanggapi komentar Mike di bawah ini ...

Jika kelas B dalam uraian saya memiliki metode virtual foo () yang tidak diganti oleh D dan bilah metode virtual () yang diganti, maka vtbl D akan memiliki penunjuk ke foo () B dan ke barnya sendiri () . Masih belum ada vtbl yang dibuat untuk B.

Andrew Stein
sumber
Ini tidak benar karena 2 alasan: 1) kelas abstrak mungkin memiliki metode virtual biasa selain metode virtual murni, dan 2) metode virtual murni secara opsional dapat memiliki definisi yang dapat dipanggil dengan nama yang memenuhi syarat.
Michael Burr
Benar - setelah dipikir-pikir, saya membayangkan bahwa jika semua metode virtual adalah virtual murni, kompiler mungkin mengoptimalkan vtable (itu akan membutuhkan bantuan membentuk linker untuk memastikan tidak ada definisi juga).
Michael Burr
1
Jawabannya adalah tidak ada tabel virtual yang dibuat sama sekali untuk kelas abstrak. ” Salah. “ Tidak perlu karena tidak ada objek dari kelas ini yang dapat dibuat! ” Salah.
penasaran
Saya dapat mengikuti pemikiran Anda bahwa tidak ada untuk vtable B harus diperlukan. Hanya karena beberapa metodenya memiliki implementasi (default) tidak berarti mereka harus disimpan dalam vtable. Tapi saya baru saja menjalankan kode Anda (modulo beberapa perbaikan untuk membuatnya dikompilasi) melalui gcc -Sdiikuti oleh c++filtdan jelas ada vtable untuk Bdisertakan di sana. Saya rasa itu mungkin karena vtable juga menyimpan data RTTI seperti nama kelas dan warisan. Mungkin diperlukan untuk a dynamic_cast<B*>. Bahkan -fno-rttitidak membuat vtable hilang. Dengan clang -O3bukannya gcctiba-tiba hilang.
MvG
@MvG " Hanya karena beberapa metodenya memiliki implementasi (default) tidak berarti mereka harus disimpan dalam vtable " Ya, itu berarti hanya itu.
wonderguy
0

bukti konsep yang sangat lucu yang saya buat sedikit lebih awal (untuk melihat apakah urutan pewarisan itu penting); beri tahu saya jika implementasi C ++ Anda benar-benar menolaknya (versi gcc saya hanya memberikan peringatan untuk menetapkan struct anonim, tapi itu bug), saya penasaran.

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

keluaran:

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!!!!

perhatikan karena saya tidak pernah mengalokasikan objek palsu saya, tidak perlu melakukan penghancuran apa pun; destructors secara otomatis diletakkan di akhir lingkup objek yang dialokasikan secara dinamis untuk mendapatkan kembali memori literal objek itu sendiri dan penunjuk vtable.

Dmitry
sumber