Урок №175. Віртуальні таблиці

  Юрій  | 

  Оновл. 23 Бер 2021  | 

 27

Для реалізації віртуальних функцій мова C++ використовує спеціальну форму пізнього зв’язування — віртуальні таблиці.

Віртуальні таблиці

Віртуальна таблиця в мові С++ — це таблиця пошуку функцій для виконання викликів функцій в режимі пізнього (динамічного) зв’язування. Віртуальну таблицю ще називають «vtable» або «таблицею віртуальних функцій/методів».

Віртуальна таблиця насправді досить-таки проста, хоча її складно описати словами.

По-перше, будь-який клас, який використовує віртуальні функції (або дочірній клас, батьківський клас якого використовує віртуальні функції), має свою власну віртуальну таблицю. Це звичайний статичний масив, який створюється компілятором під час компіляції. Віртуальна таблиця містить по одному запису на кожну віртуальну функцію, яка може бути викликана об’єктами класу. Кожен запис в цій таблиці — це вказівник на функцію, який вказує на “найдочірніший” метод, доступний об’єкту цього класу.

По-друге, компілятор також додає прихований вказівник на батьківський клас, який ми називатимемо *__vptr. Цей вказівник автоматично створюється при створенні об’єкта класу і вказує на віртуальну таблицю цього класу. На відміну від прихованого вказівника *this, який фактично є параметром функції, який компілятор використовує для “вказівки на самого себе”, *__vptr є реальним вказівником. Отже, розмір кожного об’єкта збільшується на розмір цього вказівника. *__vptr також успадковується дочірніми класами.

Зараз ви, швидше за все, трохи здивовані і, можливо, задаєтесь питанням: “Як це все разом працює?”. Тому давайте розглянемо наступний простий приклад:

Тут у нас є 3 класи, відповідно, компілятор створить 3 віртуальні таблиці: одна для Parent, одна для C1 і одна для C2.

Компілятор також додасть прихований вказівник на головний батьківський клас з віртуальними функціями. Хоча компілятор робить це автоматично, ми покажемо, де цей вказівник додається:

При створенні об’єктів класів Parent, C1 або C2, *__vptr вказуватиме на віртуальну таблицю класу Parent, C1 або C2 (відповідно).

Як заповнюються віртуальні таблиці?

У прикладі, наведеному вище, у нас є тільки дві віртуальні функції, тому кожна віртуальна таблиця матиме два записи (один для function1() і один для function2()). Пам’ятайте, що при заповненні віртуальних таблиць вибираються “найдочірніші” методи, доступ до яких мають об’єкти.

Віртуальна таблиця для об’єктів класу Parent проста. Об’єкт класу Parent має доступ тільки до членів класу Parent, він не має доступ до членів класів C1 і C2. Отже, запис function1 вказуватиме на Parent::function1(), а запис function2 вказуватиме на Parent::function2().

Віртуальна таблиця для C1 вже трохи складніша. Об’єкт класу C1 має доступ як до членів C1, так і до членів Parent. Однак C1 має перевизначення function1(), що робить C1::function1() більш дочірнім методом, ніж Parent::function1(). Отже, запис function1 вказуватиме на C1::function1(). C1 не перекриває function2(), тому запис function2 залишається вказувати на Parent::function2().

У віртуальній таблиці для C2 запис function1 вказуватиме на Parent::function1(), а запис function2 вказуватиме на C2::function2().

Дивимося:

Хоча тут уже можна здивуватися вдруге, все, насправді, дуже просто: *__vptr кожного класу вказує на віртуальну таблицю цього ж класу. Записи в віртуальній таблиці вказують на “найдочірніші” методи (перевизначення), доступ до яких мають об’єкти.

Розглянемо, що станеться при створенні об’єкта класу C1:

Оскільки c1 є об’єктом класу C1, то він має свій *__vptr, який вказує на віртуальну таблицю класу C1.

Тепер створимо вказівник класу Parent на об’єкт c1:

Оскільки cPtr є вказівником класу Parent, то він вказує лише на частину Parent об’єкта c1. Однак, *__vptr теж знаходиться в частині Parent, тому cPtr має доступ до цього вказівника. Нарешті, cPtr->__vptr вказуватиме на віртуальну таблицю C1, оскільки cPtr вказує на об’єкт класу C1! Навіть якщо cPtr є вказівником класу Parent, він все одно має доступ до віртуальної таблиці C1.

Тому, що відбудеться, якщо ми спробуємо викликати cPtr->function1()?

По-перше, компілятор розпізнає, що function1() є віртуальною функцією. По-друге, він використовуватиме cPtr->__vptr для переходу до віртуальної таблиці C1. По-третє, він шукатиме, яку версію function1() викликати у віртуальній таблиці C1. Він знайде C1::function1(). Отже, cPtr->function1() викликатиме C1::function1()!

Тепер ви можете запитати: «А якби cPtr вказував на об’єкт класу Parent замість об’єкта класу C1? Викликав би він як і раніше C1::function1()?». Відповідь: “Ні, не викликав би!”.

В цьому випадку, при створенні об’єкта p, *__vptr вказує на віртуальну таблицю класу Parent замість C1. Отже, pPtr->__vptr також вказуватиме на віртуальну таблицю класу Parent. Запис function1() у віртуальній таблиці класу Parent вказуватиме на Parent::function1(). Таким чином, pPtr->function1() викликатиме Parent::function1(), який є “найдочірнішим” методом, доступ до якого має об’єкт p.

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

Виклик віртуальної функції відбувається повільніше, ніж виклик невіртуальної функції, тому що:

   По-перше, ми повинні використовувати *__vptr для переходу до відповідної віртуальної таблиці.

   По-друге, ми повинні індексувати віртуальну таблицю, щоб знайти правильну функцію для виклику.

   І тільки тепер ми зможемо виконати виклик функції.

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

Висновки

Будь-клас, який використовує віртуальні функції, має свій *__vptr, і розмір кожного об’єкта цього класу збільшується на розмір цього вказівника. Віртуальні функції потужні, але ціна цього — продуктивність.

Оцінити статтю:

1 Зірка2 Зірки3 Зірки4 Зірки5 Зірок (2 оцінок, середня: 5,00 з 5)
Loading...

Залишити відповідь

Ваш E-mail не буде опублікований. Обов'язкові поля відмічені *