posts - 16,  comments - 34,  trackbacks - 0

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

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
posted on 2009-04-21 15:53 OwnWaterloo 阅读(4869) 评论(2)  编辑 收藏 引用

FeedBack:
# re: 可变长参数列表误区与陷阱——va_end是必须的吗?
2009-04-22 09:05 | abettor
我有个工程运行1周左右就死,可以肯定的是内存泄漏,但是调试了很多次,甚至带上Visual Leak Detector(自己看源代码查更是不知道多少遍了),也一直没有找到泄漏的地方。

我一直注意看的是new-delete和malloc-free的匹配,还真没有想到这里还有泄漏的可能,我决定再仔细找找。  回复  更多评论
  
# re: 可变长参数列表误区与陷阱——va_end是必须的吗?
2009-05-02 16:50 | sswv
受教了。  回复  更多评论
  

只有注册用户登录后才能发表评论。
网站导航: 博客园   IT新闻   BlogJava   知识库   博问   管理


<2009年5月>
262728293012
3456789
10111213141516
17181920212223
24252627282930
31123456

常用链接

留言簿(8)

随笔档案(16)

链接

搜索

  •  

积分与排名

  • 积分 - 196665
  • 排名 - 132

最新随笔

最新评论

阅读排行榜

评论排行榜