Подтвердить что ты не робот

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

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

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

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

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

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

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

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


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

4b9b3361

Ответ 1

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

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

  • Используется как библиотека только для заголовков
  • Используется как статическая библиотека
  • Используется как динамическая библиотека (функции импортируются)
  • Встроенная динамическая библиотека (функции экспортируются)

Итак, давайте составим четыре препроцессора для этих случаев: 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 определено, функции объявляются inline и реализация включается как файл .inl.

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

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

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

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

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

Ответ 2

Это конкретная операционная система и компилятор. В Linux с очень недавним GCC компилятором (версия 4.9) вы можете создать статическую библиотеку, используя межпроцедурная оптимизация ссылок.

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

Ответ 3

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

  • общие шаблоны для пользовательского параметра шаблона;

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

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

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

Ответ 4

Это дополнение к ответу @Horstling.


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

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

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


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

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


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


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

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

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

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

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

Ответ 5

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

Я знаком с компилятором Microsoft, который я точно знаю об этом как о VS10 (если не раньше).

Ответ 6

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

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

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

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

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

Ответ 7

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

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

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

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

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

//// 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 не имеет никакого способа добиться такого же эффекта без изменения источника библиотеки.