Дилемма проектирования C/C++ API
Я анализировал проблему разработки API в C++ и как обойти большую дыру в языке, когда дело доходит до отделения интерфейсов от реализаций.
Я пурист и твердо верю в аккуратное отделение открытого интерфейса системы от любой информации о ее реализации. Я ежедневно работаю над огромной кодовой базой, которую не только очень медленно построить, в основном из-за того, что заголовочные файлы тянут большое количество других заголовочных файлов, но и чрезвычайно трудно копать как клиент, что-то делает, так как интерфейс содержит все виды функции для общественного, внутреннего и частного использования.
Моя библиотека разбита на несколько слоев, каждый из которых использует несколько других. Выбор дизайна заключается в том, чтобы предоставлять клиенту каждый уровень, чтобы они могли расширять возможности высокоуровневых сущностей, используя низкоуровневые сущности без необходимости раскручивать мой репозиторий.
И теперь приходит проблема. После долгого размышления о том, как это сделать, я пришел к выводу, что в C++ буквально нет способа отделить открытый интерфейс от деталей для класса таким образом, чтобы удовлетворить все следующие требования:
Не требует дублирования / избыточности кода. Причина: он не масштабируемый, и хотя для некоторых типов все в порядке, он быстро становится намного больше кода для реалистичных баз кода. Каждая строка в кодовой базе имеет затраты на обслуживание, которые я бы предпочел потратить на значимые строки кода.
Это имеет нулевые накладные расходы. Причина: я не хочу платить за производительность за то, что (или, по крайней мере, должно!) Быть хорошо известным во время компиляции.
Это не хак. Причина: удобочитаемость, ремонтопригодность и потому что это просто безобразно.
Насколько я знаю, и в этом мой вопрос, в C++ есть три способа полностью скрыть реализацию класса от его открытого интерфейса.
- Виртуальный интерфейс: нарушает требования 1 (дублирование кода) и 2 (накладные расходы).
- Пимпл: нарушает требования 1 и 2.
- Переинтерпретируйте приведение указателя this к фактическому классу в.cpp. Ноль накладных расходов, но вводит некоторое дублирование кода и нарушает (3).
С здесь побеждает. Определение непрозрачного дескриптора для вашей сущности и набора функций, которые принимают этот дескриптор в качестве первого аргумента, прекрасно удовлетворяет всем требованиям, но это не идиоматический C++. Я знаю, что можно сказать "просто используйте C-стиль при написании C++", но это не отвечает на вопрос, поскольку мы говорим об идиоматическом решении C++ для этого.
1 ответ
Определение непрозрачного дескриптора для вашей сущности и набора функций, которые принимают этот дескриптор в качестве первого аргумента, прекрасно удовлетворяет всем требованиям, но это не идиоматический C++.
Вы все еще можете инкапсулировать это в классе. Непрозрачный дескриптор будет единственным закрытым элементом данных класса, его реализация не будет публично предоставлена. С точки зрения реализации, это будет просто указатель на частную структуру данных, разыменованную функциями-членами класса. Это все еще небольшое улучшение по сравнению с решением C, поскольку все связанные данные и функции будут инкапсулированы в одном классе, и клиенту не нужно отслеживать дескриптор и передавать его каждой функции.
Да, я полагаю, что разыменование указателя приводит к некоторой тривиальной сумме накладных расходов, но решение C будет иметь такую же проблему.
Никакого дублирования кода не требуется, и хотя его можно считать хаком (или, по крайней мере, неэлегичным дизайном C++), это, безусловно, не более чем хак, чем тот же подход, реализованный в C. Единственное отличие заключается в том, что у программистов на C меньше порог для того, что такое "взлом", потому что их язык имеет меньше способов выразить дизайн.
Грубый набросок дизайна, о котором я думаю (в основном такой же, как PIMPL, но только с непрозрачными элементами данных):
// In a header file:
class DrawingPen
{
public:
DrawingPen(...); // ctor
~DrawingPen(); // dtor
void SetThickness(int thickness);
// ...and other member functions
private:
void *pPen; // opaque handle to private data
};
// In an implementation file:
namespace {
struct DrawingPenData
{
int thickness;
int red;
int green;
int blue;
// ... whatever else you need to describe the object or track its state
};
}
// Definitions of the ctor, dtor, member functions, etc.
// For instance:
void DrawingPen::SetThickness(int thickness)
{
// Get the object data through the handle.
DrawingPenData *pData = reinterpret_cast<DrawingPenData*>(this->pPen);
// Update the thickness.
pData->thickness = thickness;
}
Если вам нужны частные функции, которые работают на DrawingPen
, но это вы не хотите выставлять в DrawingPen
заголовок, вы просто поместите их в том же анонимном пространстве имен в файле реализации, принимая ссылку на объект класса.