Дизайн библиотеки: позволяет пользователю выбирать между «только заголовками» и динамически связаны?

Я создал несколько библиотек C ++, которые в настоящее время заголовок только. И интерфейс, и реализация моих классов написаны одинаково .hpp файл.

Я недавно начал думать, что такой дизайн не очень хорош:

  1. Если пользователь хочет скомпилировать библиотеку и динамически связать ее, он не может.
  2. Изменение одной строки кода требует полной перекомпиляции существующих проектов, которые зависят от библиотеки.

Мне действительно нравятся аспекты библиотек только для заголовков: все функции потенциально встроены, и их очень очень легко включить в ваши проекты — не нужно ничего компилировать / связывать, просто #include директивы.

Можно ли получить лучшее из обоих миров? Я имею в виду — позволить пользователю выбирать, как он / она хочет использовать библиотеку. Это также ускорит разработку, так как я буду работать с библиотекой в ​​«режиме динамической компоновки», чтобы избежать абсурдного времени компиляции, и выпускать свои готовые продукты в «режиме только заголовков», чтобы максимизировать производительность.

Первым логическим шагом является разделение интерфейса и реализации в .hpp а также .inl файлы.

Я не уверен, как идти вперед, хотя. Я видел много готовых библиотек LIBRARY_API макросы к объявлениям их функций / классов — может быть, нужно что-то подобное, чтобы позволить пользователю выбирать?


Все мои библиотечные функции имеют префикс inline ключевое слово, чтобы избежать «множественное определение …» ошибки. Я предполагаю, что ключевое слово будет заменено на LIBRARY_INLINE макрос в .inl файлы? Макрос разрешил бы inline для «режима только заголовка» и ничего для «режима динамической компоновки».

28

Решение

Предварительное примечание: я предполагаю, что среда Windows, но это должно быть легко перенесено в другие среды.

Ваша библиотека должна быть подготовлена ​​к четырем ситуациям:

  1. Используется как библиотека только для заголовков
  2. Используется как статическая библиотека
  3. Используется как динамическая библиотека (функции импортируются)
  4. Построен как динамическая библиотека (функции экспортируются)

Итак, давайте составим четыре определения препроцессора для этих случаев: INLINE_LIBRARY, STATIC_LIBRARY, IMPORT_LIBRARY, а также EXPORT_LIBRARY (это всего лишь пример; вы можете использовать сложную схему именования).
Пользователь должен определить один из них, в зависимости от того, что он / она хочет.

Затем вы можете написать свои заголовки так:

// foo.hpp

#if defined(INLINE_LIBRARY)
#define LIBRARY_API inline
#elif defined(STATIC_LIBRARY)
#define LIBRARY_API
#elif defined(EXPORT_LIBRARY)
#define LIBRARY_API __declspec(dllexport)
#elif defined(IMPORT_LIBRARY)
#define LIBRARY_API __declspec(dllimport)
#endif

LIBRARY_API void foo();

#ifdef INLINE_LIBRARY
#include "foo.cpp"#endif

Ваш файл реализации выглядит как обычно:

// foo.cpp

#include "foo.hpp"#include <iostream>

void foo()
{
std::cout << "foo";
}

Если INLINE_LIBRARY определяется, функции объявляются встроенными, а реализация включается как файл .inl.

Если STATIC_LIBRARY определяется, функции объявляются без какого-либо спецификатора, и пользователь должен включить файл .cpp в свой процесс сборки.

Если IMPORT_LIBRARY определяется, функции импортируются, и нет необходимости в какой-либо реализации.

Если EXPORT_LIBRARY определяется, функции экспортируются, и пользователь должен скомпилировать эти файлы .cpp.

Переключение между статическим / импортом / экспортом — обычная вещь, но я не уверен, что хорошо ли добавлять в уравнение только заголовки. Обычно, есть веские причины для определения чего-то встроенного или нет.

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

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

14

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

Это зависит от операционной системы и компилятора. На Linux с очень недавним НКУ компилятор (версия 4.9) вы можете создать статический использование библиотеки межпроцедурная оптимизация времени соединения.

Это означает, что вы строите свою библиотеку с g++ -O2 -flto как во время компиляции, так и во время ссылки библиотеки, и что вы используете свою библиотеку с g++ -O2 -flto как во время компиляции, так и во время соединения вызывающей программы.

4

Это должно дополнить ответ @ Хорстлинга.


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

Динамически связанные библиотеки будут объединены в основной проект во время выполнения (а не во время ссылки). Когда вы компилируете библиотеку, вы получаете файл .dll (который содержит фактический скомпилированный код) и файл .lib (который содержит достаточно данных для компилятора / среды выполнения, чтобы найти функции / объекты в файле .dll). Во время компоновки исполняемый файл будет настроен для загрузки .dll и использования скомпилированного кода из этой .dll по мере необходимости. Вам нужно будет распространять файл .dll вместе с исполняемым файлом, чтобы иметь возможность его запустить.

При проектировании библиотеки нет необходимости выбирать между статическим или динамическим связыванием (или только заголовком), вы создаете несколько файлов проекта / make-файла, один для создания статического .lib, другой для создания пары .lib / .dll и распространяете обе версии, для пользователя на выбор. (Вам нужно будет использовать макросы препроцессора, подобные тем, которые предлагает @Horstling).


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

Также обратите внимание, что современные компиляторы / компоновщики обычно не учитывают встроенный модификатор. Они могут встроить функцию, даже если она не обозначена как встроенную, или могут динамически вызывать другую, которая имеет встроенный модификатор, как они считают нужным. (Независимо от этого, я бы посоветовал явно указать inline, где это применимо для максимальной совместимости). Таким образом, не будет никакого снижения производительности во время выполнения, если вы будете использовать статически связанную библиотеку вместо библиотеки только для заголовков (и, конечно, включите оптимизацию компилятора / компоновщика). Как и предполагали другие, для действительно небольших функций, которые наверняка выиграют от вызова inline, рекомендуется помещать их в заголовочные файлы, чтобы динамически связанные библиотеки также не испытывали значительного снижения производительности. (В любом случае встроенные функции влияют на производительность только тех функций, которые вызываются очень часто, внутри циклов, которые будут вызываться тысячи / миллионы раз).


Вместо помещения встроенных функций в заголовочные файлы (с #include "foo.cpp" в вашем заголовке), вы можете изменить настройки makefile / project и добавить foo.cpp в список исходных файлов для компиляции. Таким образом, если вы измените какую-либо реализацию функции, вам не нужно будет перекомпилировать весь проект, и только foo.cpp будет перекомпилирован. Как я упоминал ранее, оптимизирующий компилятор будет по-прежнему выделять ваши маленькие функции, и вам не нужно об этом беспокоиться.


Если вы используете / проектируете предварительно скомпилированную библиотеку, вы должны рассмотреть случай, когда библиотека компилируется с другой версией компилятора для основного проекта. В каждой версии компилятора (даже в разных конфигурациях, таких как Debug или Release) используется разное время выполнения C (например, memcpy, printf, fopen, …) и время выполнения стандартной библиотеки C ++ (например, std :: vector).<>, std :: string, …). Эти различные реализации библиотек могут усложнять компоновку или даже создавать ошибки во время выполнения.

Как правило, всегда избегайте совместного использования объектов времени выполнения компилятора (структуры данных, которые не определены стандартами, такие как FILE *) между библиотеками, потому что несовместимые структуры данных приведут к ошибкам времени выполнения.

При связывании вашего проекта функции времени выполнения C / C ++ должны быть связаны с вашей библиотекой .lib или .lib / .dll или исполняемым файлом .exe. Сама среда выполнения C / C ++ может быть связана как статическая или динамическая библиотека (вы можете установить это в настройках makefile / project).

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

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

2

обоснование

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

  1. универсальные шаблоны для определяемого пользователем параметра шаблона;

  2. очень короткие удобные функции, когда встраивание дает значительный
    спектакль.

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

Заключение

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

2

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

Я знаком с компилятором Microsoft, который точно знает, что делает это с Visual Studio 2010 (если не раньше).

1

Шаблонный код обязательно будет иметь только заголовок: для создания экземпляра этого кода параметры типа должны быть известны во время компиляции. Нет способа встроить код шаблона в общие библиотеки. Только .NET и Java поддерживают создание экземпляров JIT из байт-кода.

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

Чтобы избежать «безумного времени компиляции», Microsoft Visual C ++ имеет функцию «предварительно скомпилированных заголовков». Я не думаю, что GCC имеет аналогичную функцию.

Длинные функции не должны быть встроены в любом случае.

У меня был один проект, в котором были биты только для заголовков, биты скомпилированной библиотеки и некоторые биты, которые я не мог решить, где они находятся. В итоге у меня были файлы .inc, условно включенные в .hpp или .cxx в зависимости от #ifdef. По правде говоря, проект всегда компилировался в режиме «max inline», поэтому через некоторое время я избавился от файлов .inc и просто переместил содержимое в файлы .hpp.

1

Можно ли получить лучшее из обоих миров?

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

Я недавно начал думать, что такой дизайн не очень хорош.

Так и должно быть. Библиотеки только для заголовков идеальны, потому что они упрощают развертывание: делает механизм повторного использования языка похожим почти на все остальные, что просто разумно делать. Но это C ++. Текущие инструменты C ++ по-прежнему полагаются на модели полувековой давности, которые устраняют важные степени гибкости, такие как выбор точек входа для импорта или экспорта на отдельном уровне. без быть вынужденным изменить исходный код библиотеки. Кроме того, C ++ не имеет надлежащей системы модулей и все еще полагается на прославленные операции копирования-вставки (хотя это лишь побочный фактор рассматриваемой проблемы).

На самом деле MSVC немного лучше в этом отношении. Это единственная крупная реализация, пытающаяся достичь некоторой степени модульности в C ++ (например, попытка Модули C ++). И это единственный компилятор, который фактически позволяет, например, следующие:

//// Module.c++
#pragma once
inline void Func() { /* ... */ }

//// Program1.c++
#include <Module.c++>
// Inlines or "vague" links Func(), whatever is better.
int main() { Func(); }

//// Program2.c++
// This forces Func() to be imported.
// The declaration must come *BEFORE* the definition.
__declspec(dllimport) __declspec(noinline) void Func();
#include <Module.c++>
int main() { Func(); }

//// Program3.c++
// This forces Func() to be exported.
__declspec(dllexport) __declspec(noinline) void Func();
#include <Module.c++>

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

GCC также принимает это (но порядок объявлений должен быть изменен), и у Clang нет никакого способа достичь того же эффекта без изменения источника библиотеки.

0
По вопросам рекламы [email protected]