【发布时间】:2009-05-14 22:47:17
【问题描述】:
我正在编写一个更新结构中许多不同字段的 API。
我可以通过使更新函数可变参数来帮助添加未来字段:
update(FIELD_NAME1, 10, FIELD_NAME2, 20);
然后在不更改任何现有调用的情况下添加FIELD_NAME3:
update(FIELD_NAME1, 10, FIELD_NAME2, 20, FIELD_NAME3, 30);
请说些智慧的话?
【问题讨论】:
我正在编写一个更新结构中许多不同字段的 API。
我可以通过使更新函数可变参数来帮助添加未来字段:
update(FIELD_NAME1, 10, FIELD_NAME2, 20);
然后在不更改任何现有调用的情况下添加FIELD_NAME3:
update(FIELD_NAME1, 10, FIELD_NAME2, 20, FIELD_NAME3, 30);
请说些智慧的话?
【问题讨论】:
一般不会。
Varargs 抛出了很多类型安全问题——你可以传递指针、浮点数等,而不是 int,它会毫无问题地编译。滥用可变参数(例如省略参数)可能会由于堆栈损坏或读取无效指针而导致奇怪的崩溃。
例如,以下调用将编译并导致崩溃或其他奇怪的行为:
UpdateField(6, "Field1", 7, "Field2", "Foo");
最初的 6 是期望的参数数量。它将字符串指针 "Foo" 转换为 int 以放入 Field2,并尝试读取和解释其他两个不存在的参数,这可能会因取消引用堆栈噪声而导致崩溃。
我相信在 C 中实现 varargs 是一个错误(考虑到今天的环境 - 它在 1972 年可能非常有意义。)实现是你在堆栈上传递一堆值,然后被调用者将遍历堆栈拾取参数,基于它对一些初始控制参数的解释。这种类型的实现基本上会让你以一种很难诊断的方式犯错误。 C# 对此的实现,在方法上传递具有属性的对象数组,只是必须更明智,尽管不能直接映射到 C 语言中。
【讨论】:
我倾向于避免使用可变参数,除非在一种非常有用的特定情况下。变量参数并没有真正提供超出单个函数调用可以完成的所有好处,尤其是在您的情况下。
就可读性而言(除了非常特殊的情况,我通常更喜欢原始速度),以下两个选项之间没有真正的区别(我在可变参数版本中添加了一个计数,因为您需要一个计数或哨兵来检测数据的结束):
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);
以“每个字段名称一次调用”的方式执行此操作会导致函数本身的代码更简单,并且不会降低调用的可读性。此外,它还允许编译器正确检测它无法对可变参数执行的某些错误,例如不正确的类型或用户提供的计数与实际计数不匹配。
如果您真的必须能够调用单个函数来执行此操作,我会选择:
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() 等函数的日志库,以提供相同的功能。在我需要使用它的漫长(我的意思是,很长 :-) 的时间里,我想不出任何其他时间。
顺便说一句,如果您决定使用可变参数,我倾向于使用哨兵而不是计数,因为这样可以防止在添加字段时出现不匹配。您很容易忘记调整计数并最终得到:
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 中可变参数的一个问题是您不知道传递了多少个参数,因此您需要将其作为另一个参数:
update(2, FIELD_NAME1, 10, FIELD_NAME2, 20);
update(3, FIELD_NAME1, 10, FIELD_NAME2, 20, FIELD_NAME3, 30);
【讨论】:
update(FIELD_NAME1, 10, FIELD_NAME2, 20, NULL);,因为您不需要编辑第一个参数来添加另一个字段
NULL 是有效的FIELD_NAME。我希望FIELD_NAMEs 是enum 值,所以你需要一个特殊的END_FIELD 值。在合适的时候使用NULL 很好,但并不总是合适。
为什么没有一个参数,一个数组。更好的是,指向数组的指针。
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 个参数。我撒了谎,并认为长度参数会很好。
【讨论】:
void** 变量......
我会仔细研究任何设计用于外部(甚至内部)使用的“更新”功能,它使用相同的功能来更新结构中的许多不同字段。是否有特定原因导致您不能使用离散功能来更新字段?
【讨论】:
到目前为止,避免使用可变参数的理由都很好。让我添加另一个尚未给出的,因为它不太重要,但可以遇到。可变参数要求参数在堆栈上传递,从而减慢函数调用。在某些架构上,差异可能是显着的。在 x86 上它不是很重要,因为它缺少寄存器,例如,在 SPARC 上,它可能很重要。最多可以在寄存器上传递 5 个参数,如果您的函数使用的局部变量很少,则不会进行堆栈调整。如果您的函数是叶函数(即不调用另一个函数),则也没有窗口调整。因此,通话费用非常小。使用可变参数,在堆栈上传递参数、堆栈调整和窗口管理的正常顺序是正常的,否则您的函数将无法获取参数。这会显着增加通话费用。
【讨论】:
这里的很多人都建议传递参数#,但其他人正确地指出,这会导致隐蔽的错误,即字段# 发生变化,但传递给可变参数函数的计数却没有。我在产品中通过使用空终止来解决这个问题:
send_info(INFO_NUMBER,
Some_Field, 23,
Some_other_Field, "more data",
NULL);
这样,当复制和粘贴程序员不可避免地复制它时,他们不太可能搞砸。更重要的是,我不太可能把事情搞砸。
回顾最初的问题,你有一个函数必须更新一个包含很多字段的结构,并且结构会增长。将此类数据传递给函数的常用方法(在 Win32 和 MacOS 经典 API 中)是通过传递另一个结构(甚至可以是与您正在更新的结构相同的结构),即:
无效更新(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。
【讨论】: