β

再见,Q_FOREACH!

DevBean's World 66 阅读

原文地址: 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 循环:

恭祝愉快地移除!

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

作者:DevBean's World
Colorful Technologies...
原文地址:再见,Q_FOREACH!, 感谢原作者分享。

发表评论