Почему виртуальная функция C++, определенная в заголовке, не может быть скомпилирована и связана в vtable?

Ситуация следует. Я совместно использовал библиотеку, которая содержит определение класса -

QueueClass : IClassInterface
{
   virtual void LOL() { do some magic}
}

Моя общая библиотека инициализирует участника класса

QueueClass *globalMember = new QueueClass();

Моя библиотека доли экспортирует функцию C, которая возвращает указатель на globalMember -

void * getGlobalMember(void) { return globalMember;}

Мое приложение использует globalMember как это

((IClassInterface*)getGlobalMember())->LOL();

Теперь очень uber материал - если я не ссылаюсь на LOL из общей библиотеки, затем LOL не связан в, и вызов его из приложения повышает исключение. Причина - VTABLE содержит nul вместо указателя на LOL () функция.

Когда я перемещаю LOL () определение от.h файла до .cpp, внезапно это появляется в VTABLE, и все просто работает отлично. Что объясняет это поведение?! (gcc компилятор + архитектура ARM _)

6
задан Brian Tompsett - 汤莱恩 9 February 2017 в 20:28
поделиться

5 ответов

Виновником является линкер. Когда функция встроена, у нее есть несколько определений, по одному в каждом файле cpp, где на нее ссылаются. Если ваш код никогда не ссылается на функцию, он никогда не создается.

Однако макет vtable определяется во время компиляции с определением класса. Компилятор может легко определить, что LOL () - это виртуальная функция, и для нее требуется запись в vtable .

Когда доходит до времени связывания для приложения, оно пытается заполнить все значения QueueClass :: _ VTABLE , но не находит определения LOL () и оставляет его пустым (null).

Решение состоит в том, чтобы ссылаться на LOL () в файле в общей библиотеке. Что-то столь же простое, как & QueueClass :: LOL; . Возможно, вам придется назначить его переменной для выброса, чтобы компилятор перестал жаловаться на утверждения, которые не действуют.

6
ответ дан 10 December 2019 в 02:43
поделиться

Функции, определенные в файлах заголовков, размещаются по мере использования. Они не скомпилированы как часть библиотеки; вместо этого там, где выполняется вызов, код функции просто заменяет код вызова, и это то, что компилируется.

Я не удивлен, увидев, что вы не нашли запись v-table (на что бы она указывала?), И я не удивился, увидев, что перемещение определения функции в файл .cpp внезапно заставляет вещи работать. Я немного удивлен тем, что создание экземпляра объекта с вызовом в библиотеке имеет значение.

Я не уверен, что это поспешность с вашей стороны, но из предоставленного кода IClassInterface не обязательно содержит LOL, только QueueClass. Но вы выполняете приведение к указателю IClassInterface, чтобы выполнить вызов LOL.

0
ответ дан 10 December 2019 в 02:43
поделиться

Если этот пример упрощен и ваше фактическое дерево наследования использует множественное наследование, это можно легко объяснить. Когда вы выполняете приведение типа для указателя объекта, компилятор должен настроить указатель так, чтобы на него ссылалась правильная vtable. Поскольку вы возвращаете void * , компилятор не имеет необходимой информации для выполнения настройки.

Изменить: Не существует стандарта для компоновки объектов C ++, но для примера того, как может работать множественное наследование, см. Эту статью самого Бьярна Страуструпа: http: //www-plan.cs.colorado. edu / diwan / class-paper / mi.pdf

Если это действительно ваша проблема, вы можете исправить ее одним простым изменением:

IClassInterface *globalMember = new QueueClass();

Компилятор C ++ внесет необходимые изменения в указатель, когда сделает присваивание , чтобы функция C могла вернуть правильный указатель.

0
ответ дан 10 December 2019 в 02:43
поделиться

Я не согласен с @sechastain .

Встраивание далеко не автоматическое. Независимо от того, определен ли метод на месте или используется подсказка ( inline ключевое слово или __ forceinline ), компилятор - единственный, кто решает, действительно ли произойдет встраивание, и для этого используется сложная эвристика. Однако один частный случай заключается в том, что он не должен встраивать вызов, когда виртуальный метод вызывается с использованием диспетчеризации во время выполнения, именно потому, что диспетчеризация во время выполнения и встраивание несовместимы.

Чтобы понять точность «использования диспетчеризации во время выполнения»:

IClassInterface* i = /**/;
i->LOL();                   // runtime dispatch
i->QueueClass::LOL();       // compile time dispatch, inline is possible

@ 0xDEAD BEEF : Я считаю ваш дизайн хрупким, если не сказать больше.

Использование приведений в стиле C здесь неверно:

QueueClass* p = /**/;
IClassInterface* q = p;

assert( ((void*)p) == ((void*)q) ); // may fire or not...

По сути, нет гарантии, что 2 адреса равны: это определено реализацией и вряд ли сопротивляется изменениям.

Я хочу, чтобы вы могли безопасно преобразовать указатель void * в указатель IClassInterface * , тогда вам нужно создать его из IClassInterface * изначально так что компилятор C ++ может выполнять правильную арифметику указателя в зависимости от расположения объектов.

Конечно, я также подчеркну, чем использование глобальных переменных ... вы, наверное, это знаете.

Что касается причины отсутствия? Честно говоря, я не вижу ничего, кроме ошибки в компиляторе / компоновщике.Я видел встроенное определение виртуальных функций несколько раз (точнее, метод clone ), и это никогда не вызывало проблем.

РЕДАКТИРОВАТЬ : Поскольку «правильная арифметика указателей» не была так хорошо понята, вот пример

struct Base1 { char mDum1; };

struct Base2 { char mDum2; };

struct Derived: Base1, Base2 {};

int main(int argc, char* argv[])
{
  Derived d;
  Base1* b1 = &d;
  Base2* b2 = &d;

  std::cout << "Base1: " << b1
          << "\nBase2: " << b2
          << "\nDerived: " << &d << std::endl;

  return 0;
}

И вот что было напечатано:

Base1: 0x7fbfffee60
Base2: 0x7fbfffee61
Derived: 0x7fbfffee60

Нет разницы между значением b2 и & d , даже если они относятся к одному объекту. Это можно понять, если подумать о структуре памяти объекта.

Derived
Base1     Base2
+-------+-------+
| mDum1 | mDum2 |
+-------+-------+

При преобразовании из Derived * в Base2 * компилятор выполнит необходимую настройку (здесь увеличит адрес указателя на один байт), чтобы указатель в конечном итоге фактически указывал к части Base2 Derived , а не к части Base1 , ошибочно интерпретируемой как объект Base2 (что было бы неприятно).

Вот почему следует избегать использования приведений в стиле C при понижающем преобразовании. Здесь, если у вас есть указатель Base2 , вы не можете интерпретировать его заново как указатель Derived . Вместо этого вам придется использовать static_cast (b2) , который уменьшит указатель на один байт, чтобы он правильно указывал на начало объекта Derived .

Обработка указателей обычно называется арифметикой указателей. Здесь компилятор автоматически выполнит правильную настройку ... при условии, что он знает тип.

К сожалению, компилятор не может выполнить их при преобразовании из void * , поэтому разработчик должен убедиться, что он правильно обработал это. Простое практическое правило следующее: T * -> void * -> T * с одинаковым типом, отображаемым с обеих сторон.

Следовательно, вы должны (просто) исправить свой код, объявив: IClassInterface * globalMember , и у вас не возникнет проблем с переносимостью. У вас, вероятно, все еще будет проблема с обслуживанием, но это проблема использования C с OO-кодом: C не знает о каких-либо объектно-ориентированных вещах.

2
ответ дан 10 December 2019 в 02:43
поделиться

Я предполагаю, что GCC использует возможность встроить вызов LOL. Я посмотрю, смогу ли я найти для вас ссылку на это ...

Я вижу, что sechastain превзошел меня в более подробном описании, и я не смог найти в Google ссылку, которую искал. Так что я оставлю все как есть.

1
ответ дан 10 December 2019 в 02:43
поделиться
Другие вопросы по тегам:

Похожие вопросы: