Безопасное использование контейнеров в интерфейсе библиотеки С++

При разработке библиотеки С++ я читал, что плохой практикой является включение стандартных библиотек, таких как std::vector в открытый интерфейс (см., например, Последствия использования std::vector в dll экспортированная функция).

Что делать, если я хочу выставить функцию, которая принимает или возвращает список объектов? Я мог бы использовать простой массив, но тогда мне пришлось бы добавить параметр count, что сделает интерфейс более громоздким и менее безопасным. Кроме того, это не помогло бы, если бы я хотел использовать map, например. Я думаю, что библиотеки, такие как Qt, определяют свои собственные контейнеры, которые безопасны для экспорта, но я бы предпочел не добавлять Qt в качестве зависимости, и я не хочу откатывать свои собственные контейнеры.

Какая наилучшая практика касается контейнеров в интерфейсе библиотеки? Может быть, крошечная реализация контейнера (желательно только один или два файла, которые я могу зайти, с разрешительной лицензией), которые я могу использовать как "клей"? Или существует даже способ сделать std::vector и т.д. Безопасным через границы .DLL/.so и с разными компиляторами?

Ответ 1

На самом деле это относится не только к контейнерам STL, но применимо практически ко всем типам С++ (в частности, также к всем другим типам стандартных библиотек).

Поскольку ABI не стандартизирован, вы можете столкнуться со всеми неприятностями. Обычно для каждой поддерживаемой версии компилятора вам необходимо предоставить отдельные двоичные файлы, чтобы они работали. Единственный способ получить действительно портативную DLL - придерживаться простого интерфейса C. Обычно это приводит к чему-то вроде COM, так как вы должны убедиться, что все распределения и соответствующие деаллокации происходят в одном модуле, и что никаких подробностей о фактический макет объекта предоставляется пользователю.

Ответ 2

Вы можете реализовать функцию шаблона. Это имеет два преимущества:

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

Например, поместите это в свой файл заголовка:

template <typename Iterator>
void foo(Iterator begin, Iterator end)
{
  for (Iterator it = begin; it != end; ++it)
    bar(*it); // a function in your library, whose ABI doesn't depend on any container
}

Затем ваши пользователи могут вызывать foo с любым типом контейнера, даже те, которые они изобрели, о которых вы не знаете.

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

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

typedef bool(*Callback)(int value, void* userData);
void getElements(Callback cb, void* userData) // implementation in .cpp file, not header
{
  for (int value : internalContainer)
    if (!cb(value, userData))
      break;
}

Это довольно старый учебный курс "С", но он дает вам стабильный интерфейс и довольно удобен для использования практически любым абонентом (даже реальный код C с незначительными изменениями). Эти две ошибки - это void * userData, чтобы пользователь мог помешать некоторому контексту там (скажем, если они хотят вызвать функцию-член) и тип возврата bool, чтобы позволить обратному сообщению сказать вам остановиться. Вы можете сделать обратный вызов намного более привлекательным с помощью std:: function или что-то еще, но это может привести к поражению некоторых ваших других целей.

Ответ 3

TL; DR Нет проблем, если вы распространяете либо исходный код, либо скомпилированные двоичные файлы для различных поддерживаемых наборов (реализация ABI + Standard Library).

В целом, последний считается громоздким (с причинами), поэтому руководство.


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

Это руководство связано с проблемой совместимости с ABI: ABI представляет собой сложный набор спецификаций, который определяет точный интерфейс скомпилированной библиотеки. Он включает в себя:

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

Подробнее см., например, Itanium ABI. В отличие от C, который имеет очень простой ABI, С++ имеет гораздо более сложную площадь поверхности... и поэтому для него создано множество различных ABI.

Помимо совместимости с ABI, также существует проблема с реализацией стандартной библиотеки. Большинство компиляторов имеют собственную реализацию Стандартной библиотеки, и эти реализации несовместимы друг с другом (они, например, не представляют собой std::vector одинаково, хотя все реализуют один и тот же интерфейс и гарантии).

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

Приветствия: нет проблемы, если вы распространяете исходный код и компилируете клиент.

Ответ 4

Если вы используете С++ 11, вы можете использовать cppcomponents. https://github.com/jbandela/cppcomponents

Это позволит вам использовать между прочим std::vector как параметр или возвращаемое значение в файлах Dll/.so, созданных с использованием разных компиляторов или стандартных библиотек. Взгляните на мой ответ на аналогичный вопрос для примера Передача ссылки на вектор STL через границу dll

Примечание для примера вам нужно добавить CPPCOMPONENTS_REGISTER(ImplementFiles) после оператора CPPCOMPONENTS_DEFINE_FACTORY()