Ответ 1
Стандарт (из рабочего проекта С++ 0x через рабочий проект в 2014 году), раздел 14.7.2 описывает Явное создание экземпляра, в котором указано, что существуют две формы явного инстанцирования, определения и объявления. В нем говорится: "Явная декларация о создании начинается с ключевого слова extern". Далее он указывает, что объявления, использующие extern, не генерируют код.
Необходимо проявлять осторожность, чтобы декларации выдавались в пространстве имен объявления класса шаблона или, в частности, ссылались на пространство имен в квалифицированном имени, например:
namespace N {
template< class T > void f( T& ) {}
}
template void N::f<int>(int &);
Создание экземпляра функции шаблона и генерация кода (определения). Принимая во внимание:
extern template void N::f<int>(int &);
Создает функцию шаблона для типа int в качестве объявления, но не генерирует код. Ключевое слово extern сообщает компилятору, что код будет предоставлен во время ссылки из другого источника (возможно, в динамическую библиотеку, но стандарт не обсуждает концепцию конкретной платформы).
Кроме того, можно создавать элементы-члены и функции-члены выборочно, как в:
namespace N {
template<class T> class Y { void mf() { } };
}
template void N::Y<double>::mf();
Это генерирует код только для функции mf(), для двойников. Таким образом, можно объявлять экземпляры (используя extern), а затем определять экземпляры (без extern) для определенных частей типа шаблона. Можно было бы сгенерировать код или элементы для некоторых частей класса шаблона в каждом модуле компиляции (inline) и заставить генерировать другие части кода в конкретный блок или библиотеку компиляции.
Статья IBM Knowledge Center для компилятора XLC V 11.1, поддерживающая проект С++ 0x, обсуждает стратегию использования ключевого слова extern при создании библиотек. Из их примера и проектов нормативных документов в течение нескольких лет (которые были согласованы с 2008 годом по этому вопросу) было ясно, что extern имеет ограниченную применимость к особенностям динамических библиотек, но в целом ограничивается контролем, где сгенерированный код размещены. Автору все равно придется придерживаться специфических требований к платформе относительно динамической компоновки (и загрузки). Это выходит за рамки ключевого слова extern.
Extern в равной степени применим к статическим библиотекам или динамическим библиотекам, но ограничение на дизайн библиотеки имеет большое значение.
Скажите объявление класса шаблона, представленное в файле заголовка, например:
namespace N
{
template< typename T >
class Y
{
private:
int x;
T v;
public:
void f1( T & );
void f2( T &, int );
};
}
Далее в файле CPP:
namespace N
{
template< typename T> void Y<T>::f1( T & ) { .... }
template< typename T> void Y<T>::f2( T &, int ) { .... }
}
Теперь рассмотрим потенциальное использование Y. Потребителям библиотеки могут потребоваться только экземпляры Y для int, float и double. Все остальные виды использования не будут иметь никакого значения. Это точка дизайна автора библиотеки, а не какое-то общее понятие об этой концепции. По какой-то причине автор поддерживает только те три типа для T.
С этой целью в заголовочный файл могут быть включены явные декларации о создании объектов
extern template class N::Y< int >;
extern template class N::Y< float >;
extern template class N::Y< double >;
Поскольку это обрабатывается пользователем различными единицами компиляции, компилятор информирован о том, что для этих трех типов будет создан код, но код не генерируется в каждом модуле компиляции по мере сборки пользователем. Действительно, если автор не включает файл CPP, определяющий функции f1 и f2 для класса шаблона Y, пользователь не сможет использовать libary.
Предполагая, что на данный момент статическая библиотека является предполагаемым продуктом относительно класса шаблона Y (для упрощения этого обсуждения), автор компилирует статическую библиотеку с определяющими CPP функциями f1 и f2 вместе с явными определениями создания:
template class N::Y< int >;
template class N::Y< float >;
template class N::Y< double >;
Это приведет к созданию кода для класса шаблона Y от имени этих трех типов, создавая статическую библиотеку. Пользовательский код теперь должен будет ссылаться на эту библиотеку, но больше не использовать классы. Их единицы компиляции не будут генерировать код для класса шаблона Y, вместо этого он будет включать этот код из библиотеки.
Такая же концепция применима к динамической библиотеке, но особенности платформы, касающиеся деклараций функций, динамической загрузки и динамической компоновки, не найдены в стандартах рабочих черновиков С++ до 2014, касающихся С++ 0x, С++ 11 или C + +14, в настоящее время. Ключевое слово extern в явных шаблонных экземплярах ограничено созданием деклараций, отсутствие которых создает определения (где генерируется код).
Это вызывает вопрос о том, что пользователи такой библиотеки намерены использовать Y для unsigned long, char или другого типа, не предоставленного в динамической или статической библиотеке. У автора есть выбор отказаться от поддержки этого, не распространяя источник генерации кода (определения функций для f1 и f2 для класса шаблона Y). Однако, если автор действительно хотел бы поддержать такое использование, распространяя этот источник, для пользователя потребуется инструкция для создания новой библиотеки для замены существующей или создания второй библиотеки для дополнительных типов.
В обоих случаях было бы разумно разделить явные определения экземпляров в CPP файле, который включает заголовок, объявляющий шаблонный класс Y, включая заголовок определений funtion для f1 и f2 для класса шаблона Y (в отличие от практика включения файла CPP, который также может работать). Таким образом, пользователь создаст файл CPP, который включает заголовок для класса шаблона Y, затем определения функций для класса шаблона Y, а затем выдаст новые явные определения экземпляров:
#include "ydeclaration.h" // the declaration of template class Y
#include "ydefinition.h" // the definition of template class Y functions (like a CPP)
template class N::Y< unsigned long >;
template class N::Y< char >;
Для статической библиотеки потребуется немного другое, и пользователь может выбрать создание дополнительного модуля компиляции в своем проекте, избавляя от необходимости статической целевой библиотеки.
Однако, если пользователь хотел создать динамическую библиотеку, потребуется помощь в отношении кода конкретной платформы относительно динамических библиотек на конкретной платформе. Например, в Windows, например, это может означать явную загрузку новой динамической библиотеки.
Учитывая сложности, связанные с созданием динамических библиотек, это чудо, которое когда-либо было. Иногда просто нет другого выбора. Ключом к решению является точное определение необходимости использования динамической библиотеки. В эпоху древних эпох компьютеров с 1 ГБ ОЗУ одно из обоснований заключалось в экономии памяти за счет совместного использования кода, но для какой-либо конкретной библиотеки, какова вероятность того, что совместное использование кода приведет к экономии памяти? Для чего-то такого же общего, как среда выполнения C или DLL Windows MFC, это может быть весьма вероятным. С другой стороны, библиотеки, которые предоставляют высоконаправленные сервисы, с большей вероятностью будут использоваться только одной запущенной программой.
Одна действительно хорошая цель - это концепция подключаемого модуля. Браузеры, IDE, программное обеспечение для редактирования фотографий, программное обеспечение для САПР и другие используют преимущества всей индустрии приложений, распространяемых как плагины для существующих продуктов, которые распространяются как динамические библиотеки.
Другим оправданием является распространение обновлений. Хотя это привлекательная теория, практика может вызвать больше проблем, чем это стоит.
Другим общим оправданием является "модульность". С какой целью? Разделение единиц компиляции уже сокращает время компиляции. Динамические библиотеки будут влиять на время ссылки больше, чем время компиляции, но стоит ли это дополнительно?
В противном случае предоставление динамических библиотек, особенно для довольно небольшого продукта, действительно не стоит того.
Вся книга может быть написана на тему написания переносных динамических библиотек, применимых как к Windows, так и к Linux.
В Windows выбор использования __declspec (dllexport/dllimport) может применяться ко всему классу. Однако важно понять, что любой компилятор, используемый для генерации DLL, может использоваться только с целями, построенными с помощью того же самого компилятора или совместимых компиляторов. Внутри линии MS VC многие версии НЕ совместимы друг с другом на этом уровне, поэтому DLL, построенная с одной версией Visual Studio, может быть несовместима с другими версиями, что накладывает нагрузку на автора для создания DLL для всех возможных компиляторов/поддерживаемой версии.
Существуют похожие проблемы в отношении статических библиотек. Клиентский код должен связываться с той же версией и конфигурацией CRT, что и DLL, с помощью (является ли CRT статически связана?). Клиентский код должен также выбирать одни и те же настройки обработки исключений и параметры RTTI по мере создания библиотеки.
Когда переносимость в Linux или UNIX (или Android/iOS) должна быть рассмотрена, проблемы увеличиваются. Динамическое связывание - это концепция платформы, которая не обрабатывается на С++.
Статические библиотеки, вероятно, были бы лучшим подходом, и для тех __declspec (dllexport/dllimport) не следует использовать.
При всем том, что сказано против динамических библиотек, здесь один из многих способов реализовать это в Windows (полностью неприменимый к Linux/UNIX/etc).
Простой (возможно, наивный, но удобный) подход заключается в том, чтобы охватить весь класс, экспортированный из DLL (импортированный в клиентский код). Это имеет небольшое преимущество перед объявлением каждой функции как экспортированной или импортируемой, потому что этот подход включает данные класса, и, что не менее важно, для вашего класса может создаваться код АВТОМАТИЧЕСКОГО назначения/деструктор/конструктор С++. Это может быть жизненно важно, если вы не внимательно следите за ними и экспортируете их вручную.
В заголовок, который должен быть включен для создания DLL:
#define DLL_EXPORT // or something similar, to indicate the DLL is being built
Это будет включено в верхнюю часть заголовка, объявляя классы шаблона библиотеки. Заголовок, объявляющий DLL_EXPORT, используется только в проекте, сконфигурированном для компиляции библиотеки DLL. Весь клиентский код будет импортировать пустую версию в противном случае. (Myriad Другие методы для этого существуют).
Таким образом, DLL_EXPORT определяется при создании библиотеки DLL, не определяемой при создании кода клиента.
В заголовке объявления классов шаблона библиотеки:
#ifdef _WIN32 // any Windows compliant compiler, might use _MSC_VER for VC specific code
#ifdef DLL_EXPORT
#define LIB_DECL __declspec(dllexport)
#else
#define LIB_DECL __declspec(dllimport)
#endif
Или то, что вы предпочитаете видеть вместо LIB_DECL как средство объявления всех классов, экспортируемых из DLL, импортированных в клиентский код.
Выполните объявления классов как:
namespace N
{
template< typename T >
struct LIB_DECL Y
{
int x;
T v;
std::vector< T > VecOfT;
void f1( T & );
void f2( T &, int );
};
}
Явные объявления для создания экземпляров для этого были бы следующими:
extern template struct LIB_DECL N::Y< int >;
extern template struct LIB_DECL N::Y< float >;
extern template struct LIB_DECL N::Y< double >;
extern template class LIB_DECL std::vector< int >;
extern template class LIB_DECL std::vector< float >;
extern template class LIB_DECL std::vector< double >;
Обратите внимание на std::vector, используемый в классе Y в этом примере. Рассмотрим проблему тщательно. Если ваша библиотека DLL использует std::vector (или любой класс STL, это просто пример), то реализация, которую вы использовали в момент создания DLL, должна соответствовать тому, что пользователь выбирает при создании кода клиента. 3 явных экземпляра вектора соответствуют требованиям класса шаблона Y и создают экземпляр std::vector внутри DLL, и это объявление становится экспортируемым из DLL.
Рассмотрим, как бы использовать код DLL USE std::vector. Что будет генерировать код в DLL? Из опыта очевидно, что источник для std::vector является встроенным - это файл только заголовка. Если ваша DLL создает экземпляр векторного кода, как клиентский код сможет получить к нему доступ? Клиентский код "увидит" std::vector источник и попытается создать собственное встроенное генерирование этого кода, в котором будет работать клиент std::vector. Если и только если они будут точно совпадать, это сработает. Любая разница между источником, используемым для сборки библиотеки DLL, и источником, используемым для создания клиента, будет отличаться. Если клиентский код имел доступ к std::vector в классе шаблонов T, то был бы хаос, если бы клиент использовал другую версию или реализацию (или имел разные настройки компилятора) при использовании std::vector.
У вас есть возможность явно генерировать std::vector и информировать клиентский код для использования этого сгенерированного кода, объявляя std::vector как класс extern-шаблона, который должен быть импортирован в код клиента (экспортирован в сборках DLL).
Теперь, в CPP, где построена DLL, - определения функций библиотеки - вы должны явно создавать определения:
template struct LIB_DECL N::Y< int >;
template struct LIB_DECL N::Y< float >;
template struct LIB_DECL N::Y< double >;
template class LIB_DECL std::vector< int >;
template class LIB_DECL std::vector< float >;
template class LIB_DECL std::vector< double >;
В некоторых примерах, таких как MS KB 168958, они предлагают сделать ключевое слово extern define, изменив этот план следующим образом:
#ifdef _WIN32 // any Windows compliant compiler, might use _MSC_VER for VC specific code
#ifdef DLL_EXPORT
#define LIB_DECL __declspec(dllexport)
#define EX_TEMPLATE
#else
#define LIB_DECL __declspec(dllimport)
#define EX_TEMPLATE extern
#endif
Так что в файле заголовка для сборки DLL и клиента вы можете просто указать
EX_TEMPLATE template struct LIB_DECL N::Y< int >;
EX_TEMPLATE template struct LIB_DECL N::Y< float >;
EX_TEMPLATE template struct LIB_DECL N::Y< double >;
EX_TEMPLATE template class LIB_DECL std::vector< int >;
EX_TEMPLATE template class LIB_DECL std::vector< float >;
EX_TEMPLATE template class LIB_DECL std::vector< double >;
В то время как это имеет преимущество, выдавая эти строки один раз, в заголовке, я лично предпочитаю, чтобы явное выражение ключевого слова extern явно использовалось в заголовке, так что я знаю, что, без сомнения, единственное создание кода места может иметь место в CPP сборки DLL (где они появляются, второй раз, без ключевого слова extern). Таким образом, extern в заголовке - это декларации, которые не противоречат явным определениям инкапсуляции в CPP, и избегают обфускации ключевого слова extern при использовании в клиентском коде. Это, пожалуй, своеобразное предпочтение моего.
Возможно, вы думаете: "а как насчет другого кода клиента и std::vector". Ну, это важно учитывать. В файл заголовка входит std::vector, но помните, что ваша DLL построена с кодом, доступным для ВАС во время компиляции. У вашего клиента будет свой собственный заголовок, он в одинаковых версиях VC, который должен быть таким же. СЛЕДУЕТ не очень хороший план. Это может быть иначе. Они могут просто предположить, что VC 2015 такой же и пашет вперед. Любая разница, будь то макет объекта, фактический код... все, может обречь запущенное приложение с очень странными эффектами. Если вы экспортируете свою версию, клиентам будет рекомендовано включать объявления явных манифестаций во всех своих единицах компиляции, поэтому все использует ВАШУ версию std::vector... но есть серьезный улов.
Что, если какая-то другая библиотека сделала это тоже с другой версией std::vector?
Это делает использование STL немного неприятным в этих контекстах, поэтому существует довольно хороший дизайн, который устраняет это. Не подвергайте использование STL.
Если вы все время используете STL в своей библиотеке и никогда не подвергаете контейнер STL клиентскому коду, вы, вероятно, находитесь в понятном виде. Если вы выберете это в дизайне, вам не нужно явно создавать экземпляр std::vector (или любой STL) в вашей библиотеке.
Я включил этот пример, чтобы обсудить его, как он документально подтвержден MS (KB 168958) и почему вы, вероятно, не хотите этого делать. Однако также возникает обратный сценарий.
В исходном запросе следует использовать использование std::string (или один из его альтернатив). Подумайте об этом: в DLL, как будет использоваться экземпляр std::string? Что делать, если есть какая-либо разница между кодом std::string, доступным, когда DLL была построена по сравнению с тем, что используется клиентской сывороткой, которую они создают? В конце концов, клиент мог бы использовать другие STL, чем MS. Конечно, вы могли бы оговаривать, что они этого не делают, но... возможно, вы можете явно создать экземпляр std::string как extern WITHIN вашей DLL. Таким образом, у вас нет кода STL, встроенного в DLL, и теперь компилятор сообщает, что он должен найти этот код, созданный клиентом, а не внутри DLL. Я предлагаю это для исследований и мысли.
Коварная проблема, с которой вы сталкиваетесь, такова: все это будет работать на вашем компьютере в ваших тестах, потому что вы используете один компилятор. Это было бы безупречно, но могло бы эффектно выйти из строя в клиентских сборках из-за различий в кодах или установить различия, достаточно тонкие, чтобы избежать предупреждений.
Итак, допустим, вы согласны и пропустите последние три строки в примерах, которые создают экземпляр std::vector... это сделано?
Это зависит от ваших настроек IDE, которые я вам оставлю. Вопрос касался использования __declspec (dllxxxxx) и его использования, и есть несколько способов реализовать его использование, я сосредоточился на одном. Независимо от того, нужно ли явно загружать библиотеку, полагаться на функции автоматической динамической компоновки, рассмотрите DLL_PATH... это общие темы для создания DLL, которые вы либо знаете, или находятся за пределами реальной сферы вопроса.