Лучший способ использования непрозрачного указателя для Pimpl

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

Обычный шаблон, который у меня есть, заключается в том, чтобы сделать данные члена поставщика непрозрачными, используя только указатель на некоторый «неизвестный» тип поставщика struct/class/typedef/pod.

// myclass.h
class MyClass 
{
 ...
private:
   VendorThing* vendorData;
};

и реализация (примечание: каждая реализация зависит от поставщика; у всех один и тот же файл *.h)

// myclass_for_vendor_X.cpp
#include "vendor.h"
... {
   vendorData->doSomething();
or
   VendorAPICall(vendorData,...);
or whatever

У меня проблема в том, что VendorThing может быть много разных вещей. Это может быть класс, структура, тип или модуль. Я не знаю, и я не хочу заботиться о заголовочном файле. Но если вы выберете не тот, он не скомпилируется, если заголовочный файл поставщика будет включен вместе с моим заголовочным файлом. Например, если это фактическое объявление VendorThing в "vendor.h":

typedef struct { int a; int b; } VendorThing;

Тогда вы не можете просто объявить VendorThing как class VendorThing;. Меня вообще не волнует тип VendorThing, я хочу, чтобы публичный интерфейс воспринимал его как void * (т.е. выделял место для указателя и все), а реализация думала об этом, используя правильный тип указателя.

Два решения, с которыми я столкнулся, - это метод "d-pointer", найденный в Qt, где вы добавляете уровень косвенности, заменяя VendorThing новой структурой VendorThingWrapper.

// myclass.h
struct VendorThingWrapper;
class MyClass 
{
 ...
private:
   VendorThingWrapper* vendorDataWrapper;
};

и в вашем файле cpp

// myclass.cpp
#include "vendor.h"
struct VendorThingWrapper {
   VendorThing* vendorData;
};

... {
   vendorDataWrapper->vendorData->doSomething();
}

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

Другое дело, просто объявить его недействительным

// myclass.h
class MyClass 
{
 ...
private:
   void* vendorDataUntyped;
};

и в реализации

//myclass.cpp
#include "vendor.h"
#define vendorData ((VendorThing*)vendorDataUntyped)

 ... {
   vendorData->doSomething();
}

но #define всегда оставляет неприятный привкус во рту. Должно быть что-то лучше.


person Mark Lakata    schedule 27.07.2014    source источник
comment
Рассматривали ли вы вместо этого использование абстрактного базового класса с различными реализациями? При желании можно реализовать фабричную функцию, поэтому только ее код должен знать фактический тип.   -  person Ulrich Eckhardt    schedule 27.07.2014
comment
Если VendorThing определено в заголовочном файле, то это не идиома pImpl... в pImpl она указывает на класс, который существует только в одном файле .cpp. Это просто сдерживание VendorThing. Ваше первое предложение - это pImpl, и я думаю, что вы преувеличиваете проблемы двух уровней косвенности. Как насчет того, чтобы VendorData имел тип VendorThing (не указатель)?   -  person M.M    schedule 27.07.2014
comment
@UlrichEckhardt - в любой момент времени во время компиляции будет реализован код только одного поставщика. Наличие виртуальных функций просто добавляет еще один уровень разыменования указателя.   -  person Mark Lakata    schedule 27.07.2014
comment
@MattMcNabb - у меня нет контроля над VendorThing - это сторонняя библиотека. Я пытаюсь сделать мой интерфейс pimpl, чтобы он скрывал сторонние реализации.   -  person Mark Lakata    schedule 27.07.2014
comment
@MarkLakata У тебя есть контроль над vendorData   -  person M.M    schedule 27.07.2014
comment
Избегание виртуальных функций действительно является причиной, но измеряли ли вы их влияние на производительность, особенно по сравнению с непрямым вызовом PIMPL? Во всяком случае, вам нужно что-то в вашем (только/базовом) классе, который может хранить любой объект поставщика. В некоторой степени Boost.Any может это сделать, хотя я считаю, что для этого требуется возможность копирования содержащихся объектов. Если это не поможет, вы все равно можете пойти по пути создания массива символов в базе, а затем на месте создать объект поставщика в конструкторе поверх него.   -  person Ulrich Eckhardt    schedule 27.07.2014
comment
Все это направлено на прошивку, где учитывается каждый цикл (особенно в обработчике прерываний) и каждая ячейка памяти. У меня может быть 8 КБ пространства для кода и 4 КБ ОЗУ. По этой причине большинство людей не пишут прошивки на C++. Я пробую это.   -  person Mark Lakata    schedule 27.07.2014
comment
@MattMcNabb - Если я сделаю VendorThing vendorData; частной функцией-членом, то мне нужно поместить #include "vendor.h" в myclass.h, что противоречит всей цели сделать мой заголовок независимым от поставщика.   -  person Mark Lakata    schedule 27.07.2014
comment
@MarkLakata Вы до сих пор не упомянули, что VendorThing была функцией. Но это в любом случае не имеет значения, вам не нужно #include "vendor.h" в myclass.h. myclass.h делает #include "myclass.cpp", а myclass.cpp делает #include "vendor.h". Имя VendorThing должно появляться только в пределах myclass.cpp .   -  person M.M    schedule 27.07.2014
comment
@MattMcNabb - извините за путаницу. Вы сказали VendorThing, но имели в виду второе упоминание этого слова, а не первое. Да, теперь я понимаю. Ваше предложение совпадает с ответом jxh ниже.   -  person Mark Lakata    schedule 27.07.2014
comment
Ах, так оно и есть. Извините за путаницу   -  person M.M    schedule 27.07.2014


Ответы (2)


Вы можете избежать дополнительного разыменования указателя, используя:

#include "vendor.h"

struct VendorThingWrapper : public VendorThing {};

Конечно, в этот момент имеет смысл использовать имя MyClassData вместо VendorThingWrapper.

МойКласс.h:

struct MyClassData;

class MyClass
{
   public:

      MyClass();
      ~MyClass();

   private:
      MyClassData* myClassData;
};

MyClass.cpp:

struct MyClassData : public VendorThing {};

MyClass::MyClass() : myClassData(new MyClassData())
{
}

MyClass::~MyClass()
{
   delete myClassData;
}

Обновить

Мне удалось скомпилировать и построить следующую программу. Безымянный struct не проблема.

struct MyClassData;

class MyClass
{
   public:

      MyClass();
      ~MyClass();

   private:
      MyClassData* myClassData;
};


typedef struct { int a; int b; } VendorThing;

struct MyClassData : public VendorThing
{
};

MyClass::MyClass() : myClassData(new MyClassData())
{
   myClassData->a = 10;
   myClassData->b = 20;
}

MyClass::~MyClass()
{
   delete myClassData;
}

int main() {}
person R Sahu    schedule 27.07.2014
comment
Я почти уверен, что это работает только в том случае, если VendorThing является классом C++. В моем конкретном случае это typedef для безымянной структуры (т.е. C). - person Mark Lakata; 27.07.2014
comment
@MarkLakata, это не проблема. Взгляните на мое обновление. - person R Sahu; 27.07.2014
comment
это выглядит лучше. Это действительно работает. Единственное, что сейчас нужно сделать, это выяснить, как автоматически выполнять понижение, чтобы я мог сделать что-то вроде myClassData = new VendorThing(), например. Я могу static_cast это, но я думаю, что это UB. (см. stackoverflow.com/questions/24978194/) - person Mark Lakata; 27.07.2014
comment
@MarkLakata, но в этом нет необходимости. У вас есть доступ ко всем данным участников VendorThing от myClassData. Мало того, вы можете добавить дополнительные данные в MyClassData, если есть необходимость. - person R Sahu; 27.07.2014
comment
Но только если у меня будет свобода построить myClassData на своих условиях. Рассматриваемое значение указателя является фиксированной ячейкой памяти. В моем конкретном случае vendor.h дает мне #define SPI1 ((SPI_TypeDef *) 0xE3EE0000), и мне нужно назначить его myClassData. - person Mark Lakata; 27.07.2014
comment
@MarkLakata, у вас есть полный контроль над тем, как вы хотите инициализировать myClassData и что с ним делать в деструкторе. - person R Sahu; 27.07.2014
comment
Давайте продолжим это обсуждение в чате. - person Mark Lakata; 27.07.2014

Если вы готовы пойти по пути VendorThingWrapper, то вы просто позволяете оболочке содержать сами данные, а не указатель на них. Это дает вам уровень абстракции и позволяет избежать дополнительного разыменования.

// myclass.cpp
#include "vendor.h"
struct VendorThingWrapper {
   VendorThing vendorData;
};

... {
   vendorDataWrapper->vendorData.doSomething();
}
person jxh    schedule 27.07.2014
comment
Я думал об этом. Проблема становится немного запутанной, когда вы хотите обращаться с vendorDataWrapper как с указателем, например, присвоив ему значение. Затем нужно сделать слепок. Например, если API поставщика дает вам указатель на VendorThing, вам придется сделать vendorDataWrapper = reinterpret_cast<VendorThingWrapper*>(ptr); Выполнить, но все равно оставить меня грязным. - person Mark Lakata; 27.07.2014
comment
Пока API-интерфейс поставщика возвращает тот же указатель, который вы ему передали, здесь нет проблем. Если API-интерфейс поставщика возвращает другой указатель, вы, вероятно, все равно захотите скопировать содержимое. - person jxh; 27.07.2014