Що таке а vtable
?
Можливо, буде корисно дізнатися, про що йдеться у повідомленні про помилку, перш ніж спробувати її виправити. Почну з високого рівня, а потім перейду до деяких деталей. Таким чином, люди можуть пропуститись вперед, як тільки їм буде зручно з розумінням рейтингів. … І зараз йде купа людей, що пропускають попереду прямо зараз. :) Для тих, хто тримається навколо:
В основному, vtable є найбільш поширеною реалізацією поліморфізму в C ++ . Коли використовуються vtables, кожен поліморфний клас має vtable десь у програмі; ви можете думати про це як про (прихованому) static
члені даних класу. Кожен об'єкт поліморфного класу асоціюється з vtable для його найбільш похідного класу. Перевіряючи цю асоціацію, програма може відпрацьовувати свою поліморфну магію. Важливий застереження: vtable - деталь реалізації. Це не передбачено стандартом C ++, хоча більшість (усі?) Компіляторів C ++ використовують vtables для реалізації поліморфної поведінки. Я представляю деталі або типовий, або розумний підхід. Компілятори дозволяють відступати від цього!
Кожен поліморфний об'єкт має (прихований) вказівник на vtable для найбільш похідного класу об'єкта (можливо, декілька покажчиків у складніших випадках). Подивившись на вказівник, програма може сказати, що таке «реальний» тип об’єкта (за винятком під час будівництва, але пропустимо цей особливий випадок). Наприклад, якщо об’єкт типу A
не вказує на vtable з A
, то цей об'єкт насправді є суб’єктом чогось, що походить від A
.
Назва «віртуальні таблиці» походить від « про irtual функції таблиці ». Це таблиця, в якій зберігаються вказівники на (віртуальні) функції. Компілятор вибирає свою умову щодо того, як складається таблиця; простий підхід полягає у проходженні віртуальних функцій у тому порядку, в якому вони оголошені у визначеннях класу. Коли викликується віртуальна функція, програма слідує за вказівником об'єкта на vtable, переходить до запису, пов'язаного з потрібною функцією, а потім використовує збережений вказівник функції для виклику правильної функції. Існують різні хитрощі, щоб зробити цю роботу, але я тут не буду вникати в них.
Де / коли vtable
генерується?
Компілятор автоматично створює vtable (іноді його називають "emit"). Компілятор може випустити vtable у кожному блоці перекладу, який бачить визначення поліморфного класу, але це, як правило, зайве зайве. Альтернативою ( використовуваною gcc , і, мабуть, іншими), є вибір єдиного блоку перекладу, в якому розмістити vtable, аналогічно тому, як ви вибрали б єдиний вихідний файл, у який слід помістити статичні дані класу класу. Якщо цей процес відбору не зможе вибрати будь-які одиниці перекладу, то vtable стає невизначеною посиланням. Звідси помилка, повідомлення якої, мабуть, не особливо чітка.
Аналогічно, якщо процес відбору вибирає одиницю перекладу, але цей об'єктний файл не надається до лінкера, то vtable стає невизначеною посиланням. На жаль, повідомлення про помилку може бути навіть менш зрозумілим у цьому випадку, ніж у випадку, коли процес відбору не вдався. (Завдяки відповідачам, які згадували про таку можливість. Я, мабуть, забув би її інакше.)
Процес вибору, використовуваний gcc, має сенс, якщо ми починаємо з традиції виділення (єдиного) вихідного файлу кожному класу, якому потрібен його для його реалізації. Було б непогано видавати vtable при компілюванні цього вихідного файлу. Назвемо це нашою метою. Однак процес відбору повинен працювати, навіть якщо цієї традиції не дотримуватися. Тож замість того, щоб шукати впровадження всього класу, давайте шукатимемо реалізацію конкретного члена класу. Якщо дотримуватися традиції - і якщо цей член насправді впроваджується, - це досягає мети.
Член, обраний gcc (і, можливо, іншими компіляторами), - це перша не вбудована віртуальна функція, яка не є чисто віртуальною. Якщо ви є частиною натовпу, який оголошує конструкторів та деструкторів перед іншими функціями-членами, то цей деструктор має хороші шанси бути обраним. (Ви пам'ятали, щоб зробити деструктор віртуальним, правда?) Є винятки; Я б очікував, що найпоширеніші винятки - це коли для деструктора надається вбудоване визначення та коли запитується деструктор за замовчуванням (використовуючи " = default
").
Проникливий може помітити, що поліморфному класу дозволено надавати вбудовані визначення для всіх його віртуальних функцій. Це не викликає збій процесу відбору? Це робиться у старих компіляторах. Я читав, що останні компілятори вирішили цю ситуацію, але не знаю відповідних номерів версій. Я міг би спробувати переглянути це, але простіше або кодувати його навколо, або чекати, коли компілятор поскаржиться.
Підсумовуючи, є три основні причини помилки "невизначена посилання на vtable":
- Функція-член не має свого визначення.
- Об'єктний файл не пов'язаний.
- Усі віртуальні функції мають вбудовані визначення.
Ці причини самі по собі є недостатніми, щоб самостійно викликати помилку. Швидше, це те, на що ви звернетесь, щоб вирішити помилку. Не сподівайтесь, що навмисне створення однієї з цих ситуацій неодмінно призведе до цієї помилки; є інші вимоги. Очікуйте, що вирішення цих ситуацій усуне цю помилку.
(Гаразд, номер 3, можливо, було б достатньо, коли це питання було задано.)
Як виправити помилку?
Ласкаво просимо, люди пропускають вперед! :)
- Подивіться на своє визначення класу. Знайдіть першу не вбудовану віртуальну функцію, яка не є чистою віртуальною (не "
= 0
") та визначення якої ви надаєте (не " = default
").
- Якщо такої функції немає, спробуйте змінити свій клас, щоб він був. (Помилка можливо усунена.)
- Дивіться також відповідь Філіпа Томаса про застереження.
- Знайдіть визначення для цієї функції. Якщо він відсутній, додайте його! (Помилка можливо усунена.)
- Перевірте свою команду посилання. Якщо він не згадує об’єктний файл із визначенням цієї функції, виправте це! (Помилка можливо усунена.)
- Повторіть кроки 2 і 3 для кожної віртуальної функції, потім для кожної невіртуальної функції, поки помилка не буде усунена. Якщо ви все ще застрягли, повторіть для кожного члена статичних даних.
Приклад
Деталі того, що робити, можуть змінюватись, а іноді й розбиватися на окремі запитання (наприклад, що таке невизначена посилання / невирішена помилка зовнішнього символу та як це виправити? ). Я, однак, наведу приклад того, що робити у конкретному випадку, який може зіпсувати нові програмісти.
У кроці 1 згадується модифікація вашого класу, щоб він мав функцію певного типу. Якщо опис цієї функції перевершить вашу голову, ви можете опинитися в ситуації, яку я маю на меті вирішити. Майте на увазі, що це спосіб досягти мети; це не єдиний спосіб, і у вашій конкретній ситуації легко можуть бути кращі шляхи. Давайте назвемо ваш клас A
. Чи оголошено ваш деструктор (у визначенні вашого класу) як будь-який
virtual ~A() = default;
або
virtual ~A() {}
? Якщо так, два кроки перетворять ваш деструктор на тип потрібної нам функції. По-перше, змініть цей рядок на
virtual ~A();
По-друге, помістіть наступний рядок у вихідний файл, який є частиною вашого проекту (бажано, файл із реалізацією класу, якщо у вас є):
A::~A() {}
Це робить ваш (віртуальний) деструктор не вбудованим, а не генерується компілятором. (Не соромтеся змінювати речі, щоб вони краще відповідали вашому стилю форматування коду, наприклад додавання коментаря до заголовка до визначення функції.)