2013-02-14 3 views
6

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

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

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; 
     ... 
} 

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

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

ответ

5

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

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

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

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

5

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

2

Это было очень удивительно для меня, когда я был в подобной ситуации, и я обнаружил, что 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 строителем и GCC не изменить порядок, но

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

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

2

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

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

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

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

Объяснение

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

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

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

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

Инструменты

Попробуйте использовать инструмент abi-compliance-checker проверить обратную бинарную совместимость ваших библиотек классов версий на Linux.

Смежные вопросы