更好的使用不透明指针实现Pimpl

4
我正在为嵌入式系统(固件级别)中的许多不同硬件库编写一个C++包装库,使用来自不同供应商(C或C ++)的各种库。头文件公开的API应该与供应商无关... 所有供应商标头库都未包含在任何我的标头文件中。
我通常采用的一种常见模式是通过仅使用指向某个“未知”供应商结构/类/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可能是多种不同的东西。它可以是一个类、结构体、类型或者轻量级对象。我不知道,也不想在头文件中关心这个。但是如果你选择了错误的类型,当供应商头文件和我的头文件一起被包含时,它就不能编译通过。例如,如果这是"vendor.h"VendorThing的实际声明:

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

那么你不能仅仅将VendorThing声明为class VendorThing;。我不在乎VendorThing的类型是什么,我只需要公共接口认为它是void *(即分配指针空间就可以了),而实现使用正确的指针类型。

我遇到的两个解决方案是Qt中发现的“d指针”方法,其中通过替换VendorThing为新的结构体VendorThingWrapper来添加一层间接性。

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

在你的cpp文件中。
// myclass.cpp
#include "vendor.h"
struct VendorThingWrapper {
   VendorThing* vendorData;
};

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

但这会增加第二个指针的解引用,虽然这不是什么大问题,但由于这是针对嵌入式系统的,我不想因为语言不能实现我想要的而增加这种开销。

另一件事就是将其声明为void。

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

在实现过程中,
//myclass.cpp
#include "vendor.h"
#define vendorData ((VendorThing*)vendorDataUntyped)

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

但是#define总是让我感到不舒服。一定有更好的方法。


你是否考虑过使用带有不同实现的抽象基类?可以选择性地实现一个工厂函数,这样只有它的代码需要知道实际类型。 - Ulrich Eckhardt
1
如果VendorThing在头文件中定义,那么这不是pImpl惯用语...在pImpl中,它指向一个仅存在于一个.cpp文件中的类。这只是对VendorThing的包含。您的第一个建议是pImpl,我认为您夸大了两个间接级别的“问题”。将VendorData的类型设置为VendorThing(而不是指针)如何? - M.M
@UlrichEckhardt - 在编译时,一次只会实现一个供应商的代码。使用虚函数只是增加了另一层指针解引用。 - Mark Lakata
@MattMcNabb - 我无法控制VendorThing -它是第三方库。我正在尝试使我的接口pimpl,以便隐藏第三方实现。 - Mark Lakata
这些都是针对固件的,每个周期(特别是在中断处理程序中)和每个内存位置都很重要。我可能有8KB的代码空间和4KB的RAM。大多数人出于这个原因不会用C++编写固件。我正在尝试这个。 - Mark Lakata
显示剩余6条评论
2个回答

1
如果您愿意选择 VendorThingWrapper,那么您只需要允许包装器包含数据本身,而不是指向它的指针。 这将为您提供抽象层并避免额外的解引用。
// myclass.cpp
#include "vendor.h"
struct VendorThingWrapper {
   VendorThing vendorData;
};

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

我想到了这个。当你想要将vendorDataWrapper视为指针时,问题会变得有点混乱,例如给它赋值。然后你就必须进行强制转换。例如,如果供应商API给你一个指向VendorThing的指针,你就必须执行vendorDataWrapper = reinterpret_cast<VendorThingWrapper*>(ptr);。虽然可行,但让我感觉很不舒服。 - Mark Lakata
只要供应商API返回的指针与您传递给它的指针相同,这里就没有问题。如果供应商API返回不同的指针,则您可能仍然想要复制内容。 - jxh

1

您可以通过使用以下方法避免额外的指针解引用:

#include "vendor.h"

struct VendorThingWrapper : public VendorThing {};

当然,此时使用名称 MyClassData 而不是 VendorThingWrapper 更加合理。

MyClass.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() {}

这看起来不错。它实际上可以工作。现在唯一的问题是要弄清楚如何自动向下转换,以便我可以像myClassData = new VendorThing()这样做某些事情。我可以使用static_cast进行转换,但我认为这是未定义的行为(UB)。 (见http://stackoverflow.com/questions/24978194/better-way-of-using-an-opaque-pointer-for-pimpl/24978317#24978317) - Mark Lakata
@MarkLakata,但是没有必要。您可以从myClassData访问VendorThing的所有成员数据。不仅如此,如果需要,您还可以向MyClassData添加其他数据。 - R Sahu
但前提是我有自由构建myClassData的权利。所涉及的指针值是一个固定的内存位置。在我的特定情况下,vendor.h 给了我 #define SPI1 ((SPI_TypeDef *) 0xE3EE0000),我需要将其分配给 myClassData - Mark Lakata
@MarkLakata,您完全可以控制如何初始化myClassData以及在析构函数中对其进行的操作。 - R Sahu
让我们在聊天中继续这个讨论 - Mark Lakata
显示剩余2条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接