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

Ситуация следующая.У меня есть общая библиотека, которая содержит определение класса -

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

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

QueueClass *globalMember = new QueueClass();

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

void * getGlobalMember(void) { return globalMember;}

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

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

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

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

Ответы [ 5 ]

6 голосов
/ 24 мая 2010

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

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

.

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

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

2 голосов
/ 24 мая 2010

Я не согласен с @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 ++ мог выполнять правильную арифметику указателей в зависимости от макета объектов.

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

Как по причине отсутствия? Честно говоря, я не вижу ничего, кроме ошибки в компиляторе / компоновщике. Я видел встроенное определение функций virtual несколько раз (точнее, метод 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<Derived*>(b2), который уменьшит указатель на один байт, чтобы он правильно указывал на начало объекта Derived.

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

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

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

1 голос
/ 24 мая 2010

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

0 голосов
/ 24 мая 2010

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

Редактировать: Не существует стандарта для компоновки объектов C ++, но один пример того, как может работать множественное наследование, смотрите в этой статье самого Бьярна Страуструпа: http://www -plan.cs.colorado .edu / Diwan / класс-бумага / mi.pdf

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

IClassInterface *globalMember = new QueueClass();

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

0 голосов
/ 24 мая 2010

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

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

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

...