在C API中使用varargs来设置键值对是一个好主意吗?

我正在编写一个API来更新结构中的许多不同字段。

我可以通过使更新函数variadic来帮助添加将来的字段:

update(FIELD_NAME1, 10, FIELD_NAME2, 20); 

然后添加FIELD_NAME3更改任何现有的调用:

 update(FIELD_NAME1, 10, FIELD_NAME2, 20, FIELD_NAME3, 30); 

请问智慧的话语?

一般来说,没有。

Varargs抛出了很多类型安全 – 你可以传递指针,浮点数等,而不是整数,它将编译没有问题。 滥用varargs(例如省略参数)可能会因堆栈损坏或读取无效指针而引入奇怪的崩溃。

例如,以下调用将编译并导致崩溃或其他奇怪的行为:

 UpdateField(6, "Field1", 7, "Field2", "Foo"); 

最初的6是预期的参数数量。 它会将字符串指针“Foo”转换为int以放入Field2,并且它将尝试读取和解释其他两个不存在的参数,这可能会导致崩溃,从而解除引用堆栈噪声。

我相信在C语言中实现varargs是一个错误(考虑到今天的环境 – 它可能在1972年完全合理。)实现是你在堆栈上传递一堆值然后被调用者将在堆栈中获取参数,基于关于它对一些初始控制参数的解释。 这种类型的实现基本上会让您在可能非常难以诊断的方式中犯错。 C#的实现,传递一个带有方法属性的对象数组,只是必须更加安全,尽管不能直接映射到C语言。

我倾向于避免使用varargs,除非在一个非常有用的特定情况下。 除了单个函数调用可以完成的任务之外,变量参数并没有真正提供所有好处,特别是在您的情况下。

在可读性方面(除了非常具体的情况,这通常比我更喜欢原始速度),以下两个选项之间没有真正的区别(我已经为varargs版本添加了一个计数,因为你需要一个计数或者哨兵来检测数据的结尾):

 update(2, FIELD_NAME1, 10, FIELD_NAME2, 20); update(3, FIELD_NAME3, 10, FIELD_NAME4, 20, FIELD_NAME5, 30); /* ========== */ update(FIELD_NAME1, 10); update(FIELD_NAME2, 20); update(FIELD_NAME3, 10); update(FIELD_NAME4, 20); update(FIELD_NAME5, 30); 

实际上,随着varargs版本变得越来越长,无论如何都需要将其拆分,以便进行格式化:

 update(5, FIELD_NAME1, 10, FIELD_NAME2, 20, FIELD_NAME3, 10, FIELD_NAME4, 20, FIELD_NAME5, 30); 

这样做“每个字段名称一次调用”的方式导致函数本身的代码更简单,并且不会降低调用的可读性。 此外,它允许编译器正确检测它不能对varargs执行的某些错误,例如不正确的类型或用户提供的计数与实际计数之间的不匹配。

如果你真的必须能够调用一个函数来执行它,我会选择:

 void update (char *k1, int v1) { ... } void update2 (char *k1, int v1, char *k2, int v2) { update (k1, v1); update (k2, v2); } void update3 (char *k1, int v1, char *k2, int v2, char *k3, int v3) { update (k1, v1); /* or these two could be a single */ update (k2, v2); /* update2 (k1, v1, k2, v2); */ update (k3, v3); } /* and so on. */ 

如果您愿意,您甚至可以将更高级别的function用作宏,而不会丢失类型安全性。

我倾向于使用varargs函数的唯一地方是提供与printf()相同的function – 例如,我偶尔必须编写具有提供相同function的logPrintf()等函数的日志库。 在我需要使用它的时候,我想不起任何其他时间在我的长期(我的意思是,长期:-)时间。

顺便说一句,如果你决定使用varargs,我倾向于选择哨兵而不是计数,因为这可以防止在添加字段时出现不匹配。 您可能很容易忘记调整计数并最终得到:

 update (2, k1, v1, k2, v2, k3, v3); 

添加时,这是阴险的,因为它默默地跳过k3 / v3,或者:

 update (3, k1, v1, k2, v2); 

删除时,这对于程序的成功运行几乎肯定是致命的。

有哨兵可以防止这种情况(当然,只要你不忘记哨兵):

 update (k1, v1, k2, v2, k3, v3, NULL); 

C语言中的varargs的一个问题是你不知道传递了多少个参数,所以你需要它作为另一个参数:

 update(2, FIELD_NAME1, 10, FIELD_NAME2, 20); update(3, FIELD_NAME1, 10, FIELD_NAME2, 20, FIELD_NAME3, 30); 

为什么没有一个arg,一个数组。 更好的是,指向数组的指针。

 struct field { int val; char* name; }; 

甚至…

 union datatype { int a; char b; double c; float f; // etc; }; 

然后

 struct field { datatype val; char* name; }; union (struct* field_name_val_pairs, int len); 

好的2 args。 我撒谎,并认为一个长度的参数会很好。

我会仔细研究任何旨在外部(甚至内部)使用的“更新”function,它使用相同的function来更新结构中的许多不同字段。 是否有特定原因导致您无法使用离散function来更新字段?

到目前为止,避免varargs的原因都很好。 让我添加另一个尚未给出的,因为它不太重要,但可以遇到。 vararg强制要求参数在堆栈上传递,从而减慢函数调用。 在一些架构上,差异可能是有意义的。 在x86上它不是很重要,因为它没有注册,例如,在SPARC上,它可能很重要。 寄存器上最多传递5个参数,如果您的函数使用少量本地,则不进行堆栈调整。 如果您的函数是叶函数(即不调用其他函数),则也没有窗口调整。 因此,通话费用非常小。 使用vararg,可以在堆栈上进行正常的传递参数序列,堆栈调整和窗口管理,或者您的函数无法获取参数。 这显着增加了呼叫的成本。

这里有很多人建议传递参数#,但是其他人正确地注意到这导致了一些阴险的错误,其中#字段发生了变化但传递给vararg函数的计数却没有。 我通过使用null终止来解决这个问题:

 send_info(INFO_NUMBER, Some_Field, 23, Some_other_Field, "more data", NULL); 

这样,当复制和粘贴程序员不可避免地复制它时,它们就不会搞砸了。 更重要的是,我不太可能弄乱它。

回顾一下原始问题,你有一个必须用很多字段更新结构的函数,结构会增长。 将此类数据传递给函数的常用方法(在Win32和MacOS经典API中)是通过传入另一个结构(甚至可以与您正在更新的结构相同),即:

void update(UPDATESTRUCTURE * update_info);

要使用它,您将填充字段:

 UPDATESTRUCTURE my_update = { UPDATESTRUCTURE_V1, field_1, field_2 }; update( &my_update ); 

稍后添加新字段时,可以更新UPDATESTRUCTURE定义并重新编译。 通过输入版本#,您可以支持不使用新字段的旧代码。

主题的变体是为您不想更新的字段赋值,例如KEEP_OLD_VALUE(理想情况下为0)或NULL。

 UPDATESTRUCTURE my_update = { field_1, NULL, field_3 }; update( &my_update); 

我没有包含版本,因为当我们增加UPDATESTRUCTURE中的字段数时,我会利用这个事实,额外的字段将初始化为0或KEEP_OLD_VALUE。