我正在编写一个更新结构中很多不同字段的API。
我可以通过使更新函数variadic来帮助添加将来的字段:
update(FIELD_NAME1, 10, FIELD_NAME2, 20);
然后添加FIELD_NAME3
而不更改任何现有的调用:
update(FIELD_NAME1, 10, FIELD_NAME2, 20, FIELD_NAME3, 30);
智慧的话语好吗?
答案 0 :(得分:11)
一般来说,没有。
Varargs抛出了很多类型安全性 - 你可以传递指针,浮点数等,而不是整数,它会编译而没有问题。滥用varargs(例如省略参数)可能会因堆栈损坏或读取无效指针而引入奇怪的崩溃。
例如,以下调用将编译并导致崩溃或其他奇怪的行为:
UpdateField(6, "Field1", 7, "Field2", "Foo");
最初的6是预期的参数数量。它会将字符串指针“Foo”转换为int以放入Field2,并且它将尝试读取和解释其他两个不存在的参数,这可能会导致此处出现崩溃,从而取消引用堆栈噪声。
我认为在C语言中实现varargs是一个错误(考虑到今天的环境 - 它可能在1972年完全有道理。)实现是你在堆栈上传递一堆值然后被调用者将走向堆栈拾取参数,基于其对一些初始控制参数的解释。这种类型的实现基本上会让您在可能非常难以诊断的方式中犯错。 C#的实现,传递一个带有方法属性的对象数组,只是必须更安全,尽管不能直接映射到C语言。
答案 1 :(得分:7)
我倾向于避免使用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. */
如果您愿意,您甚至可以将更高级别的功能用作宏,而不会丢失类型安全性。
我倾向于使用varargs函数的唯一地方是提供与printf()
相同的功能 - 例如,我偶尔必须编写具有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);
答案 2 :(得分:5)
C语言中的varargs的一个问题是你不知道传递了多少个参数,所以你需要将它作为另一个参数:
update(2, FIELD_NAME1, 10, FIELD_NAME2, 20);
update(3, FIELD_NAME1, 10, FIELD_NAME2, 20, FIELD_NAME3, 30);
答案 3 :(得分:3)
为什么没有一个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);
ok 2 args。我撒谎,并认为一个长度的参数会很好。
答案 4 :(得分:2)
我需要仔细研究任何旨在外部(甚至内部)使用的“更新”功能,它使用相同的功能来更新结构中的许多不同字段。是否有特定原因导致您无法使用离散功能来更新字段?
答案 5 :(得分:2)
迄今为止避免使用varargs的原因都很好。让我添加另一个尚未给出的,因为它不太重要,但可以遇到。 vararg强制要求参数在堆栈上传递,从而减慢函数调用。在一些架构上,差异可能是有意义的。在x86上它不是很重要,因为它没有注册,例如,在SPARC上,它可能很重要。寄存器上最多传递5个参数,如果您的函数使用少量本地,则不进行堆栈调整。如果您的函数是叶函数(即不调用其他函数),则也没有窗口调整。因此,通话费用非常小。使用vararg,可以在堆栈上进行正常的传递参数序列,堆栈调整和窗口管理,或者您的函数无法获取参数。这显着增加了通话费用。
答案 6 :(得分:1)
这里有很多人建议传递参数#,但是其他人正确地注意到这导致了一些阴险的错误,其中#字段发生了变化但是传递给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。