36

在 Qt 中,有一个foreach使用宏 ( ) 实现的循环Q_FOREACH。有不同的实现,具体取决于编译器。

GCC的定义如下:

#define Q_FOREACH(variable, container)                                \
for (QForeachContainer<__typeof__(container)> _container_(container); \
     !_container_.brk && _container_.i != _container_.e;              \
     __extension__  ({ ++_container_.brk; ++_container_.i; }))        \
    for (variable = *_container_.i;; __extension__ ({--_container_.brk; break;}))

QForeachContainer...使用定义如下的助手类:

template <typename T>
class QForeachContainer {
public:
    inline QForeachContainer(const T& t) : c(t), brk(0), i(c.begin()), e(c.end()) { }
    const T c;
    int brk;
    typename T::const_iterator i, e;
};

Q_FOREACH宏中的容器必须是一个类T,它至少必须提供T::const_iterator类型、aT.begin()T.end()方法,所有 STL 容器以及大多数 Qt 容器(如QList, QVector, QMap, QHash, ...

我现在的问题是:这个宏是如何工作的?

有一件事似乎很奇怪:变量在宏定义中只出现一次。所以例如在任何时候foreach(QString item, list)都有一个QString item =但没有之后......然后如何在每个步骤中更改item =变量?item

更令人困惑的是Q_FOREACH MS VC++ 编译器的以下定义:

#define Q_FOREACH(variable,container)                                                         \
if(0){}else                                                                                     \
for (const QForeachContainerBase &_container_ = qForeachContainerNew(container);                \
     qForeachContainer(&_container_, true ? 0 : qForeachPointer(container))->condition();       \
     ++qForeachContainer(&_container_, true ? 0 : qForeachPointer(container))->i)               \
    for (variable = *qForeachContainer(&_container_, true ? 0 : qForeachPointer(container))->i; \
         qForeachContainer(&_container_, true ? 0 : qForeachPointer(container))->brk;           \
         --qForeachContainer(&_container_, true ? 0 : qForeachPointer(container))->brk)

为什么true : 0 ? ...?这不是总是被评估0吗?qForeachPointer(container)即使之前的条件?为真,函数调用是否也会执行?

为什么我们需要两个 for 循环?

如果有人能让我更清楚一点,那就太酷了!

4

1 回答 1

74

GCC 版本


GCC 真的很简单。首先它是这样使用的:

Q_FOREACH(x, cont)
{
    // do stuff
}

这将扩展到

for (QForeachContainer<__typeof__(cont)> _container_(cont); !_container_.brk && _container_.i != _container_.e; __extension__  ({ ++_container_.brk; ++_container_.i; }))
    for (x = *_container_.i;; __extension__ ({--_container_.brk; break;}))
    {
        // do stuff
    }

所以首先:

for (QForeachContainer<__typeof__(cont)> _container_(cont); !_container_.brk && _container_.i != _container_.e; __extension__  ({ ++_container_.brk; ++_container_.i; }))

这是实际的for循环。它设置了一个QForeachContainer来帮助迭代。brk变量初始化为 0。然后测试条件:

!_container_.brk && _container_.i != _container_.e

brk为零所以!brk是真的,并且大概如果容器有任何元素i(当前元素)不等于e(最后一个元素)。

然后进入那个outer的body for,也就是:

for (variable = *_container_.i;; __extension__ ({--_container_.brk; break;}))
{
    // do stuff
}

所以x设置为*_container_.i迭代所在的当前元素,并且没有条件,所以大概这个循环将永远持续下去。然后进入循环体,也就是我们的代码,它只是一个注释,所以它什么也不做。

然后进入内循环的增量部分,有趣的是:

__extension__ ({--_container_.brk; break;})

它递减brk,所以现在是 -1,并跳出循环(__extension__这使得 GCC 不会发出使用 GCC 扩展的警告,就像你现在知道的那样)。

然后进入外循环的增量部分:

__extension__  ({ ++_container_.brk; ++_container_.i; })

它再次递增brk并再次变为0,然后i递增,因此我们到达下一个元素。检查条件,因为brk现在是 0 并且i可能还不等于e(如果我们有更多元素),重复该过程。

为什么我们先递减然后再递增brk呢?break原因是因为如果我们在代码主体中使用,内部循环的增量部分将不会被执行,如下所示:

Q_FOREACH(x, cont)
{
    break;
}

然后brk当它跳出内循环时仍然为0,然后将进入外循环的增量部分并将其增加到1,然后!brk将为假,外循环的条件将评估为假,而foreach将停止。

诀窍是要意识到有两个for循环;外层的生命是整个foreach,而内层的生命只持续一个元素。这是一个无限循环,因为它没有条件,但它要么break被它的增量部分所淘汰,要么被break你提供的代码中的 a 所淘汰。这就是为什么x看起来它被分配给“仅一次”但实际上它被分配给外循环的每次迭代。

VS 版本


VS 版本稍微复杂一点,因为它必须解决缺少 GCC 扩展__typeof__和块表达式的问题,而且它为 (6) 编写的 VS 版本没有auto或其他花哨的 C++11 功能。

让我们看一下我们之前使用的扩展示例:

if(0){}else
    for (const QForeachContainerBase &_container_ = qForeachContainerNew(cont); qForeachContainer(&_container_, true ? 0 : qForeachPointer(cont))->condition(); ++qForeachContainer(&_container_, true ? 0 : qForeachPointer(cont))->i)
        for (x = *qForeachContainer(&_container_, true ? 0 : qForeachPointer(cont))->i; qForeachContainer(&_container_, true ? 0 : qForeachPointer(cont))->brk; --qForeachContainer(&_container_, true ? 0 : qForeachPointer(cont))->brk)
        {
            // stuff
        }

if(0){}else是因为 VC++ 6 对for变量的作用域做错了,在循环的初始化部分声明的变量for可以在循环外使用。所以这是一个 VS 错误的解决方法。他们这样做的原因if(0){}else不仅仅是if(0){...}因为你不能else在循环之后添加一个,比如

Q_FOREACH(x, cont)
{
    // do stuff
} else {
    // This code is never called
}

其次,让我们看一下外部的初始化for

const QForeachContainerBase &_container_ = qForeachContainerNew(cont)

的定义QForeachContainerBase是:

struct QForeachContainerBase {};

的定义qForeachContainerNew

template <typename T>
inline QForeachContainer<T>
qForeachContainerNew(const T& t) {
    return QForeachContainer<T>(t);
}

的定义QForeachContainer

template <typename T>
class QForeachContainer : public QForeachContainerBase {
public:
    inline QForeachContainer(const T& t): c(t), brk(0), i(c.begin()), e(c.end()){};
    const T c;
    mutable int brk;
    mutable typename T::const_iterator i, e;
    inline bool condition() const { return (!brk++ && i != e); }
};

因此,为了弥补__typeof__(类似于decltypeC++11 的)的不足,我们必须使用多态性。该qForeachContainerNew函数按值返回 aQForeachContainer<T>但由于temporaries 的生命周期延长,如果我们将其存储在 a 中const QForeachContainer&,我们可以将其生命周期延长到外部结束for(实际上是if因为 VC6 的错误)。我们可以将 a 存储QForeachContainer<T>在 a 中,QForeachContainerBase因为前者是后者的子类,我们必须将其设为引用QForeachContainerBase&而不是值,QForeachContainerBase以避免切片。

然后对于外部的条件for

qForeachContainer(&_container_, true ? 0 : qForeachPointer(cont))->condition(); 

的定义qForeachContainer

inline const QForeachContainer<T> *qForeachContainer(const QForeachContainerBase *base, const T *) {
    return static_cast<const QForeachContainer<T> *>(base);
}

的定义qForeachPointer

template <typename T>
inline T *qForeachPointer(const T &) {
    return 0;
}

这是您可能不知道发生了什么的地方,因为这些功能似乎毫无意义。以下是它们的工作方式以及您需要它们的原因:

我们将 aQForeachContainer<T>存储在对 a 的引用中QForeachContainerBase,但无法将其取回(我们可以看到)。我们必须以某种方式将其转换为正确的类型,这就是两个函数的用武之地。但是我们如何知道将其转换为什么类型呢?

三元运算符的规则x ? y : z是,y并且z必须是同一类型。我们需要知道容器的类型,所以我们使用qForeachPointer函数来做到这一点:

qForeachPointer(cont)

的返回类型qForeachPointerT*,所以我们使用模板类型推导来推导容器的类型。

true ? 0 : qForeachPointer(cont)是为了能够将NULL正确类型的指针传递给它,qForeachContainer这样它就会知道将我们给它的指针转换为什么类型。为什么我们为此使用三元运算符而不仅仅是做qForeachContainer(&_container_, qForeachPointer(cont))?这是为了避免cont多次评估。?:除非条件是 ,否则不会评估第二个(实际上是第三个)操作数false,并且由于条件true本身就是,我们可以在cont不评估它的情况下获得正确的类型。

这样就解决了这个问题,我们使用qForeachContainer转换_container_为正确的类型。电话是:

qForeachContainer(&_container_, true ? 0 : qForeachPointer(cont))

再次定义是

inline const QForeachContainer<T> *qForeachContainer(const QForeachContainerBase *base, const T *) {
    return static_cast<const QForeachContainer<T> *>(base);
}

第二个参数总是NULL因为我们true ? 0总是计算0结果T为):QForeachContainer<T>*for

qForeachContainer(&_container_, true ? 0 : qForeachPointer(cont))->condition()

condition返回:

(!brk++ && i != e)

这与上面的 GCC 版本相同,只是它brk在评估后递增。所以!brk++计算为true然后brk递增到 1。

然后我们进入内部for并开始初始化:

x = *qForeachContainer(&_container_, true ? 0 : qForeachPointer(cont))->i

这只是将变量设置为迭代器i指向的内容。

那么条件:

qForeachContainer(&_container_, true ? 0 : qForeachPointer(cont))->brk

既然brk是1,就进入循环体,也就是我们的注释:

// stuff

然后输入增量:

--qForeachContainer(&_container_, true ? 0 : qForeachPointer(cont))->brk

brk回到 0。然后再次检查条件:

qForeachContainer(&_container_, true ? 0 : qForeachPointer(cont))->brk

Andbrk为 0,即false退出循环。我们来到外部的增量部分for

++qForeachContainer(&_container_, true ? 0 : qForeachPointer(cont))->i

并且递增i到下一个元素。然后我们得到条件:

qForeachContainer(&_container_, true ? 0 : qForeachPointer(cont))->condition()

哪个检查brk是 0(它是)并再次将其增加到 1,并且如果 重复该过程i != e

break在客户端代码中的处理方式与 GCC 版本略有不同,因为brk如果我们break在代码中使用它不会递减并且它仍然是 1,并且condition()外部循环将为 false,外部循环将为break.

正如 GManNickG 在评论中所说,这个宏很像 Boost 的BOOST_FOREACH,你可以在这里阅读。这样就搞定了,希望能帮到你。

于 2012-05-09T18:56:04.673 回答