C - 接受已知大小但未知类型的参数

C - Accept Argument of Known Size but Unknown Type

我有兴趣在 C 中创建我自己的双重 linked 列表。目标是使其尽可能灵活和 "user" 友好。这意味着它不能局限于一种类型的数据。我还想尽量减少列表代码之外的内存管理。也就是说,我希望列表代码处理分配和释放任何必要的内存。 (但是,当然,该列表可用于存储指向动态分配数据的指针。)

我正在使用两个结构。 "node" 结构保存指向它之前和紧随其后的节点的指针,一个指向它包含的数据的 void* 指针,以及数据的大小。但是,因为这是 C,它不能保存数据类型。 "list" 结构跟踪列表的开头和结尾、列表中元素的数量等。我已经实现了初始化列表和将数据附加到列表的功能。内存分配和释放以及 linking 似乎工作正常,列表似乎 link 彼此正确。问题是如何在创建列表节点时实际导入数据。以下是我考虑过的方法:

  1. 通过 void* 传递指向数据的指针,并将大小作为另一个参数。这可以通过添加一个宏来获取变量的地址和大小并将它们传递给函数来变得更加用户友好。问题?并非我想添加到列表中的所有内容都可以使用其地址。例如,考虑 list_append(list, 17)。这应该将一个整数负载值为 17 的新节点添加到列表的末尾,但它不会工作,因为整数文字 17 无法获取其地址。

  2. 将数据的大小作为一个参数传递,并将调用堆栈上的数据本身作为一个额外的参数传递。 C 通过 .../stdarg.h 方法支持未知数量、类型和大小的参数。我想我可以使用宏来获取 sizeof() 被追加的项目并将其与项目本身一起传递给追加函数。这里的问题是可变参数宏要我指定一个类型(而不仅仅是类型的大小)。所以,我做了一些挖掘,发现 GCC 显然使用 __builtin_next_arg 宏来实现 stdarg.h 中的变量参数。显然,这会使我的代码依赖于 GCC(或者至少依赖于这个特定的宏),但它至少可以与这个特定的编译器一起工作。据称 __builtin_next_arg 宏给出了参数列表中最后一个命名参数之后的参数地址作为 void*。当我在 Windows(使用 MinGW 32 位)上尝试这种方法时,它按预期工作。一个简单的 memcpy()__builtin_next_arg 给定的值到新分配的缓冲区复制了数据。然而,当我在 Ubuntu 上使用 GCC 64 位时,一切都乱套了。 __builtin_next_arg 给我的地址与我预期的论点相去甚远。编译器偶尔也会开始抱怨 va_start 的第二个参数不是列表中的最后一个参数,尽管事实上我根本没有在我的代码中使用 va_start。此外,无论我做什么,我得到的值似乎都是零(NULL0)。

有什么办法可以解决这个问题吗?我基本上想要的是 va_arg 的一个版本,它给出了堆栈上参数的地址。其他方法也是可以接受的。

在 C++ 中,我可以使用模板来完全避免这个问题,但我想使用 C。

我建议您要么根本不使用 __builtin_* 函数,要么通过阅读您编写的简单函数的 docs/gcc sources/asm 编译器输出来彻底研究它们的实现,看看它是如何实现的有效。

首先,__builtin_next_arg 的行为在 mingwlinux 环境之间可能完全不同,因为使用的 ABI 不同。

接下来,__builtin_* 功能可能会被不同 gcc 版本中的另一个功能取代。例如,在 gcc-4.8.3(linux build,准确地说)中,va_* macroses 是使用 __builtin_va_start()__builtin_va_end()__builtin_va_arg() 函数并且 gcc 内部头文件中没有出现 __builtin_next_arg

不,您不能在此处使用 __builtin_next_arg。原因是即使使用 __builtin_next_arg 等,您的 调用函数 也必须知道 数据类型

不,函数参数不存储在堆栈中,比如 Linux x86-64 ABI;前 6 个参数在寄存器中提供。并且寄存器取决于它们的类型,所以 floatint 在不同的寄存器中,即使它们的大小都是 4。抱歉,你不能这样做,即使在特定的 GCC 版本中这个 ABI。


但是,如果您使用的是 C11,则可以使用 ,它使用 static_assert 断言传入的 sizeof 对象;然后你可以使用一些 _Generic 技巧将不同类型存储到包含 char contents[sizeof val];struct 中,例如:

#include <assert.h>
#include <string.h>

#define VALUE_SIZE (sizeof(int))

struct list;
struct value_type {
    char contents[VALUE_SIZE];
};

int _list_append(struct list *l, struct value_type v);

struct value_type coerce_float(float arg) {
    struct value_type rv;
    memcpy(&arg, rv.contents, sizeof arg);
    return rv;
}

struct value_type coerce_int(int arg) {
    struct value_type rv;
    memcpy(&arg, rv.contents, sizeof arg);
    return rv;
}

static_assert(sizeof(float) == VALUE_SIZE,
              "code relies on float and int being of the same size");

#define coerce_arg(X) (_Generic((X),           \
                          float: coerce_float, \
                          int: coerce_int,      \
                          void*: coerce_int \
                      )((int)X))

#define list_append(L, X) _list_append(L, coerce_arg(X))

list_append(l, 4);
list_append(l, 4.0f);
list_append(l, (void*)0);  // will throw an error since it is not supported

这是非常可移植的,但请注意,例如 MSVC 编译器甚至不支持 C99。 GCC 和 LLVM 应该没有问题。然而,不利的一面是,您必须手动添加 each 支持的兼容类型。包括每个指针类型,或者将您的指针转换为 (void*).


如果您只使用 GCC 没问题,我相信您可以编写一个使用 typeof 的宏来生成具有确切参数类型的变量,然后 memcpy 将其内容直接放入此结构中.