Как разработать C++ API для бинарной совместимой расширяемости


Я разрабатываю API для библиотеки C++, которая будет распространяться в dll / shared объекте. Библиотека содержит полиморфные классы с виртуальными функциями. Я обеспокоен тем,что если я выставляю эти виртуальные функции на DLL API, я отрезаю себя от возможности расширения тех же классов с большим количеством виртуальных функций, не нарушая бинарную совместимость с приложениями, построенными для предыдущей версии библиотеки.

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

Как бы вы спроектировали класс API, который может быть подклассом в приложении, не теряя возможности расширить API с помощью (не абстрактных) виртуальных методов в новой версии dll, оставаясь при этом обратно бинарно совместимым?

Обновление: целевые платформы для библиотека-это windows / msvc и linux / gcc.

5 31

5 ответов:

Несколько месяцев назад я написал статью под названием "бинарная совместимость общих библиотек, реализованных в C++ на системах GNU/Linux" [pdf]. Хотя понятия схожи в системе Windows, я уверен, что они не совсем одинаковы. Но прочитав статью, вы можете получить представление о том, что происходит на двоичном уровне C++, что имеет какое-либо отношение к совместимости.

Кстати, двоичный интерфейс приложения GCC обобщен в проекте стандартного документа " Itanium ABI ", поэтому у вас будет формальное основание для выбранного вами стандарта кодирования.

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

Но в любом случае правила иногда слишком сложны для понимания. Поэтому вас может заинтересовать инструмент, который проверяет совместимость двух заданных версий: abi-compliance-checker для Linux.

В базе знаний KDE есть интересная статья, в которой описывается, что делать и чего не делать, когда речь идет о бинарной совместимости при написании библиотеки: политики / проблемы бинарной совместимости с C++

C++ binary compat, как правило, трудно, даже без наследования. Посмотри например на ССЗ. За последние 10 лет, я не уверен, сколько разрушительных изменений ABI у них было. Тогда MSVC имеет другой набор соглашений, поэтому привязка к нему с помощью GCC и наоборот не может быть выполнена... Если сравнить это с миром Си, то взаимодействие компиляторов там выглядит немного лучше.

Если вы находитесь на Windows, вы должны смотреть на COM. По мере внедрения новых функциональных возможностей можно добавлять интерфейсы. Затем звонящие может QueryInterface() для нового, чтобы предоставить эту новую функциональность, и даже если вы в конечном итоге изменить вещи много, вы можете либо оставить старую реализацию там, или вы можете написать оболочки для старых интерфейсов.

Я думаю, что вы неправильно понимаете проблему подклассов.

Вот ваш сутенер:

// .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, чтобы те, кто извлекает из производных, знали, что они могут переписать их тоже (только если вы этого хотите, что, кстати, является отличным способом предоставления final для тех, кто его не имеет), и вы все еще можете переопределить его для себя в Impl, который может даже вызвать Base.]} версия.

Там нет никаких проблем с Pimpl.

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

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