Чистые виртуальные функции и двоичная совместимость

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

У меня есть класс интерфейса и класс реализации, скомпилированные в общую библиотеку, например:

class Interface {
public:
static Interface* giveMeImplPtr();
...
virtual void Foo( uint16_t arg ) = 0;
...
}

class Impl {
public:
...
void Foo( uint16_t arg );
....
}

Мое основное приложение использует эту разделяемую библиотеку и может быть написано так:

Interface* foo = Implementation::giveMeImplPtr();
foo->Foo( 0xff );

Другими словами, приложение не имеет классов, которые являются производными от InterfaceОн просто использует это.

Теперь скажите, что я хочу перегрузить Foo( uint16_t arg ) с Foo( uint32_t arg )я в безопасности сделать:

 class Interface {
public:
static Interface* giveMeImplPtr();
...
virtual void Foo( uint16_t arg ) = 0;
virtual void Foo( uint32_t arg ) = 0;
...
}

и перекомпилировать мою общую библиотеку без необходимости перекомпилировать приложение?

Если так, есть ли какие-то необычные предостережения, о которых я должен знать? Если нет, есть ли у меня какие-либо другие варианты, кроме как взять и обновить версию библиотеки, тем самым нарушая обратную совместимость?

6

Решение

ABI в основном зависит от размера и формы объекта, включая vtable. Добавление виртуальной функции определенно изменит vtable, и то, как она изменится, зависит от компилятора.

В этом случае нужно учесть еще и то, что вы предлагаете не просто взломать изменение ABI, а взломать API, который очень сложно обнаружить во время компиляции. Если это не были виртуальные функции и совместимость с ABI не была проблемой, после вашего изменения, что-то вроде:

void f(Interface * i) {
i->Foo(1)
}

тихо завершит вызов вашей новой функции, но только если этот код будет перекомпилирован, что может затруднить отладку.

5

Другие решения

Простой ответ: нет. Каждый раз, когда вы меняете класс
определение в все, вы потенциально потеряете двоичную совместимость.
Добавление не виртуальной функции или статических членов обычно безопасно
на практике, хотя все еще формально неопределенное поведение, но
это об этом. Все остальное, вероятно, сломает двоичный файл
совместимость.

5

Вы пытаетесь описать популярный «Сделать классы не производными» методика сохранения бинарной совместимости, которая используется, например, в Symbian C ++ API (ищите NewL заводской метод):

  1. Обеспечить заводскую функцию;
  2. Объявите конструктор C ++ закрытым (и не экспортируемым, не встроенным, и у класса не должно быть дочерних классов или функций), это делает класс не выводимым, и тогда вы можете:

    • Добавьте виртуальные функции в конце объявления класса,
    • Добавьте данные членов и измените размер класса.

Эта техника работает только для НКУ компилятор, потому что он сохраняет исходный порядок виртуальных функций на двоичном уровне.

объяснение

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

Совместимость будет нарушена, если ваш класс имеет открытый конструктор (встроенный или не встроенный):

  • в соответствии: приложения будут копировать старую v-таблицу и старый макет памяти класса, которые будут отличаться от тех, которые используются в новой библиотеке; если вы вызываете какой-либо экспортируемый метод или передаете объект в качестве аргумента такому методу, это может привести к повреждению памяти из-за ошибки сегментации;

  • не-рядный: ситуация лучше, потому что вы можете изменить v-таблицу, добавив новые виртуальные методы в конец листового объявления класса, потому что компоновщик переместит макет v-таблицы производных классов на стороне клиента, если вы загрузите новый версия библиотеки; но вы все равно не можете изменить размер класса (то есть, добавив новые поля), потому что размер жестко задан во время компиляции, и вызов конструктора новой версии может разрушить память соседних объектов в стеке клиента или куче.

инструменты

Попробуйте использовать аби-податливость корректор инструмент для проверки обратной двоичной совместимости версий вашей библиотеки классов в Linux.

3

Это было очень удивительно для меня, когда я был в подобной ситуации, и я обнаружил, что MSVC неудачи порядок перегруженных функций. Согласно вашему примеру, MSVC создаст v_table (в двоичном виде) следующим образом:

virtual void Foo( uint32_t arg ) = 0;
virtual void Foo( uint16_t arg ) = 0;

Если мы немного расширим ваш пример, вот так:

class Interface {
virtual void first() = 0;
virtual void Foo( uint16_t arg ) = 0;
virtual void Foo( uint32_t arg ) = 0;
virtual void Foo( std::string arg ) = 0;
virtual void final() = 0;
}

MSVC создаст следующую v_table:

    virtual void first() = 0;
virtual void Foo( std::string arg ) = 0;
virtual void Foo( uint32_t arg ) = 0;
virtual void Foo( uint16_t arg ) = 0;
virtual void final() = 0;

Borland Builder и GCC не меняют порядок, но

  1. Они делают это не в тех версиях, которые я тестировал.
  2. Если ваша библиотека скомпилирована GCC (например), а приложение будет скомпилировано MSVC, это будет грандиозный сбой

Конец … Никогда не полагайтесь на двоичную совместимость. Любое изменение класса должно вызывать перекомпиляцию всего кода, использующего его.

2