首页 Qt 再见,Q_FOREACH!

再见,Q_FOREACH!

3 1.9K

原文地址: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 评论

f0g 2016年10月18日 - 23:35

const auto containerCopy = container; // 由于...,doSomethingWith() 可能要修改 container
for (const auto &e : containerCopy)
doSomethingWith(e);

不太明白,既然是const,就代表值不被修改. 即使修改,修改拷贝的容器的值又有什么意义呢?

回复
豆子 2016年10月20日 - 21:38

这段代码是用来替代 Q_FOREACH 的,由于 Q_FOREACH 做了拷贝,所以在移除 Q_FOREACH 时,必须做同样的处理才能保证移除之后的代码仍能继续工作。

回复
Xyp9x 2022年1月3日 - 03:09

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,并非临时对象,还是相当于原先对象
}

回复

回复 Xyp9x 取消回复

关于我

devbean

devbean

豆子,生于山东,定居南京。毕业于山东大学软件工程专业。软件工程师,主要关注于 Qt、Angular 等界面技术。

主题 Salodad 由 PenciDesign 提供 | 静态文件存储由又拍云存储提供 | 苏ICP备13027999号-2