Дилемма проектирования C/C++ API

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

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

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

И теперь приходит проблема. После долгого размышления о том, как это сделать, я пришел к выводу, что в C++ буквально нет способа отделить открытый интерфейс от деталей для класса таким образом, чтобы удовлетворить все следующие требования:

  1. Не требует дублирования / избыточности кода. Причина: он не масштабируемый, и хотя для некоторых типов все в порядке, он быстро становится намного больше кода для реалистичных баз кода. Каждая строка в кодовой базе имеет затраты на обслуживание, которые я бы предпочел потратить на значимые строки кода.

  2. Это имеет нулевые накладные расходы. Причина: я не хочу платить за производительность за то, что (или, по крайней мере, должно!) Быть хорошо известным во время компиляции.

  3. Это не хак. Причина: удобочитаемость, ремонтопригодность и потому что это просто безобразно.

Насколько я знаю, и в этом мой вопрос, в C++ есть три способа полностью скрыть реализацию класса от его открытого интерфейса.

  1. Виртуальный интерфейс: нарушает требования 1 (дублирование кода) и 2 (накладные расходы).
  2. Пимпл: нарушает требования 1 и 2.
  3. Переинтерпретируйте приведение указателя 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 заголовок, вы просто поместите их в том же анонимном пространстве имен в файле реализации, принимая ссылку на объект класса.

Другие вопросы по тегам