再见,Q_FOREACH!

原文地址:https://www.kdab.com/goodbye-q_foreach/

Q_FOREACH(有时也会被称为foreach)将在不久的将来被废除,有可能是在 Qt 5.9。从 Qt 5.7 开始,你可以使用QT_NO_FOREACH宏定义来确保代码中没有依赖Q_FOREACH。【译注:这里应该是说,在 .pro 文件中添加DEFINES += QT_NO_FOREACH定义】

你可能想知道为什么这么大惊小怪。为什么针对 Qt 使用 C++11 的范围for循环替换掉Q_FOREACH会提交一大堆 commit ?为什么要用这么多次提交和这么多个 Qt 版本来逐步移植替换Q_FOREACH?难道我们不能全局搜索,把Q_FOREACH (a, b)替换为for (a : b)就完事了?

阅读下文,你就可以找到答案。

什么是Q_FOREACH

Q_FOREACH是 Qt 4 添加的一个宏,允许方便地遍历 Qt 容器:

事实上,Q_FOREACH是将第二个参数拷贝到一个QForeachContainer变量,然后对其进行遍历。我之所以提到这一点,主要是两个原因:第一,你会在废除警告(可能从 Qt 5.9 开始有这个警告)的地方看到QForeachContainer这个内部的名字;第二,是的,你没有听错,这个操作拷贝了容器。

这个拷贝操作有两个影响:第一,因为存在拷贝,所以这个循环的对象本质上是一个常量,不会在遍历时发生分离,这与使用 C++98 或者 C++11 是不一样的:

上面两个例子(显式或者隐式地)调用了begin()end(),这会引起一个非const容器与共享数据分离,换句话说,这会导致深拷贝数据以获得一个唯一的拷贝。

这个问题众所周知,以至于有许多工具(例如 Clazy)都能检测这一情形,所以这里我们不再赘述。简单来说就是,Q_FOREACH不会导致分离。

除非它要这么做。

Q_FOREACH是方便还是魔鬼?

Q_FOREACH复制容器的第二个影响是,循环体可以随意修改原始容器。下面是利用这一点的一个非常非常糟糕的代码:

当然,因为Q_FOREACH进行了拷贝,一旦你执行第一次遍历,languages就会从Q_FOREACH的那个拷贝中分离,但是在Q_FOREACH中使用这种代码是安全的,但是类似这样使用 C++11 的范围for循环则不然:

所以,正如我们见到的那样,Q_FOREACH对于代码非常方便。【译注:这里的“写”代码,即代表你自己编写代码,又表示一个写操作】

如果你希望理解使用Q_FOREACH的代码,事情就有点不同了。因为你通常很难分清Q_FOREACH无条件进行的拷贝,究竟是在种种特殊情形下真的需要,还是不需要。在遍历时修改容器经常导致循环平白无故地崩溃,这在使用Q_FOREACH循环的时候更容易出现。

这让我们想到了移除Q_FOREACH

来到没有Q_FOREACH的世界

如果你能够全局检索Q_FOREACH (a, b)并替换为for (a : b)的话,事就这么成了。但是,一般来说这都没那么容易。

现在我们知道,Q_FOREACH的循环体允许我们在遍历时修改正在遍历的容器,连一分钟都不用,我们就能想到,要识别出上面languages的那种代码并不会那么容易。对容器的修改可能距离循环体本身都要几层的函数调用。

所以,你需要问你自己的第一个问题是,

这个循环体究竟有没有(直接或者间接)修改正在遍历的容器?

如果答案是“是”,你就需要自己创建一个拷贝,然后在拷贝上进行遍历,但是,作为一个聪明的程序员,你可以写下一行注释,来告诉后人为什么需要这么一个拷贝:

应该注意到,如果容器的修改仅限于追加元素,你可以使用索引循环来避免拷贝(以及因此造成的分离):

避免分离

如果你的容器是std::标准容器或QVarLengthArray,这么做就行了。有人说,Q_FOREACH绝对不能用在这样的容器上,因此拷贝这样的容器就会拷贝所有元素(深拷贝)。

如果你的容器是const左值或者const右值,这么做也足够了。const对象不会分离,就连 Qt 容器也不会。

如果你的容器是非const右值,需要将其保存到一个auto const变量,然后再遍历:

最后,如果你的容器是非const左值,你有两个选择:将容器转换成const,如果不行的话,使用std::as_const()或者qAsConst()(Qt 5.7 新增,但是你自己也能简单地实现)将其转换成const

好了,没有分离了,没有不必须的拷贝。最好的性能以及最好的可读性。

结论

下面是为什么要从你的代码移除Q_FOREACH,使用 C++11 的范围for循环:

  • Q_FOREACH很快就要被废除。
  • Q_FOREACH只对(某些)Qt 容器有效;对于所有的标准容器、QVarLengthArray的性能都非常差,而且不能使用 C 数组。
  • 即便那些宣称的适用范围,Q_FOREACH的每次循环也会比 C++11 范围for循环多出大约 100 字节。
  • Q_FOREACH无条件拷贝容器导致它的代码难以理解。

恭祝愉快地移除!

【如果想了解更多有关 Qt 容器和 C++11 的范围for循环的内容,可以阅读这篇文章。如果需要的话,豆子会在以后将这篇也一起翻译出来。】

上一篇

Comments (2)

  1. f0g 2016年10月18日
    • 豆子 2016年10月20日

Leave a Reply