在堆栈上分配不完整的类型

Posted

技术标签:

【中文标题】在堆栈上分配不完整的类型【英文标题】:Allocate incomplete type on stack 【发布时间】:2016-07-12 04:44:16 【问题描述】:

我正在用 C 封装一个 C++ 库。C++ 库是一个用于数据库服务器的库。它使用包装类来传递序列化数据。我不能直接在 C 中使用该类,所以我定义了一个可以在 C 代码中使用的结构,如下所示:

include/c-wrapper/c-wrapper.h(这是我的 C 包装器的客户所包含的包装器)

extern "C" 
    typedef struct Hazelcast_Data_t Hazelcast_Data_t;

    Hazelcast_Data_t *stringToData(char *str);
    void freeData(Hazelcast_Data_t *d);

impl.pp

extern "C" struct Hazelcast_Data_t 
    hazelcast::client::serialization::pimpl::Data data; // this is the C++ class
;

Hazelcast_Data_t *stringToData(char *str) 
     Data d = serializer.serialize(str);

     Hazelcast_Data_t *dataStruct = new Hazelcast_Data_t();
     dataStruct->data = d;

     return dataStruct;


...

现在可以了,我的 C 库的客户端只能看到 typedef struct Hazelcast_Data_t Hazelcast_Data_t;。问题是,上述类型无法在堆栈上分配,就像我想提供这样的 API:

// this is what I want to achieve, but Hazelcast_Data_t is an incomplete type
#include <include/c-wrapper/c-wrapper.h>

int main() 
    char *str = "BLA";
    Hazelcast_Data_t d;
    stringToData(str, &d);

编译器会抛出 Hazelcast_Data_t 是不完整类型的错误。我仍然想提供一个 API,允许将堆栈分配的 Hazelcast_Data_t 引用传递给序列化函数,但是因为 Hazelcast_Data_t 有一个指向 C++ 类的指针,这似乎几乎是不可能的。然而,可以选择传递堆栈分配的引用将大大简化我的 C 库客户端的代码(无需释放 newed 结构)。

是否可以重新定义Hazelcast_Data_t 类型以便它可以在C 中使用并且仍然在堆栈上分配?

【问题讨论】:

Hazelcast_Data_t 真的包含指向 C++ 类的指针,还是包含实际的 C++ 结构或类对象?如果它真的只是一个指针,那么你应该不会有很多问题,因为指针的大小即使对于不完整的类型也是众所周知的(即它总是 4 或 8 个字节,取决于你的系统)。您可能必须将指针存储为 (void *) 以使 C 编译器更容易理解,但除此之外我不明白为什么会出现问题。 @JeremyFriesner Hazelcast_Data_t 包含实际数据。我还认为我可以使用 void 指针,但我不能将 Hazelcast_Data_t data 转换为 void * 并且我不能获取它的地址,因为它是一个本地堆栈变量。 投票结束的人:请帮助我理解为什么不清楚我的问题所问的内容。我对 C/C++ 编程还是很陌生,可能使用了错误的术语来描述我的问题。以“不清楚”的理由投票结束对我毫无帮助。 【参考方案1】:

您正在考虑执行此操作的大多数 hack 都会调用未定义的行为,因为 C 在创建结构时不会为包含的对象调用 C++ 构造函数,并且在结构超出范围时不会调用 C++ 析构函数.为了使其工作,您需要该结构包含正确大小的缓冲区,并在 init 函数中将新缓冲区放入该缓冲区,并在完成后调用该缓冲区上的析构函数。这意味着代码看起来像这样(假设没有抛出任何东西 - 在这种情况下,您需要添加异常处理和翻译......)

struct wrapper 
  char buffer[SIZE_OF_CXX_CLASS];


void wrapper_init() 
   new (buffer) Wrapped();


void wrapper_destroy() 
   ((Wrapper*)buffer)->~Wrapper();



  struct wrapper wrapped;
  wrapper_init(&wrapped);
  // ... use it ...
  wrapper_destroy(&wrapped);

如果您忘记致电wrapper_init,一切都会进入未定义的行为领域。如果你忘记给wrapper_destroy打电话,我想你也会得到UB。

但是由于这会迫使您的调用者调用 init 和 destroy 函数,因此使用指针几乎没有什么好处。我什至声称使用结构而不是指针向 API 用户表明初始化应该是微不足道的,并且不需要破坏。 IE。作为 API 用户,我希望能够做到

 
   struct wrapper wrapped = WRAPPER_INIT; //Trivial initialisaton macro
   // .. use it ..
   // No need to do anything it is a trivial object.
 

在不可能的情况下(如你的),我会坚持使用通常的 在堆上分配它 习惯用法


   struct wrapper* wrapped = wrapper_create();
   // ... use it ...
   wrapper_destroy(wrapped);

【讨论】:

我同意你的看法。我只是认为我可以避免许多堆分配,因为该库用于与数据库交互,我可以想象有很多创建/销毁的调用会创建很多小内存分配/释放调用。但最后,我也希望 API 安全且可用。 如果您以后发现这些对象的分配成为瓶颈,您可以通过在wrapper_create 内部使用不同的分配方案(例如使用池)来优化大部分成本。 非常好的洞察力,我同意。我现在将构建简单版本,看看它是否真的会产生问题。非常感谢迈克尔,有时单独从事一个项目会让人很难不卡在某个想法上。【参考方案2】:

您需要在头文件中提供结构的定义,以便客户端知道要在堆栈上分配多少空间。但是当 C++ 类中的底层表示不能被 extern "C" 公开时,这就变得很棘手了。

解决方案是指向 C++ 类而不是实际类的指针。由于指针大小相同,这将在 C 客户端中工作,即使它不了解 C++。

因此在标题中

typedef struct Hazelcast_Data_t 
       void *data
 Hazelcast_Data_t

并且在 C++ 文件中,您可以使用 static_cast 通过此指针访问 C++ 类。

【讨论】:

这与传递指向原始不完整类型的指针没有什么不同。【参考方案3】:

制作一个包装结构,它只包含一个大数组并且对齐到足以包含您的 C++ 类型。在其中放置新的 C++ 类型。

您可能必须构建一个小型 C++ 可执行文件,该可执行文件将生成一个 C 头文件,其中 SIZEOF_HAZELCAST_T 和 ALIGNOF_HAZELCAST_T 已适当定义。

【讨论】:

我不确定这种方法,即使我想这样做,但你能解释一下ou will probably have to build a small C++ executable that would generate a C header file with SIZEOF_HAZELCAST_T and ALIGNOF_HAZELCAST_T appropriately defined. 这样的可执行文件是什么样子的吗?每次看到的课程看起来相对容易,但我真的不知道如何获得它的“大小”github.com/hazelcast/hazelcast-cpp-client/blob/master/hazelcast/… 但无论如何,正如迈克尔安德森指出的那样,我需要一个销毁函数,否则永远不会调用 CPP 类的析构函数。 如果你的类需要一个析构函数,你最好忘记整个想法,因为用户将不得不以一种或另一种方式调用清理函数。 我查看了 Data 类的当前实现,它现在似乎没有析构函数,但我怀疑它可能会在 C++ 客户端的未来版本中得到一个。我认为这是我无法更改的第 3 方代码,因此依赖它太冒险了。尽管如此,还是非常感谢您的见解。

以上是关于在堆栈上分配不完整的类型的主要内容,如果未能解决你的问题,请参考以下文章

是否在堆栈上分配了值类型的本地数组并立即收集垃圾? [复制]

品味类型——值类型和引用类型

c语言中啥是类(class),啥是结构。两者有啥区别?详细点。书上不太清楚。。谢谢

菜鸟笔记三数据类型

C++ 优化 - 堆栈分配的数组类型与外部链接维度?

JAVA中堆栈和内存分配原理