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

这本应是一个无须争论的问题——当然必须调用。

stdarg(或varargs,下略)中提供的功能就是一种契约
“你按我的约定方式使用这些宏
——即必须调用va_end
——我就给你提供实现可变长参数列表所需要的功能。”


使用stdarg本来是简单的事情
——按照一个简单的契约(另见相关链接)办事就可以了
——根本无须了解其具体实现。

有人乐意去研究该功能是如何实现的, 也很好。


可是某些人
——或通过研究其的实现,或通过实践
——发现他所使用的平台下, va_end是可以忽略的。
之后,他就开始大放厥词 : “va_end是不必要的!”

由此, 造成一些不必要的误解与争论。



让我们看看对va_end的两种态度:



一、 va_end能省则省?

假设你使用的某个C/C++编译器,提供的va_end是可忽略的。
比如msvc中的va_end的实现如下:
#define  va_end(apap = (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
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值