【问题标题】:how to wrap a c library with opaque pointers in c++ binding如何在 C++ 绑定中使用不透明指针包装 C 库
【发布时间】:2012-07-20 18:57:46
【问题描述】:

我正在考虑用 c++ 包装一些 c 库,但我不确定包装不透明指针的最佳方法是什么。

当 C 结构是公共 API 的一部分时

typedef struct _SomeType
{
    int a;
    int b;
} SomeType_t;

其中有几个“成员”函数:

void SomeTypeFoo( SomeType_t* obj, ... );
void SomeTypeBar( SomeType_t* obj, ... );

我喜欢从基类派生以简单地将这些“成员”函数关联为实际类成员的方法。即:

class SomeTypeWrapper:
    public SomeType_t
{
    void foo( ... );
    void bar( ... );
};

就我的理解而言,我相信SomeTypeWrapperstruct _SomeType 是“二进制兼容的”,因此SomeTypeWrapper* 可以转换为SomeType_t*,这样c++ 方法的实现可能是:

SomeTypeWrapper::foo( ... )
{
    SomeTypeFoo( (SomeType_t*)this, ... );
}

见注[1]。

但是,在某些库中,他们喜欢将结构的定义保持为私有(我想这是为了允许在不更改标头/API 的情况下更改实现)。所以在标题中我有这样的东西:

typedef struct _SomeType SomeType_t;

然后所有的“成员”函数都处理这些不透明的指针。为了将这些方法“包装”到一个接口中,到目前为止我所做的是提供一个包含指针的类。

class SomeTypeWrapper
{
    private:
        SomeType_t* m_data;

    public:
        SomeTypeWrapper( SomeType_t* data ): m_data(data){}
        void foo(...);
        void bar(...);
};

这意味着对于 c 库中返回这种不透明指针的任何函数,我必须分配一个新对象来携带该指针。对于许多可能很好但这种设计使包装器实现复杂化的库以及执行大量小对象分配的快速 c 库,我怀疑这种额外的对象开销会导致显着的性能损失。此外,底层对象可能是引用计数的,所以在某些情况下,我必须决定 SomeWrapperType 是否增加引用计数、实现复制构造函数、具有私有构造函数等。

在大多数情况下,我很乐意为更好的界面付出代价,但我正在寻找其他选择。有没有更简洁的方法来处理 c++ 包装库中的不透明 c 指针?相关:是否有一种实现包装类的好方法,以便 c++ 标头不必包含 c 库标头并且不需要包装器对象分配?

注意[1]:我相信只要使用相同的编译器并且SomeTypeWrapper 没有添加额外的成员并且没有虚拟方法,这种强制转换是安全的。通过相同的编译器,我假设由 gcc 编译的 c 库将与由相同 gcc 编译的 c++ 包装器一起使用。也许这不能保证?

【问题讨论】:

  • 如果操作正确,C++ 接口将不会与任何SomeType_t 公开交互。

标签: c++ c


【解决方案1】:

SomeTypeWrapper(在问题中)的最终版本是前进的方向。

这意味着对于 c 库中返回这种不透明指针的任何函数,我必须分配一个新对象来携带该指针。对于许多可能很好但这种设计使包装器实现复杂化的库以及执行大量小对象分配的快速 c 库,我怀疑这种额外的对象开销会导致显着的性能损失。

没有开销。具有一个数据成员且没有 virtual 方法的 class 的实例不大于其唯一成员。如果您在标头中内联定义方法,则也不会有函数调用开销。所以,只要你不使用new分配这些实例,而是在堆栈上:

SomeTypeWrapper some_object(make_some_object());  // one allocation on the heap

那么你就有了一个零开销的解决方案。

【讨论】:

  • 在需要时使用“新”并没有错。基于堆栈的对象将在其堆栈帧超出范围时被释放,这并不总是可取的。
  • @RemyLebeau:关键是它会导致双重间接并在new 失败时打开内存泄漏的可能性。当对象必须在其范围内生存时,它应该获得一个复制或移动构造函数。 (不过,使用指向底层 C 对象的智能指针是一个更好的主意。)
  • 你说服了我。我知道复制对象应该与复制原始指针一样昂贵,因此您可以免费获得漂亮的界面。我继续沿着这条路走,到目前为止还没有遇到太多问题……除了将这些对象视为指针的奇怪之处。我正在考虑将类命名为“SomeTypePtr”,以明确它们只是在指针周围随机播放并附有方法。
  • @cheshirekow:这泄露了实现细节。我从不调用 C 类包装器*Ptr;我希望对指针的成员访问需要->。如果您想要类似指针的访问,请更喜欢带有自定义析构函数的智能指针。 (哦,顺便说一句,如果可能的话,让这些对象不可复制。这样可以省去很多麻烦。)
  • @cheshirekow:如果您有引用计数,那么就没有问题。但在您添加 operator* 和朋友之前,它们不是 C++ 意义上的指针。我相信在 Stroustrup 自己的教科书中讨论了 FILE* 的包装器,适当地称为 File,而不是 FilePtr
【解决方案2】:

如果你包装你的不透明指针,你肯定不能派生它。一种看似有吸引力的免费获得某些功能的方法,但当您使用旨在聚合的继承时,它很快就会变得混乱:您的包装器不是库的对象之一,相反,它... 包装一个库对象

如果您像对待库对象一样小心对待包装器对象,那么每个不透明指针总是有一个包装器(最多)。您可以与例如共享您的包装器对象unique_ptr(C++11 有它们!)。这是一件好事,考虑到包装的对象是一种宝贵的资源。

说到共享指针,它们可以将deleter 函数作为构造函数参数:

struct Wrapper : boost::non_copyable 
{
private:
   std::unique_ptr<SomeType> wrapped;
public:
   Wrapper()
   :wrapped(SomeType_create(), &SomeType_destroy)
   // upon destruction of wrapped, use SomeType_destroy
   {
   }

   void foo() { SomeType_foo(wrapped.get()); }
   int bar(int i) { return SomeType_bar(wrapped.get(),i); }
};

是的:SomeType 中的每个成员函数都需要一个方法包装器;这是完全正常的,因为 C++ 使用与 C 不同的调用约定,并且您必须用 C++ 替换每个 C 样式的成员函数。

您必须注意为每个库对象创建一个包装器;此对象在您的控制之下,您可以与您信任的任何人共享它,但它只是一个对象。

【讨论】:

  • unique_ptr 很好,但我发现更多时候不是用户代码没有获得对象的所有权(因此没有销毁权限)。所以我不确定这在这里特别有用。此外,当您说“您的包装器不是库的对象之一,相反,它......包装了一个库对象。”我不一定认为这是理所当然的......但我确实看到它可能是一种更好的哲学。
【解决方案3】:

通常,您在上面提出的建议是一种丑陋的 hack,可能会导致各种麻烦。向上(或向下,当它有意义时)转换继承层次结构是可以的,但是当从指向包装类的指针转换为指向第一个成员指向的对象的指针时,它是未定义的)。绝对不能保证第一个成员的地址将与您的包装器地址对齐。

在我看来,你在这里试图解决一个错误的问题。也许以不正确的策略为每种小型 C“对象”创建 C++ 包装器?为什么不拥有一个包装底层 C 库的功能的库,而不是包装单个对象?当它这样做时,它以一种有意义的、简约的方式管理底层 C 对象(即通过使用 RIAA、共享指针等)

【讨论】:

  • 是的,这可能是一个丑陋的黑客。作为一种学术笔记,使用 reinterpret_cast 怎么样?不保证转换 SomeType_t* --&gt; SomeTypeWrapper* --&gt; SomeType_t* 会产生原始指针吗?
猜你喜欢
  • 2011-02-12
  • 1970-01-01
  • 1970-01-01
  • 2011-07-15
  • 1970-01-01
  • 2011-01-28
  • 1970-01-01
  • 2013-07-12
相关资源
最近更新 更多