Як реалізуються віртуальні функції та vtable?


109

Всі ми знаємо, які віртуальні функції є в C ++, але як вони реалізуються на глибокому рівні?

Чи можна змінити Vtable або навіть безпосередньо отримати доступ під час його виконання?

Чи існує vtable для всіх класів або лише для тих, хто має хоча б одну віртуальну функцію?

Чи мають абстрактні класи просто NULL для вказівника функції принаймні одного запису?

Чи гальмує наявність однієї віртуальної функції весь клас? Або лише виклик до функції, яка є віртуальною? І чи впливає на швидкість, якщо віртуальна функція насправді перезаписана чи ні, чи це не робить ефекту, поки вона віртуальна.


2
Запропонуйте прочитати шедевр Inside the C++ Object Modelавтор Stanley B. Lippman. (Розділ 4.2, стор. 124-131)
smwikipedia

Відповіді:


123

Як реалізуються віртуальні функції на глибокому рівні?

З "Віртуальних функцій в C ++" :

Кожного разу, коли програма оголошує віртуальну функцію, для класу будується av - таблиця. V-таблиця складається з адрес віртуальних функцій для класів, що містять одну або більше віртуальних функцій. Об'єкт класу, що містить віртуальну функцію, містить віртуальний покажчик, який вказує на базову адресу віртуальної таблиці в пам'яті. Щоразу, коли відбувається виклик віртуальної функції, v-таблиця використовується для вирішення адреси функції. Об'єкт класу, який містить одну або більше віртуальних функцій, містить віртуальний покажчик, який називається vptr на самому початку об'єкта в пам'яті. Отже, розмір об'єкта в цьому випадку збільшується на розмір вказівника. Цей vptr містить базову адресу віртуальної таблиці в пам'яті. Зауважте, що віртуальні таблиці специфічні для класу, тобто існує лише одна віртуальна таблиця для класу незалежно від кількості віртуальних функцій, які він містить. Ця віртуальна таблиця, в свою чергу, містить базові адреси однієї або декількох віртуальних функцій класу. У той час, коли віртуальна функція викликається на об'єкті, vptr цього об'єкта надає базову адресу віртуальної таблиці цього класу в пам'яті. Ця таблиця використовується для вирішення виклику функції, оскільки вона містить адреси всіх віртуальних функцій цього класу. Ось як вирішується динамічне прив'язування під час виклику віртуальної функції. vptr цього об'єкта надає базову адресу віртуальної таблиці для цього класу в пам'яті. Ця таблиця використовується для вирішення виклику функції, оскільки вона містить адреси всіх віртуальних функцій цього класу. Ось як вирішується динамічне прив'язування під час виклику віртуальної функції. vptr цього об'єкта надає базову адресу віртуальної таблиці для цього класу в пам'яті. Ця таблиця використовується для вирішення виклику функції, оскільки вона містить адреси всіх віртуальних функцій цього класу. Ось як вирішується динамічне прив'язування під час виклику віртуальної функції.

Чи можна змінити Vtable або навіть безпосередньо отримати доступ під час його виконання?

Універсально я вважаю, що відповідь "ні". Ви можете зайнятись керуванням пам'яттю, щоб знайти vtable, але ви все одно не знатимете, як виглядає підпис функції. Все, що ви хотіли б досягти за допомогою цієї здатності (яку підтримує мова), повинно бути можливим без доступу до vtable безпосередньо або зміни його під час виконання. Також зауважте, що специфікація мови C ++ не відповідає вказує, що потрібні vtables, але саме так більшість компіляторів реалізують віртуальні функції.

Чи існує vtable для всіх об'єктів чи лише тих, у яких є хоча б одна віртуальна функція?

Я вірю що відповідь тут - це "залежить від реалізації", оскільки специфікація не вимагає в першу чергу vtables. Однак на практиці я вважаю, що всі сучасні компілятори створюють vtable лише у тому випадку, коли клас має принаймні 1 віртуальну функцію. Існує простір накладних витрат, пов'язаний з vtable, і часові накладні витрати, пов'язані з викликом віртуальної функції проти невіртуальної функції.

Чи мають абстрактні класи просто NULL для вказівника функції принаймні одного запису?

Відповідь це не визначено специфікацією мови, тому це залежить від реалізації. Виклик чистої віртуальної функції призводить до невизначеної поведінки, якщо вона не визначена (що зазвичай не є) (ISO / IEC 14882: 2003 10.4-2). На практиці він виділяє слот у vtable для функції, але не присвоює йому адресу. Це залишає vtable неповним, що вимагає, щоб похідні класи реалізували функцію та завершили vtable. Деякі реалізації просто розміщують вказівник NULL у vtable; в інших реалізаціях розміщується вказівник на фіктивний метод, який робить щось подібне до твердження.

Зауважте, що абстрактний клас може визначати реалізацію для чистої віртуальної функції, але цю функцію можна викликати лише із синтаксисом кваліфікованого ідентифікатора (тобто, повністю вказавши клас у назві методу, аналогічно виклику методу базового класу з похідний клас). Це робиться для забезпечення простої у використанні реалізації за замовчуванням, при цьому все ж вимагається, щоб похідний клас забезпечив переосмислення.

Уповільнює чи наявність однієї віртуальної функції весь клас або лише виклик функції, яка є віртуальною?

Це підходить до краю моїх знань, тож будь-хто, будь ласка, допоможе мені тут, якщо я помиляюся!

Я вважаю, що лише ті функції, які є віртуальними в класі, переживають швидкість виконання часу, пов'язану з викликом віртуальної функції проти невіртуальної функції. Місце для класу є в будь-якому випадку. Зауважте, що якщо є vtable, то є лише 1 на клас , а не один на об'єкт .

Чи впливає на швидкість, якщо віртуальна функція насправді переосмислена чи ні, чи це не робить ефекту, поки вона віртуальна?

Я не вірю, що час виконання відміненої віртуальної функції зменшується порівняно з викликом базової віртуальної функції. Однак є додатковий накладний простір для класу, пов'язаний з визначенням іншого vtable для похідного класу проти базового класу.

Додаткові ресурси:

http://www.codersource.net/published/view/325/virtual_functions_in.aspx (через машину зворотного шляху)
http://en.wikipedia.org/wiki/Virtual_table
http://www.codesourcery.com/public/ cxx-abi / abi.html # vtable


2
Це не було б у відповідності з філософією Stroustrup C ++ для компілятора, щоб помістити непотрібний покажчик vtable в об'єкт, який йому не потрібен. Правило полягає в тому, що ви не отримуєте накладних витрат, яких немає в C, якщо ви не вимагаєте цього, і грубо для компіляторів це зламати.
Стів Джессоп,

3
Я погоджуюся, що було б нерозумно будь-який компілятор, який серйозно ставиться до використання vtable, коли немає віртуальних функцій. Однак я вважав важливим зазначити, що, наскільки мені відомо, стандарт C ++ цього не вимагає / вимагає, тому попередити його потрібно перед тим, як залежно від цього.
Zach Burlingame

8
Навіть віртуальні функції можна назвати не-практично. Це насправді є досить поширеним: якщо об’єкт знаходиться у стеці, у межах області компілятор дізнається точний тип та оптимізує пошук vtable. Особливо це стосується dtor, який повинен бути викликаний в тій же області стека.
MSalters

1
Я вважаю, що коли для класу, який має принаймні одну віртуальну функцію, кожен об'єкт має vtable, а не один для всього класу.
Asaf R

3
Загальна реалізація: кожен об'єкт має вказівник на vtable; клас належить таблиці. Магія побудови просто полягає в оновленні покажчика vtable у похідному ctor, після закінчення базового ctor.
MSalters

31
  • Чи можна змінити Vtable або навіть безпосередньо отримати доступ під час його виконання?

Не портативно, але якщо ви не проти брудних хитрощів, обов'язково!

ПОПЕРЕДЖЕННЯ : Цей прийом не рекомендується використовувати дітям, дорослим у віці до 969 років або маленьким пухнастим істотам з Альфи Кентавра. Побічні ефекти можуть включати демонів, які вилітають з вашого носа , різке поява Yog-Sothoth як необхідного схвалення всіх наступних оглядів коду або зворотне додавання IHuman::PlayPiano()до всіх існуючих примірників]

У більшості компіляторів, яких я бачив, vtbl * - це перші 4 байти об'єкта, а вміст vtbl - це просто масив покажчиків членів (як правило, у порядку, де вони були оголошені, першим базовим класом). Звичайно, є й інші можливі схеми, але я це, як правило, спостерігав.

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;

Тепер, щоб витягнути кілька шенагів ...

Зміна класу під час виконання:

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

Заміна методу для всіх примірників (маніпулювання класом)

Це трохи складніше, оскільки сам vtbl є, мабуть, пам'яттю, що лише для читання.

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

Останнє, швидше за все, змусить перевіряти віруси та посилання прокидатися та приймати повідомлення, завдяки маніпуляціям із захистом. У процесі використання біта NX він цілком може вийти з ладу.


6
Хм. Зрозуміло, що це отримало щедрість. Я сподіваюся, що це не означає @Mobilewits вважає, що подібні шнанігани насправді є гарною ідеєю ...
puetzk

1
Будь ласка, подумайте про відмову від використання цієї техніки, чітко і рішуче, а не «підморгуючи».
einpoklum

" Вміст vtbl - це просто масив покажчиків членів ", насправді це запис (структура) з різними записами, які трапляються рівномірно розташовані
curiousguy

1
Ви можете подивитися на це будь-яким способом; вказівники функцій мають різні підписи, а отже, різні типи вказівників; у цьому сенсі це справді схоже на структуру. Але в інших контекстах, але ідея індексу vtbl є корисною (наприклад, ActiveX використовує її так, як описує подвійні інтерфейси в typelibs), що є більш схожим на масив.
puetzk

17

Чи гальмує наявність однієї віртуальної функції весь клас?

Або лише виклик до функції, яка є віртуальною? І чи впливає на швидкість, якщо віртуальна функція насправді перезаписана чи ні, чи це не робить ефекту, поки вона віртуальна.

Наявність віртуальних функцій уповільнює весь клас настільки, що ще один елемент даних повинен бути ініціалізований, скопійований ... під час роботи з об'єктом такого класу. Для класу з півтора десятками членів різниця повинна бути незначною. Для класу, який містить лише одного charчлена або взагалі немає членів, різниця може бути помітною.

Крім цього, важливо зазначити, що не кожен виклик віртуальної функції - це виклик віртуальної функції. Якщо у вас є об'єкт відомого типу, компілятор може випромінювати код для виклику звичайної функції і навіть може вбудовувати цю функцію, якщо їй здається, що це схоже. Лише коли ви робите поліморфні дзвінки, через вказівник або посилання, які можуть вказувати на об'єкт базового класу або на об’єкт якогось похідного класу, вам потрібно непряме виправлення та платити за нього з точки зору продуктивності.

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
}

Етапи, якими слід здійснити обладнання, по суті однакові, незалежно від того, перезаписана чи ні функція. Адреса vtable зчитується з об'єкта, покажчик функції, отриманий з відповідного слота, і функція, що викликається вказівником. Що стосується фактичної ефективності, прогнозування галузей може мати певний вплив. Так, наприклад, якщо більшість ваших об'єктів посилаються на одну і ту ж реалізацію даної віртуальної функції, то існує певний шанс, що передбачувач гілки вірно передбачить, яку функцію викликати ще до того, як покажчик буде отриманий. Але не має значення, яка функція є загальною: це може бути більшість об'єктів, делегованих до неперезаписаного базового випадку, або більшість об'єктів, що належать до того ж підкласу, і тому делегування в один і той же перезаписаний регістр.

як вони реалізуються на глибокому рівні?

Мені подобається ідея jheriko продемонструвати це за допомогою макетної реалізації. Але я використовую C, щоб реалізувати щось подібне до коду вище, щоб низький рівень легше було помітити.

батьківський клас 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!
}

похідний клас 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
}

функція f виконання віртуального виклику функції

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

Отже, ви бачите, vtable - це лише статичний блок в пам'яті, в основному містить покажчики функцій. Кожен об’єкт поліморфного класу вказуватиме на vtable, що відповідає його динамічному типу. Це також робить зв'язок між RTTI та віртуальними функціями більш чітким: ви можете перевірити, який тип класу є, просто переглянувши, на який vtable він вказує. Сказане багато в чому спрощено, як, наприклад, багаторазове успадкування, але загальна концепція є надійною.

Якщо ви argтипу, Foo*і ви приймаєте arg->vtable, але насправді є об'єктом типу Bar, то ви все одно отримаєте правильну адресу vtable. Це тому, що vtableзавжди є першим елементом за адресою об'єкта, незалежно від того, називається він vtableабо base.vtableв правильно набраному виразі.


"Кожен об'єкт поліморфного класу вказуватиме на свій власний vtable." Ви кажете, що кожен об’єкт має свою власну версію? AFAIK vtable поділяється між усіма об'єктами одного класу. Повідомте мене, якщо я помиляюся.
Бхуван

1
@Bhuwan: Ні, ти маєш рацію: є лише одна vtable для кожного типу (що може бути для копіювання шаблонів у випадку шаблонів). Я мав на увазі сказати, що кожен об’єкт поліморфного класу з вказівкою на vtable, який застосовується до нього, тому кожен об’єкт має такий покажчик, але для об’єктів одного типу він вказуватиме на ту саму таблицю. Напевно, я повинен це переформулювати.
MvG

1
@MvG " об'єкти одного типу будуть вказувати на ту саму таблицю " не під час побудови базових класів з віртуальними базовими класами! (дуже особливий випадок)
цікавогут

1
@curiousguy: Я б зазначив, що під «вищезазначеним багато в чому спрощено», тим більше, що основне застосування віртуальних баз - це багаторазове успадкування, яке я також не моделював. Але дякую за коментар, це корисно мати тут людям, яким може знадобитися більше глибини.
MvG


2

Ця відповідь включена до відповіді Wiki Wiki

  • Чи мають абстрактні класи просто NULL для вказівника функції принаймні одного запису?

Відповідь на це полягає в тому, що вона не визначена - виклик чистої віртуальної функції призводить до невизначеної поведінки, якщо вона не визначена (що зазвичай не є) (ISO / IEC 14882: 2003 10.4-2). Деякі реалізації просто розміщують вказівник NULL у vtable; інші реалізації розміщують вказівник на фіктивний метод, який робить щось подібне до твердження.

Зауважте, що абстрактний клас може визначати реалізацію для чистої віртуальної функції, але цю функцію можна викликати лише із синтаксисом кваліфікованого ідентифікатора (тобто, повністю вказавши клас у назві методу, аналогічно виклику методу базового класу з похідний клас). Це робиться для забезпечення простої у використанні реалізації за замовчуванням, при цьому все ж вимагається, щоб похідний клас забезпечив переосмислення.


Крім того, я не думаю, що абстрактний клас може визначити реалізацію для чистої віртуальної функції. За визначенням, у чистої віртуальної функції немає тіла (наприклад, bool my_func () = 0;). Однак ви можете надати реалізації для звичайних віртуальних функцій.
Зак Берлінгам,

Чиста віртуальна функція може мати визначення. Див. "Ефективний C ++, 3-е видання" Скотта Майєрса, пункт 34, ISO 14882-2003 10.4-2, або bytes.com/forum/thread572745.html
Майкл Берр,

2

Ви можете відтворити функціональність віртуальних функцій на C ++, використовуючи функціональні покажчики як члени класу та статичні функції як реалізації, або використовуючи вказівники на членські функції та функції члена для реалізації. Між двома методами існують лише помітні переваги ... насправді виклики віртуальних функцій є лише самим нотаційним зручністю. Насправді успадкування - це лише нотаційна зручність ... все це можна реалізувати без використання мовних особливостей для успадкування. :)

Нижче наведено лайно неперевірений, напевно, баггі-код, але, сподіваємось, це демонструє ідея.

напр

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;це якийсь синтаксис Java?
curiousguy

ні, його синтаксис C / C ++ для функціональних покажчиків. Щоб процитувати себе "Ви можете відтворити функціональність віртуальних функцій на C ++ за допомогою покажчиків функцій". це неприємний синтаксис, але з чим слід ознайомитися, якщо ви вважаєте себе програмістом C.
jheriko

покажчик функції змінного струму виглядатиме більше як: int ( PROC) (); і вказівник на функцію члена класу виглядатиме так: int (ClassName :: MPROC) ();
Монас

1
@menace, ти там забув якийсь синтаксис ... ти, можливо, думаєш про typedef? typedef int (* PROC) (); тож ви можете просто зробити PROC foo пізніше замість int (* foo) ()?
jheriko

2

Я спробую зробити це просто :)

Всі ми знаємо, які віртуальні функції є в C ++, але як вони реалізуються на глибокому рівні?

Це масив із вказівниками на функції, які є реалізацією певної віртуальної функції. Індекс у цьому масиві представляє конкретний індекс віртуальної функції, визначений для класу. Сюди входять чисті віртуальні функції.

Коли поліморфний клас походить з іншого поліморфного класу, у нас можуть виникнути такі ситуації:

  • Клас виведення не додає нових віртуальних функцій, ані переосмислює жодних. У цьому випадку цей клас поділяє vtable з базовим класом.
  • Клас похідних додає і переосмислює віртуальні методи. У цьому випадку він отримує свій власний vtable, де додані віртуальні функції мають індекс, починаючи з останнього похідного.
  • Кілька поліморфних класів у спадщині. У цьому випадку ми маємо зсув індексу між другою та наступною базами та її індексом у похідному класі

Чи можна змінити Vtable або навіть безпосередньо отримати доступ під час його виконання?

Не стандартний спосіб - немає API для доступу до них. Компілятори можуть мати деякі розширення або приватні API для доступу до них, але це може бути лише розширення.

Чи існує vtable для всіх класів або лише для тих, хто має хоча б одну віртуальну функцію?

Тільки ті, хто має хоча б одну віртуальну функцію (будь то деструктор) або походить хоча б один клас, який має свій vtable ("є поліморфним").

Чи мають абстрактні класи просто NULL для вказівника функції принаймні одного запису?

Це можлива реалізація, але скоріше не практикується. Натомість зазвичай існує функція, яка друкує щось на зразок "чистої віртуальної функції, що називається", і робить abort(). Заклик до цього може статися, якщо ви спробуєте викликати абстрактний метод у конструкторі чи деструкторі.

Чи гальмує наявність однієї віртуальної функції весь клас? Або лише виклик до функції, яка є віртуальною? І чи впливає на швидкість, якщо віртуальна функція насправді перезаписана чи ні, чи це не робить ефекту, поки вона віртуальна.

Уповільнення залежить лише від того, чи буде виклик вирішено як прямий дзвінок або як віртуальний виклик. І ніщо не має значення. :)

Якщо ви викликаєте віртуальну функцію через вказівник або посилання на об'єкт, вона завжди буде реалізована як віртуальна виклик - тому що компілятор ніколи не може знати, який тип об’єкта буде призначений цьому покажчику під час виконання та чи є він клас, у якому цей метод переосмислений чи ні. Лише у двох випадках компілятор може вирішити виклик віртуальної функції як прямий виклик:

  • Якщо ви викликаєте метод через значення (змінну чи результат функції, яка повертає значення) - у цьому випадку у компілятора немає сумнівів, що таке фактичний клас об'єкта, і може "важко вирішити" його під час компіляції .
  • Якщо віртуальний метод оголошений finalу класі, до якого у вас є вказівник або посилання, через який ви його викликаєте ( тільки в C ++ 11 ). У цьому випадку компілятор знає, що цей метод не може зазнати подальшого переосмислення і він може бути лише методом з цього класу.

Зауважте, що віртуальні дзвінки мають лише накладні витрати на перенаправлення двох покажчиків. Використання RTTI (хоча доступне лише для поліморфних класів) повільніше, ніж виклик віртуальних методів, якщо ви знайдете випадок реалізувати одне й те саме двома такими способами. Наприклад, визначити, virtual bool HasHoof() { return false; }а потім змінити лише те, як bool Horse::HasHoof() { return true; }забезпечить вам можливість дзвонити, if (anim->HasHoof())що буде швидше, ніж намагатися if(dynamic_cast<Horse*>(anim)). Це тому, що dynamic_castдоводиться проходити ієрархію класів, в деяких випадках навіть рекурсивно, щоб побачити, чи може бути побудований шлях від фактичного типу вказівника та потрібного типу класу. Хоча віртуальний дзвінок завжди однаковий - перенаправлення двох покажчиків.


2

Ось запущена вручну реалізація віртуальної таблиці в сучасному C ++. Він має чітко виражену семантику, ніяких хак і ні void*.

Примітка: .*і ->*є різними операторами, ніж *та ->. Покажчики функцій членів працюють по-різному.

#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

Кожен об’єкт має vtable pointer, який вказує на масив функцій-членів.


1

Щось, що тут не згадується у всіх цих відповідях, - це те, що у випадку багаторазового успадкування, де всі базові класи мають віртуальні методи. Клас успадкування має кілька покажчиків на vmt. У результаті виходить, що розмір кожного екземпляра такого об’єкта більший. Всім відомо, що клас з віртуальними методами має додаткові 4 байти для vmt, але у випадку багаторазового успадкування саме для кожного базового класу віртуальних методів розмір 4 рази має розмір вказівника.


0

Відповіді Берлі тут правильні, окрім питання:

Чи мають абстрактні класи просто NULL для вказівника функції принаймні одного запису?

Відповідь полягає в тому, що для абстрактних класів взагалі не створюється віртуальна таблиця. Немає потреби, оскільки ніяких об’єктів цих класів не можна створити!

Іншими словами, якщо у нас є:

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

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

Вказівник vtbl, доступ до якого здійснюється через pB, буде vtbl класу D. Саме так реалізується поліморфізм. Тобто, як D-методи отримують доступ через pB. Немає потреби в vtbl для класу B.

У відповідь на коментар Майка нижче ...

Якщо клас B в моєму описі має віртуальний метод foo (), який не перекривається D, і віртуальну панель методів (), яку перекривають, то vtbl D матиме вказівник на foo () та його власний бар () . Досі не створено vtbl для Б.


Це неправильно з 2 причин: 1) абстрактний клас може мати звичайні віртуальні методи, крім чистих віртуальних методів, і 2) чисті віртуальні методи необов'язково можуть мати визначення, яке можна викликати повністю кваліфікованим ім'ям.
Майкл Берр

Правильно - по-друге, я думаю, що якби всі віртуальні методи були чистими віртуальними, компілятор може оптимізувати vtable далеко (знадобиться допомога у формуванні лінкера, щоб переконатися, що також не було визначень).
Майкл Берр

1
" Відповідь полягає в тому, що для абстрактних класів взагалі не створюється віртуальна таблиця. " Неправильно. " Немає потреби, оскільки жодні об'єкти цих класів не можуть бути створені! "
curiousguy

Я можу дотримуватися вашої обґрунтування, для якої не існує жодної версії B не потребує. Тільки тому, що деякі його методи мають (за замовчуванням) реалізацію, не означає, що вони повинні зберігатися в vtable. Але я просто запустив ваш код (по модулю кілька виправлень, щоб зробити його компіляцією), за gcc -Sяким слід, c++filtі явно є vtable для Bвключених туди. Я думаю, це може бути тому, що vtable також зберігає RTTI дані, такі як назви класів та успадкування. Це може знадобитися для dynamic_cast<B*>. Навіть -fno-rttiне приносить віталій. З clang -O3замість gccцього раптом пішло.
MvG

@MvG " Тільки тому, що деякі з його методів мають (за замовчуванням) реалізацію, не означає, що вони повинні зберігатися в vtable " Так, це означає саме це.
допитливий хлопець

0

дуже милий доказ концепції я зробив трохи раніше (щоб побачити, чи має значення порядок успадкування); дайте мені знати, якщо ваша реалізація C ++ насправді її відкидає (моя версія gcc лише попереджає про призначення анонімних структур, але це помилка), мені цікаво.

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

вихід:

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

зауважте, оскільки я ніколи не виділяю свій підроблений предмет, не потрібно робити ніяких руйнувань; деструктори автоматично ставляться в кінці області динамічно виділених об'єктів для відновлення пам'яті самого буквеного об'єкта та покажчика vtable.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.