可变长参数列表误区与陷阱——va_end是必须的吗?

http://www.cppblog.com/ownwaterloo/archive/2009/04/21/is_va_end_necessary.html

这本应是一个无须争论的问题——当然必须调用。
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提供的功能需要遵守契约之一。 契约本身仍然是简单的。 契约背后的原理也许比较晦涩, 但也可以不必关心。


Creative Commons License作品采用知识共享署名-非商业性使用-相同方式共享 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 阅读(1686) 评论(2)  编辑 收藏 引用
posted @ 2012-06-27 14:57  董雨  阅读(309)  评论(0编辑  收藏  举报