可变长参数列表误区与陷阱——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的实现如下:
#defineva_end(ap)ap= (va_list)0/ 将ap置空 /
通常直接使用va_start的函数(假设叫f)的实现体会很短。: 1. 用va_start初始化va_list2. 调用一个使用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宏, 会产生一次不必要的赋值操作, 但通常会被编译器优化为空。 即使没有被优化为空, 一次赋值操作, 真的就是不可容忍的么?

如果确实不能容忍, 作为一种特殊情形, 可以这样 :
#ifdefined(COMPILER1) ||defined(COMPILER2) || .../ special situation the machine code generated by these compliers is unacceptable, omit it/#else/generalsituation/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) 编辑 收藏 引用原文链接: https://www.cnblogs.com/jackdong/archive/2012/06/27/2565833.html

欢迎关注

微信关注下方公众号,第一时间获取干货硬货;公众号内回复【pdf】免费获取数百本计算机经典书籍

原创文章受到原创版权保护。转载请注明出处:https://www.ccppcoding.com/archives/53708

非原创文章文中已经注明原地址,如有侵权,联系删除

关注公众号【高性能架构探索】,第一时间获取最新文章

转载文章受原作者版权保护。转载请注明原作者出处!

(0)
上一篇 2023年2月9日 上午5:01
下一篇 2023年2月9日 上午5:01

相关推荐