Что такое vtable
?
Может быть полезно узнать, о чем идет речь в сообщении об ошибке, прежде чем пытаться это исправить. Я начну с высокого уровня, а затем рассмотрю некоторые детали. Таким образом, люди могут пропустить вперед, как только они освоятся с пониманием vtables. ... и там идет группа людей, пропускающих прямо сейчас. :) Для тех, кто торчит:
Vtable в основном является наиболее распространенной реализацией полиморфизма в C ++ . Когда используются vtables, у каждого полиморфного класса есть vtable где-нибудь в программе; Вы можете думать об этом как о (скрытом) static
члене данных класса. Каждый объект полиморфного класса связан с vtable для его самого производного класса. Проверяя эту связь, программа может проявить свою полиморфную магию. Важное предупреждение: vtable - это деталь реализации. Это не предусмотрено стандартом C ++, хотя большинство (все?) Компиляторов C ++ используют vtables для реализации полиморфного поведения. Детали, которые я представляю, являются либо типичными, либо разумными подходами. Компиляторы могут отклоняться от этого!
Каждый полиморфный объект имеет (скрытый) указатель на vtable для самого производного класса объекта (возможно, несколько указателей, в более сложных случаях). Посмотрев на указатель, программа может сказать, что такое «настоящий» тип объекта (кроме как во время конструирования, но давайте пропустим этот особый случай). Например, если объект типа A
не указывает на vtable of A
, то этот объект на самом деле является подобъектом чего-то, полученного из A
.
Название «виртуальные таблицы» происходит от « об irtual функции таблицы ». Это таблица, в которой хранятся указатели на (виртуальные) функции. Компилятор выбирает свое соглашение о том, как таблица составлена; простой подход состоит в том, чтобы пройти через виртуальные функции в порядке, в котором они объявлены в определениях классов. Когда вызывается виртуальная функция, программа следует за указателем объекта на vtable, переходит к записи, связанной с требуемой функцией, затем использует указатель сохраненной функции для вызова правильной функции. Существуют различные приемы для создания этой работы, но я не буду вдаваться в подробности.
Где / когда vtable
генерируется?
Vtable автоматически генерируется (иногда называемый "испускаемым") компилятором. Компилятор может генерировать виртуальную таблицу в каждом модуле перевода, который видит определение полиморфного класса, но это, как правило, будет излишним излишним. Альтернатива ( используемая gcc и, возможно, другими) заключается в том, чтобы выбрать одну единицу трансляции, в которую нужно поместить виртуальную таблицу, подобно тому, как вы выбираете один исходный файл, в который помещаются статические члены данных класса. Если этот процесс выбора не может выбрать какие-либо единицы перевода, тогда vtable становится неопределенной ссылкой. Отсюда и ошибка, сообщение которой по общему признанию не особо ясно.
Точно так же, если процесс выбора выбирает единицу перевода, но этот объектный файл не предоставляется компоновщику, тогда vtable становится неопределенной ссылкой. К сожалению, сообщение об ошибке может быть даже менее четким в этом случае, чем в случае неудачного процесса выбора. (Спасибо ответчикам, которые упомянули эту возможность. Я, вероятно, забыл бы это иначе.)
Процесс выбора, используемый gcc, имеет смысл, если мы начнем с традиции выделять (один) исходный файл каждому классу, который нужен для его реализации. Было бы неплохо создать vtable при компиляции этого исходного файла. Давайте назовем это нашей целью. Однако процесс отбора должен работать, даже если эта традиция не будет соблюдена. Поэтому вместо того, чтобы искать реализацию всего класса, давайте посмотрим на реализацию конкретного члена класса. Если следовать традициям - и если этот член действительно реализован - тогда это достигает цели.
Элемент, выбранный gcc (и, возможно, другими компиляторами), является первой не встроенной виртуальной функцией, которая не является чисто виртуальной. Если вы являетесь частью толпы, которая объявляет конструкторы и деструкторы перед другими функциями-членами, то этот деструктор имеет хорошие шансы быть выбранным. (Вы не забыли сделать виртуальный деструктор, верно?) Есть исключения; Я ожидаю, что наиболее распространенными исключениями являются случаи, когда для деструктора предоставляется встроенное определение и когда запрашивается деструктор по умолчанию (с использованием « = default
»).
Проницательный может заметить, что полиморфному классу разрешено предоставлять встроенные определения для всех его виртуальных функций. Разве это не вызывает сбой процесса выбора? Это происходит в старых компиляторах. Я читал, что последние компиляторы справились с этой ситуацией, но я не знаю соответствующих номеров версий. Я мог бы попытаться найти это, но легче либо кодировать его, либо ждать, пока компилятор пожалуется.
Таким образом, существует три основных причины ошибки «неопределенная ссылка на vtable»:
- Функция-член не имеет своего определения.
- Объектный файл не связан.
- Все виртуальные функции имеют встроенные определения.
Эти причины сами по себе недостаточны, чтобы вызвать ошибку самостоятельно. Скорее, это то, к чему вы обратились бы, чтобы устранить ошибку. Не ожидайте, что намеренное создание одной из этих ситуаций определенно приведет к этой ошибке; Есть и другие требования. Ожидайте, что разрешение этих ситуаций разрешит эту ошибку.
(ОК, номер 3, возможно, было достаточно, когда был задан этот вопрос.)
Как исправить ошибку?
Добро пожаловать обратно, люди пропускают вперед! :)
- Посмотрите на определение вашего класса. Найдите первую не встроенную виртуальную функцию, которая не является чисто виртуальной (не "
= 0
") и определение которой вы предоставляете (не " = default
").
- Если такой функции нет, попробуйте изменить класс так, чтобы он был. (Возможно, ошибка устранена.)
- Смотрите также ответ Филип Томас за оговорку.
- Найдите определение для этой функции. Если он отсутствует, добавьте его! (Возможно, ошибка устранена.)
- Проверьте вашу команду ссылки. Если он не упоминает объектный файл с определением этой функции, исправьте это! (Возможно, ошибка устранена.)
- Повторите шаги 2 и 3 для каждой виртуальной функции, затем для каждой не виртуальной функции, пока ошибка не будет устранена. Если вы все еще застряли, повторите для каждого члена статических данных.
Пример
Детали того, что нужно сделать, могут различаться и иногда разбиваться на отдельные вопросы (например, Что такое неопределенная ссылка / нерешенная внешняя ошибка символа и как ее исправить? ). Тем не менее, я приведу пример того, что делать в конкретном случае, который может сбить с толку новых программистов.
Шаг 1 упоминает изменение вашего класса, чтобы он имел функцию определенного типа. Если описание этой функции перешло вам голову, вы можете оказаться в ситуации, которую я собираюсь рассмотреть. Имейте в виду, что это способ достижения цели; это не единственный способ, и в вашей конкретной ситуации легко могут быть лучшие способы. Давайте назовем ваш класс A
. Ваш деструктор объявлен (в вашем определении класса) как
virtual ~A() = default;
или
virtual ~A() {}
? Если это так, два шага изменит ваш деструктор на тот тип функции, который мы хотим. Во-первых, измените эту строку на
virtual ~A();
Во-вторых, поместите следующую строку в исходный файл, который является частью вашего проекта (предпочтительно файл с реализацией класса, если он у вас есть):
A::~A() {}
Это делает ваш (виртуальный) деструктор не встроенным и не генерируемым компилятором. (Не стесняйтесь изменять вещи, чтобы они лучше соответствовали вашему стилю форматирования кода, например, добавив комментарий заголовка к определению функции.)