Q_FOREACH(= foreach)宏如何工作以及为什么这么复杂?

问题描述 投票:33回答:1

在Qt中,有一个使用宏(foreach)实现的Q_FOREACH循环。根据编译器的不同,有不同的实现。

海湾合作委员会的定义如下:

#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类型,T.begin()T.end()方法,所有STL容器以及大多数Qt容器如QListQVectorQMapQHash ,. ..

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

有一点似乎很奇怪:变量只在宏定义中出现一次。所以例如foreach(QString item, list)随后都有一个QString item =但没有item = ......那么变量item怎么能在每一步都改变?

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

#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循环?

如果有人能让我对我更清楚的话会很酷!

c++ qt macros foreach
1个回答
72
投票

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(最后一个元素)。

然后输入外部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将为false并且外部循环的条件将评估为false,并且foreach会停止。

诀窍是要意识到有两个for循环;外在的一生就是整个前行,但内在的一生只持续一个元素。它将是一个无限循环,因为它没有条件,但它是由它的增量部分中的breaked,或者是由你提供它的代码中的break。这就是为什么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 bug的解决方法。他们做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__(类似于C ++ 11的decltype)的缺乏,我们必须使用多态性。 qForeachContainerNew函数按值返回QForeachContainer<T>,但由于lifetime extension of temporaries,如果我们将它存储在const QForeachContainer&中,我们可以延长它的寿命直到外部for结束(实际上因为VC6的bug而导致if)。我们可以在QForeachContainer<T>中存储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;
}

这是你可能不知道发生了什么的地方,因为这些功能看起来毫无意义。那么它们是如何工作的以及为什么需要它们:

我们有一个QForeachContainer<T>存储在对QForeachContainerBase的引用中,没有办法让它退出(我们可以看到)。我们必须以某种方式将它转换为正确的类型,这就是两个函数的用武之地。但是我们如何知道将它投射到什么类型?

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

qForeachPointer(cont)

qForeachPointer的返回类型是T*,因此我们使用模板类型推导来推断容器的类型。

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 ? 00,并且我们使用qForeachPointer来推导类型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

并且brk为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版本略有不同,因为如果我们在代码中使用brkbreak将不会递减,并且它仍然是1,并且外部循环的condition()将为false,外部循环将为break

正如GManNickG在评论中所说,这个宏很像Boost的BOOST_FOREACH,你可以阅读有关here的内容。所以你有它,希望能帮到你。

© www.soinside.com 2019 - 2024. All rights reserved.