Чи гальмує наявність однієї віртуальної функції весь клас?
Або лише виклик до функції, яка є віртуальною? І чи впливає на швидкість, якщо віртуальна функція насправді перезаписана чи ні, чи це не робить ефекту, поки вона віртуальна.
Наявність віртуальних функцій уповільнює весь клас настільки, що ще один елемент даних повинен бути ініціалізований, скопійований ... під час роботи з об'єктом такого класу. Для класу з півтора десятками членів різниця повинна бути незначною. Для класу, який містить лише одного 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
в правильно набраному виразі.
Inside the C++ Object Model
авторStanley B. Lippman
. (Розділ 4.2, стор. 124-131)