Я работаю над библиотекой, которая несколько сложна. Он предлагает кучу DLL_EXPORT
Ed функции, которые используют некоторые пользовательские struct
s. На данный момент эти структуры определены с помощью функций, которые их используют, например:
struct MyDataType
{
std::wstring name;
std::wstring purpose;
}
DLL_EXPORT int DoSomething(MyDataType instruction);
В целях удобства обслуживания я постепенно переключаю эту библиотеку на более стандартный стиль интерфейса:
struct MyLibraryInterface
{
virtual int DoSomething(MyDataType instruction) = 0;
}
Чтобы немного упростить интерфейс, я работаю со сторонней библиотекой. Эта библиотека требует, чтобы я выполнил небольшую работу по настройке для каждого пользовательского типа данных, который я использую, и здесь я не уверен, как поступить. Я вижу несколько разных способов сделать это:
//MyLibraryInterface.h
namespace MyLibraryInterface_v1_Types
{
struct MyDataType
{
std::wstring name;
std::wstring purpose;
}
}
#include "MyLibraryInterface_v1_Types_Backend.private.h" //this header would contain the required "paperwork" to make my datatype work with the 3rd-party lib
struct MyLibraryInterface_v1
{
virtual int DoSomething(MyDataType instruction) = 0;
}
Преимущества:
Тот, кому нужно использовать мою библиотеку, просто должен #include
один заголовок
Пользовательское пространство имен хранит типы данных отдельно от определений функций
Недостатки:
#include
на полпути через заголовок выглядит неряшливо и неуместно (хотя я заметил, что некоторые заголовки MS используют эту технику)
Настраивается ли собственное пространство имен или я просто сбиваю с толку пользователей? (Сама инкапсуляция определенно необходима, так как я мог бы изменить определение этого типа данных в будущем. Но я не знаю, нужно ли мне собственное пространство имен или мне просто нужно поместить объявление типа данных в объявление структуры, которая будет используй это.)
//MyLibraryInterface.h
namespace MyLibraryInterface_v1_Types
{
struct MyDataType
{
std::wstring name;
std::wstring purpose;
}
}
//3rd-party "paperwork" is directly placed here
struct MyLibraryInterface_v1
{
virtual int DoSomething(MyDataType instruction) = 0;
}
Преимущества:
#include
на полпути через мой заголовокНедостатки:
Сторонние документы теперь отображаются в заголовке, который будут использовать мои пользователи (им не нужно это видеть, и я бы предпочел, чтобы их не было, по косметическим причинам или из-за простоты понимания)
Такое ощущение, что я вообще не пользуюсь мощью пространства имен типа данных, так как сторонний код остается свободно плавающим в коде моей библиотеки и не инкапсулируется
//MyLibraryInterface.h
struct MyLibraryInterface_v1
{
struct MyDataType
{
std::wstring name;
std::wstring purpose;
}
virtual int DoSomething(MyDataType instruction) = 0;
}
#include "MyLibraryInterface_v1_Types_Backend.private.h" //this header would contain the required "paperwork" to make my datatype work with the 3rd-party lib
Преимущества:
MyLibraryInterface_v1::MyDataType
вместо MyLibraryInterface_v1_Types::MyDataType
, что является более интуитивным, если они вызывают функцию в MyLibraryInterface_v1
тем не мениеНедостатки:
#include
в самом низу заголовка выглядит действительно плохой
Смешивание типов данных и объявлений функций кажется мне немного сомнительным
//MyLibraryInterface_v1_Types.h
namespace MyLibraryInterface_v1_Types
{
struct MyDataType
{
std::wstring name;
std::wstring purpose;
}
}
//3rd-party paperwork can be directly placed here, immediately following the definition of the custom datatype//MyLibraryInterface.h
#include "MyLibraryInterface_v1_Types.h" /* this header, as defined above, holds the definitions of the custom datatypes this library will use. It also includes the 3rd-party paperwork required to make those datatypes work. It can't be a private header, though, because users will need to access it to use the custom types. */
struct MyLibraryInterface_v1
{
virtual int DoSomething(MyDataType instruction) = 0;
}
Преимущества:
Недостатки:
Пользователям может быть трудно найти или использовать пользовательские типы данных
Кажется довольно не интуитивным, так как типы данных находятся как в отдельном заголовке, так и в отдельном пространстве имен
Так что лучше? Я пропускаю другой, лучший, метод полностью? Или мне просто придется прикусить пулю и принять это, независимо от того, каким образом я решу пойти с этим, у меня будут некоторые проблемы.
Обновите немного больше информации:
Сторонняя библиотека, которую я использую, оборачивает мой интерфейс в struct
для меня. Так что я смогу создать объект MyLibraryInterface*
сторонняя библиотека позволит мне получить доступ к реализации этого интерфейса из указанной библиотеки DLL, а затем я могу вызвать MyLibraryObj->DoSomething()
, По сути, это вариант pImpl.
Эта сторонняя библиотека также автоматически упаковывает любые типы STL и любые пользовательские типы данных, чтобы их можно было использовать в нескольких компиляторах, поэтому мой std::wstring
использование здесь абсолютно безопасно. Однако библиотека требует, чтобы я предоставил определенную информацию для установки как обернуть пользовательские типы. Я должен предоставить эту информацию о настройке где-то после того, как каждый пользовательский тип определен, что исключает «нормальный» шаблон размещения #include
с частной информацией о настройке вверху моего заголовка интерфейса. Я также не могу полностью удалить частную информацию о настройках из заголовка интерфейса; Любой, кто вызывает мою библиотеку через этот интерфейс, должен будет использовать стороннюю библиотеку для этого, и он должен будет снова предоставить объявление интерфейса, чтобы библиотека знала, что она ищет в данной DLL. Все, что я могу сделать, — это попытаться сделать работу частной установки как можно более аккуратной и ненавязчивой, а в идеале пометить ее как нечто, что пользователям моей библиотеки никогда не понадобится или не захочет работать напрямую.
Кроме того, у меня есть возможность поместить свои пользовательские типы данных в интерфейс struct
или в свои namespace
, Я играл с ними прямо в struct
сначала, но так как некоторые из этих типов данных являются постоянными данными (enum class
es) казалось немного неаккуратным, чтобы поместить их в struct
с объявлениями функций. namespace
«чувствуется» чище, но с другой стороны, функции и типы данных будут обрабатываться по-разному (myLibraryObj->DoSomething()
против MyLibraryInterface_v1_Types::MyDataType
) и, следовательно, может быть менее интуитивным, чем держать все в struct
(myLibraryObj->DoSomething()
, MyLibraryInterface_v1::MyDataType
).
Если вы создаете библиотеку для использования другими, всегда помещайте ее в пространство имен. Сделать это достаточно долго, чтобы быть полностью описательным. Если кто-то хочет использовать более короткое имя в квалифицированных именах, он может определить псевдоним пространства имен для своего собственного использования. Кроме того, вам не нужно беспокоиться о том, как выглядит заголовок внутри. Если вы хорошо справляетесь с документацией, никому не нужно смотреть на заголовок.
Пространства имен могут быть вложенными, и вы можете использовать это, чтобы скрыть (но не скрыть) детали реализации. Одно из часто используемых соглашений — называть такое пространство имен. detail
, Напишите документацию, которая указывает, что это пространство имен не предназначено для общего пользования и содержит сведения, которые могут быть изменены.
Напомним, что #include
это чисто текстовый механизм, просто заменяющий текстовый блок на директиву. Таким образом, если вы включите внешний заголовок внутри detail
пространство имен, оно не появится ни в глобальном пространстве имен, ни в пространстве имен библиотеки верхнего уровня. Включая внешние определения таким образом, вы можете явно предоставлять только то, что вам нужно, из внешнего заголовка; все остальное экранировано внутри detail
иначе.
Пример ниже иллюстрирует эти принципы. Вы можете приостановить неверие и предположить, что external_library
определяется внутри некоторого внешнего заголовочного файла. Этот пример иллюстрирует каждый из принципов, изложенных выше. Я предполагаю, что вам нужна внешняя библиотека как часть определения некоторых ваших типов; если нет, то его вообще не должно быть в шапке.
namespace library {
namespace detail {
#include <whatever>
namespace external_library {
class exposed {} ;
class hidden {} ;
}
}
typedef detail::external_library::exposed external_type ;
class my_type {} ;
}
library::my_type foo ;
library::external_type bar ;
Я не рассматривал вопросы о внешних связях, которые вы затронули, потому что они отделены от проблем с областями охвата, которые являются центральными для вашего вопроса.
Я не думаю, что у вас должно быть отдельное пространство имен для хранения типов вашей библиотеки. Как пользователь, мне нравится, что при использовании библиотеки NiftyLibrary все ее сущности (то есть типы, функции и тому подобное) содержатся в пространстве имен с именем Nifty
или что-то типа того. Конечно, это пространство имен может, в свою очередь, содержать другие пространства имен, если библиотека слишком большая, но вы поняли идею. Я нахожу странным ссылаться на пространство имен NiftyTypes
при использовании одного из типов NiftyLibrary и Nifty
иначе. Может случиться так, что идентификатор типа сталкивается с идентификатором функции, но тогда вы делаете что-то в корне неправильно.
Это абсолютно необходимо предоставить подробности реализации, такие как сторонняя библиотека, которую вы упоминаете своим конечным пользователям? Вы должны принять во внимание, что, когда они #include
ваши заголовки, они также будут #include
В таком случае производительность компиляции будет снижаться, и, самое главное, некоторые идентификаторы, используемые пользователями, могут конфликтовать с идентификаторами сторонних библиотек. Альтернативой было бы объявить все последние внутри detail
пространство имен, как это делают библиотеки Boost. Тем не менее, не самый чистый выбор на мой взгляд. Разве вы не можете переместить настройку, связанную с вашими пользовательскими типами данных, из заголовков? Это может быть невозможно, если нужно что-то сделать с типом, но дать ему шанс. (Вы слышали о Pimpl идиома?)
Я бы применил следующие правила для развития библиотеки: