原文地址: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(int i, container) doSomethingWith(i); Q_FOREACH(const QString &s : functionReturningQStringList()) doSomethingWith(s);
事实上,Q_FOREACH
是将第二个参数拷贝到一个QForeachContainer
变量,然后对其进行遍历。我之所以提到这一点,主要是两个原因:第一,你会在废除警告(可能从 Qt 5.9 开始有这个警告)的地方看到QForeachContainer
这个内部的名字;第二,是的,你没有听错,这个操作拷贝了容器。
这个拷贝操作有两个影响:第一,因为存在拷贝,所以这个循环的对象本质上是一个常量,不会在遍历时发生分离,这与使用 C++98 或者 C++11 是不一样的:
for (QStringList::const_iterator it = container.begin(), end = container.end(); it != end; ++it) doSomethingWith(*it); for (const auto &s : container) doSomethingWith((*it);
上面两个例子(显式或者隐式地)调用了begin()
和end()
,这会引起一个非const
容器与共享数据分离,换句话说,这会导致深拷贝数据以获得一个唯一的拷贝。
这个问题众所周知,以至于有许多工具(例如 Clazy)都能检测这一情形,所以这里我们不再赘述。简单来说就是,Q_FOREACH
不会导致分离。
除非它要这么做。
Q_FOREACH
是方便还是魔鬼?
Q_FOREACH
复制容器的第二个影响是,循环体可以随意修改原始容器。下面是利用这一点的一个非常非常糟糕的代码:
Q_FOREACH(const QString &lang, languages) languages += getSynonymsFor(lang);
当然,因为Q_FOREACH
进行了拷贝,一旦你执行第一次遍历,languages
就会从Q_FOREACH
的那个拷贝中分离,但是在Q_FOREACH
中使用这种代码是安全的,但是类似这样使用 C++11 的范围for
循环则不然:
for (const auto &lang : languages) languages += getSynonymsFor(lang); // 如果 languages.size() + getSynonymsFor(lang).size() > languages.capacity(),这将是无定义行为
所以,正如我们见到的那样,Q_FOREACH
对于写代码非常方便。【译注:这里的“写”代码,即代表你自己编写代码,又表示一个写操作】
如果你希望理解使用Q_FOREACH
的代码,事情就有点不同了。因为你通常很难分清Q_FOREACH
无条件进行的拷贝,究竟是在种种特殊情形下真的需要,还是不需要。在遍历时修改容器经常导致循环平白无故地崩溃,这在使用Q_FOREACH
循环的时候更容易出现。
这让我们想到了移除Q_FOREACH
。
来到没有Q_FOREACH
的世界
如果你能够全局检索Q_FOREACH (a, b)
并替换为for (a : b)
的话,事就这么成了。但是,一般来说这都没那么容易。
现在我们知道,Q_FOREACH
的循环体允许我们在遍历时修改正在遍历的容器,连一分钟都不用,我们就能想到,要识别出上面languages
的那种代码并不会那么容易。对容器的修改可能距离循环体本身都要几层的函数调用。
所以,你需要问你自己的第一个问题是,
这个循环体究竟有没有(直接或者间接)修改正在遍历的容器?
如果答案是“是”,你就需要自己创建一个拷贝,然后在拷贝上进行遍历,但是,作为一个聪明的程序员,你可以写下一行注释,来告诉后人为什么需要这么一个拷贝:
const auto containerCopy = container; // 由于...,doSomethingWith() 可能要修改 container for (const auto &e : containerCopy) doSomethingWith(e);
应该注意到,如果容器的修改仅限于追加元素,你可以使用索引循环来避免拷贝(以及因此造成的分离):
for (auto end = languages.size(), i = 0; i != end; ++i) // 注意,保存 languages.size() languages += getSynonymsFor(languages[i]);
避免分离
如果你的容器是std::
标准容器或QVarLengthArray
,这么做就行了。有人说,Q_FOREACH
绝对不能用在这样的容器上,因此拷贝这样的容器就会拷贝所有元素(深拷贝)。
如果你的容器是const
左值或者const
右值,这么做也足够了。const
对象不会分离,就连 Qt 容器也不会。
如果你的容器是非const
右值,需要将其保存到一个auto const
变量,然后再遍历:
const auto strings = functionReturningQStringList(); for (const QString &s : strings) doSomethingWith(s);
最后,如果你的容器是非const
左值,你有两个选择:将容器转换成const
,如果不行的话,使用std::as_const()
或者qAsConst()
(Qt 5.7 新增,但是你自己也能简单地实现)将其转换成const
:
for (const QString &s : qAsConst(container)) doSomethingWith(s);
好了,没有分离了,没有不必须的拷贝。最好的性能以及最好的可读性。
结论
下面是为什么要从你的代码移除Q_FOREACH
,使用 C++11 的范围for
循环:
Q_FOREACH
很快就要被废除。Q_FOREACH
只对(某些)Qt 容器有效;对于所有的标准容器、QVarLengthArray
的性能都非常差,而且不能使用 C 数组。- 即便那些宣称的适用范围,
Q_FOREACH
的每次循环也会比 C++11 范围for
循环多出大约 100 字节。 Q_FOREACH
无条件拷贝容器导致它的代码难以理解。
恭祝愉快地移除!
【如果想了解更多有关 Qt 容器和 C++11 的范围for
循环的内容,可以阅读这篇文章。如果需要的话,豆子会在以后将这篇也一起翻译出来。】
3 评论
const auto containerCopy = container; // 由于...,doSomethingWith() 可能要修改 container
for (const auto &e : containerCopy)
doSomethingWith(e);
不太明白,既然是const,就代表值不被修改. 即使修改,修改拷贝的容器的值又有什么意义呢?
这段代码是用来替代 Q_FOREACH 的,由于 Q_FOREACH 做了拷贝,所以在移除 Q_FOREACH 时,必须做同样的处理才能保证移除之后的代码仍能继续工作。
QVector myVec;
auto myCopyVec = myVec; //此时仅是将myVec的引用复制了,没有发生Detach
for(auto& iter : myVec)
{
qDebug() << iter;
//明显MyCopyVec被Detach了,但是如果你的本意是修改myVec,他本身就需要被Detach,所以无妨
//如果你不想要修改myVec,只读,那么myCopyVec的Detach是无意义的,是浪费性能的。
}
QVector& myVecRef;
for(auto& iter : myVecRef) //相当于for(auto& iter : myVec)
{
//不会产生Detach,因为myVecRef并没有产生新的对象,只是原先对象的别名
}
QVector* pMyVec = &myVec;
for(auto& iter : *pMyVec) //相当于for(auto& iter : myVec)
{
//不会产生Detach,因为pMyVec解引用后指向myVec,并非临时对象,还是相当于原先对象
}