这本应是一个无须争论的问题——当然必须调用。
stdarg(或varargs,下略)中提供的功能就是一种契约:
“你按我的约定方式使用这些宏
——即必须调用va_end
——我就给你提供实现可变长参数列表所需要的功能。”
使用stdarg本来是很简单的事情
——按照一个简单的契约(另见相关链接)办事就可以了
——根本无须了解其具体实现。
有人乐意去研究该功能是如何实现的, 也很好。
可是某些人
——或通过研究其的实现,或通过实践
——发现他所使用的平台下, va_end是可以忽略的。
之后,他就开始大放厥词 : “va_end是不必要的!”
由此, 造成一些不必要的误解与争论。
让我们看看对va_end的两种态度:
一、 va_end能省则省?
假设你使用的某个C/C++编译器,提供的va_end是可忽略的。
比如msvc中的va_end的实现如下:
#define va_end(ap) ap = (va_list)0 /* 将ap置空 */
通常直接使用va_start的函数(假设叫f)的实现体会很短。:
1. 用va_start初始化va_list
2. 调用一个使用va_list参数的函数(假设叫vf)
(vf 是一个固定参数列表的函数)。
因为f的实现体非常短, 一眼望穿。
所以你能确保vf返回后, ap不会再被你使用。
因此, 将ap置空除了浪费CPU周期, 没有实际意义, 是这样吗?
一、1. 编译器参与优化
你能发现代码末尾ap不再被使用, va_end将其置空毫无意义。
那么,你的编译器能发现这个问题么?
请查证一下。
如果编译器也知道, 并且没有为va_end生成任何代码, 那么省略va_end就是不必要的了。
一、2. 编译器不参与优化
你编译器真为va_end生成了无意义并且令人感到无法接受的机器码时,该怎么办?
一、2.1 你只在该编译器下工作
那么,你省略va_end好了。
但请不要宣扬一些带有误导性质的言辞。
当你说“va_end是不需要”的时候, 请附带说明:
1. 你的平台
2. 你不考虑跨平台
一、2.2 需要要考虑移植到其他编译器
注意, 其他编译器包括(但不限于):
——不同架构上的编译器
——相同架构上的不同编译器产品
——相同架构上的相同编译器产品的不同版本。
需要分析在该编译器下,对va_end的处理是否依然可以被省略。
——显然,这是一项乏味的工作。
即使你在源代码中写入 :
/* va_end is trivial, omit it */
也难保它不会被遗忘
—— 移植一个程序的时候有太多工作要做。
这么一个不起眼的地方, 会被想起来么?
如果在被移植的编译器上:
1. 省略va_end将导致函数不能正常返回(见附录)
也许立马就能发现这个bug。
崩掉了嘛, 当然要引起“重视”。
2. 省略va_end不会立马崩溃, 而是导致内存泄露(见附录)
情况就很严重了。
程序依然运行“良好”。
但是调用一次函数, 就泄漏一点点内存。
这恐怕就要花很多时间才能查出来了。
如果项目时间再紧一点, 也许根本就来不及修复这个bug就发布了。
反正漏得也“不多”, 你说是吧?
二、 va_end能留则留
我们何不换个方式?
1. 坚持使用va_end
——即便我们心里清楚它没做什么有用的事情也是如此。
代码移植本质就是: 不对平台(CPU、OS、Compiler等等)产生依赖。
stdarg就是标准库提供的一种实现可变长参数列表的可移植方式。
我们没理由弃之不用。
如果我们在源代码中坚持使用va_end:
——至少在这点上,就不会对编译器产生依赖(而省略va_end,就是一种依赖)。
——移植的时候, 自然无须为其操心。
2. va_end令编译器产生了令人无法接受的无用代码时
——通常,这是不会发生的。 编译器厂商会考虑这个事情。
比如上面的va_end宏, 会产生一次不必要的赋值操作, 但通常会被编译器优化为空。
即使没有被优化为空, 一次赋值操作, 真的就是不可容忍的么?
如果确实不能容忍, 作为一种特殊情形, 可以这样 :
#if defined(COMPILER1) || defined(COMPILER2) || ...
/** special situation
the machine code generated by these compliers is unacceptable, omit it
*/
#else
/* general situation */
va_end(ap);
#endif
附录 —— 看看大牛们是怎么说的。
从一个使用过va_start()的函数中退出之前,
必须调用一次va_end()。
这是因为va_start
可能以某种方式
修改了堆栈,这种修改可能导致
返回无法完成,va_end()能将有关的修改复原。
——《C++程序设计语言》 第3版、特别版, p139
——即上面提到的 “立即崩溃”。
我们务必记住,在使用完va_list变量后一定要调用宏va_end。
在
大多数C实现上,调用va_end与
否并无区别。
但是,
某些版本的va_start宏为了方便对va_list的遍历,就给参数列表
动态分配内存。
这样一种C实现很可能利用va_end宏来释放此前动态分配的内存;
如果忘记调用宏va_end,最后得到的程序可能在某些机型上没有问题,而在另一些机型上则发生“
内存泄露”。
——《C陷阱与缺陷》, p161
——即上面提到的“内存泄露”。
…… 最后,
必须在函数返回之前
调用va_end,以完成一些必要的
清理工作。
——《C程序设计语言》 第2版, p137
……在所有参数处理完毕后, 且在
退出函数f之前,
必须调用宏va_end一次 ……
——《C程序设计语言》 第2版, p232
相关链接:
——可变长参数列表误区与陷阱——va_arg不可接受的类型
http://www.cppblog.com/ownwaterloo/archive/2009/04/21/unacceptable_type_in_va_arg.html
这是使用stdarg提供的功能需要遵守契约之一。
契约本身仍然是简单的。
契约背后的原理也许比较晦涩, 但也可以不必关心。
本
作品采用
知识共享署名-非商业性使用-相同方式共享 2.5 中国大陆许可协议进行许可。
转载请注明 :
文章作者 - OwnWaterloo
发表时间 - 2009年04月21日
原文链接 -
http://www.cppblog.com/ownwaterloo/archive/2009/04/21/is_va_end_necessary.html
posted on 2009-04-21 15:53
OwnWaterloo 阅读(4869)
评论(2) 编辑 收藏 引用