Как разработать API С++ для двоичной совместимой расширяемости
Я разрабатываю API для библиотеки С++, которая будет распространяться в dll/shared object. Библиотека содержит полиморфные классы с виртуальными функциями. Я обеспокоен тем, что, если я раскрываю эти виртуальные функции в DLL API, я избавляюсь от возможности расширения тех же классов с более виртуальными функциями, не нарушая бинарную совместимость с приложениями, созданными для предыдущей версии библиотеки.
Один из вариантов заключается в использовании PImpl, чтобы скрыть все классы, имеющие виртуальные функции, но у них также есть ограничения: это которые теряют возможность подклассификации классов библиотеки и переопределения виртуальных методов.
Как вы создадите класс API, который может быть подклассифицирован в приложении, не теряя возможности расширять API с помощью (не абстрактных) виртуальных методов в новой версии dll при сохранении обратного двоичного соответствия?
Обновление: целевыми платформами для библиотеки являются windows/msvc и linux/gcc.
Ответы
Ответ 1
Несколько месяцев назад я написал статью под названием "Бинарная совместимость общих библиотек, реализованных в С++ в системах GNU/Linux" [pdf], Хотя концепции аналогичны в системе Windows, я уверен, что они не совсем то же самое. Но, прочитав статью, вы можете получить представление о том, что происходит на двоичном уровне С++, который имеет какое-либо отношение к совместимости.
Кстати, двоичный интерфейс приложения GCC суммируется в проекте стандартного документа " Itanium ABI", поэтому у вас будет формальное основание для выбранного стандарта кодирования.
Просто для быстрого примера: в GCC вы можете расширить класс с более виртуальными функциями, если ни один другой класс не наследует его. Прочтите статью для лучшего набора правил.
Но в любом случае правила иногда слишком сложны для понимания. Поэтому вам может быть интересен инструмент, который проверяет совместимость двух заданных версий: abi-compliance-checker для Linux.
Ответ 2
Есть интересная статья о базе знаний KDE, которая описывает действия do и don'ts, когда вы направляете бинарную совместимость при написании библиотеки: Политики/проблемы с двоичной совместимостью с С++
Ответ 3
C++ двоичная совместимость обычно сложна, даже без наследования. Посмотрите, например, на GCC. За последние 10 лет я не уверен, сколько избило изменения ABI, которые у них были. Тогда MSVC имеет другой набор условных обозначений, поэтому связь с GCC и наоборот не может быть выполнена... Если вы сравните это с миром C, компилятор будет лучше там.
Если вы находитесь в Windows, вы должны посмотреть на COM. Когда вы вводите новую функциональность, вы можете добавлять интерфейсы. Затем вызывающие могут QueryInterface()
, чтобы новый обнаружил эту новую функциональность, и даже если вы в конечном итоге меняете многое, вы можете либо оставить старую реализацию там, либо написать прокладки для старых интерфейсов.
Ответ 4
Я думаю, вы неправильно поняли проблему подкласса.
Вот ваш Pimpl:
// .h
class Derived
{
public:
virtual void test1();
virtual void test2();
private;
Impl* m_impl;
};
// .cpp
struct Impl: public Base
{
virtual void test1(); // override Base::test1()
virtual void test2(); // override Base::test2()
// data members
};
void Derived::test1() { m_impl->test1(); }
void Derived::test2() { m_impl->test2(); }
Видите? Нет проблем с переопределением виртуальных методов Base
, вам просто нужно убедиться, что они обновили их virtual
в Derived
, чтобы те, которые получены из Derived, знали, что они могут переписать их тоже (только если вы этого хотите, путь - отличный способ предоставить final
тем, у кого его нет), и вы все еще можете переопределить его для себя в Impl
, который может даже назвать версию Base
.
Нет проблем с Pimpl
.
С другой стороны, вы теряете полиморфизм, который может быть хлопотным. Вам решать, хотите ли вы полиморфизм или просто состав.
Ответ 5
Если вы выставляете класс PImpl в файле заголовка, вы можете наследовать его. Вы по-прежнему можете поддерживать обратную переносимость, поскольку внешние классы содержат указатель на объект PImpl. Конечно, если клиентский код библиотеки не очень мудрый, он может неправильно использовать этот открытый объект PImpl и разрушить двоичную обратную совместимость. Вы можете добавить некоторые примечания, чтобы предупредить пользователя в заголовочном файле PImpl.