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

lee*_*mes 35 c++ macros foreach qt

在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;}))
Run Code Online (Sandbox Code Playgroud)

...使用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;
};
Run Code Online (Sandbox Code Playgroud)

在容器Q_FOREACH宏必须是一类T至少必须提供T::const_iterator类型,T.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)
Run Code Online (Sandbox Code Playgroud)

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

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

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

Set*_*gie 74

GCC版本


海湾合作委员会非常简单.首先,它使用如下:

Q_FOREACH(x, cont)
{
    // do stuff
}
Run Code Online (Sandbox Code Playgroud)

这将扩展到

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
    }
Run Code Online (Sandbox Code Playgroud)

所以首先:

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

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

!_container_.brk && _container_.i != _container_.e
Run Code Online (Sandbox Code Playgroud)

brk是零所以!brk是真的,并且假设容器有任何元素i(当前元素)不等于e(最后一个元素).

然后for输入该外部的主体,即:

for (variable = *_container_.i;; __extension__ ({--_container_.brk; break;}))
{
    // do stuff
}
Run Code Online (Sandbox Code Playgroud)

因此x设置为*_container_.i迭代所在的当前元素,并且没有条件,因此可能这个循环将永远持续.然后输入循环体,这是我们的代码,它只是一个注释,所以它什么都不做.

然后输入内循环的增量部分,这很有趣:

__extension__ ({--_container_.brk; break;})
Run Code Online (Sandbox Code Playgroud)

它递减brk,现在为-1,并且突破循环(__extension__使得GCC不会发出使用GCC扩展的警告,就像您现在所知道的那样).

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

__extension__  ({ ++_container_.brk; ++_container_.i; })
Run Code Online (Sandbox Code Playgroud)

brk再次递增并再次使其为0,然后i递增,以便我们到达下一个元素.检查条件,因为brk现在为0并且i可能不相等e(如果我们有更多元素),则重复该过程.

为什么我们减少然后再增加brk呢?原因是因为我们break在代码体中使用了内循环的增量部分,如下所示:

Q_FOREACH(x, cont)
{
    break;
}
Run Code Online (Sandbox Code Playgroud)

然后brk当它突破内部循环时仍然是0,然后输入外部循环的增量部分并将其增加到1,然后!brk将为false并且外部循环的条件将评估为false,并且foreach将停.

诀窍是要意识到有两个for循环; 外在的一生就是整个前行,但内在的一生只持续一个元素.这是一个无限循环,因为它不具备条件,但它要么是break编出来的由它的增量部分,或由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
        }
Run Code Online (Sandbox Code Playgroud)

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
}
Run Code Online (Sandbox Code Playgroud)

其次,让我们来看看外部的初始化for:

const QForeachContainerBase &_container_ = qForeachContainerNew(cont)
Run Code Online (Sandbox Code Playgroud)

定义QForeachContainerBase是:

struct QForeachContainerBase {};
Run Code Online (Sandbox Code Playgroud)

而定义qForeachContainerNew

template <typename T>
inline QForeachContainer<T>
qForeachContainerNew(const T& t) {
    return QForeachContainer<T>(t);
}
Run Code Online (Sandbox Code Playgroud)

而定义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); }
};
Run Code Online (Sandbox Code Playgroud)

因此,为了弥补缺乏__typeof__(类似于decltypeC++ 11),我们必须使用多态.该qForeachContainerNew函数返回一个QForeachContainer<T>by值,但是由于temporaries的生命周期延长,如果我们将它存储在a中const QForeachContainer&,我们可以延长它的生命周期直到外部结束for(实际上是if因为VC6的bug).我们可以QForeachContainer<T>在a中存储a ,QForeachContainerBase因为前者是后者的子类,我们必须使它成为一个引用,QForeachContainerBase&而不是像QForeachContainerBase避免切片这样的值.

然后对于外部的条件for:

qForeachContainer(&_container_, true ? 0 : qForeachPointer(cont))->condition(); 
Run Code Online (Sandbox Code Playgroud)

定义qForeachContainer

inline const QForeachContainer<T> *qForeachContainer(const QForeachContainerBase *base, const T *) {
    return static_cast<const QForeachContainer<T> *>(base);
}
Run Code Online (Sandbox Code Playgroud)

而定义qForeachPointer

template <typename T>
inline T *qForeachPointer(const T &) {
    return 0;
}
Run Code Online (Sandbox Code Playgroud)

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

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

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

qForeachPointer(cont)
Run Code Online (Sandbox Code Playgroud)

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

true ? 0 : qForeachPointer(cont)是能够传递一个NULL正确类型的指针,qForeachContainer所以它会知道什么类型的投我们给它的指针.为什么我们使用三元运算符代替qForeachContainer(&_container_, qForeachPointer(cont))呢?这是为了避免cont多次评估.?:除非条件是false,否则不会计算第二个(实际上是第三个)操作数,并且由于条件true本身,我们可以在不对其进行评估的情况下获得正确的类型cont.

所以这解决了这个问题,我们使用它qForeachContainer_container_转换为正确的类型.电话是:

qForeachContainer(&_container_, true ? 0 : qForeachPointer(cont))
Run Code Online (Sandbox Code Playgroud)

而且定义是

inline const QForeachContainer<T> *qForeachContainer(const QForeachContainerBase *base, const T *) {
    return static_cast<const QForeachContainer<T> *>(base);
}
Run Code Online (Sandbox Code Playgroud)

第二个参数将始终是NULL因为我们true ? 0总是求值0,并且我们使用qForeachPointer推导出类型T,并使用它来将第一个参数转换为a,QForeachContainer<T>*因此我们可以使用其成员函数/变量和条件(仍然在外部for):

qForeachContainer(&_container_, true ? 0 : qForeachPointer(cont))->condition()
Run Code Online (Sandbox Code Playgroud)

condition返回:

(!brk++ && i != e)
Run Code Online (Sandbox Code Playgroud)

这与上面的GCC版本相同,只是它brk在评估之后递增.因此!brk++评估为true,然后brk递增为1.

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

x = *qForeachContainer(&_container_, true ? 0 : qForeachPointer(cont))->i
Run Code Online (Sandbox Code Playgroud)

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

然后条件:

qForeachContainer(&_container_, true ? 0 : qForeachPointer(cont))->brk
Run Code Online (Sandbox Code Playgroud)

brk1开始,输入循环体,这是我们的评论:

// stuff
Run Code Online (Sandbox Code Playgroud)

然后输入增量:

--qForeachContainer(&_container_, true ? 0 : qForeachPointer(cont))->brk
Run Code Online (Sandbox Code Playgroud)

减少brk到0.然后再次检查条件:

qForeachContainer(&_container_, true ? 0 : qForeachPointer(cont))->brk
Run Code Online (Sandbox Code Playgroud)

并且brk是0,false并且循环退出.我们来到外部的增量部分for:

++qForeachContainer(&_container_, true ? 0 : qForeachPointer(cont))->i
Run Code Online (Sandbox Code Playgroud)

然后i增加到下一个元素.然后我们达到了这个条件:

qForeachContainer(&_container_, true ? 0 : qForeachPointer(cont))->condition()
Run Code Online (Sandbox Code Playgroud)

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

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

正如GManNickG在评论中所说,这个宏很像Boost BOOST_FOREACH,你可以在这里阅读.所以你有它,希望能帮到你.

  • @leemes抱歉这对VS版本来说花了这么长时间,我正在研究它.我很难把所有事情都记在脑后,这个答案真的很长. (3认同)
  • @leemes:所有这些机器都起源于(或受其启发)Boost的'FOREACH`宏,你可以详细阅读[这里](http://www.artima.com/cppsource/foreach.html). (3认同)
  • @leemes好的,你去吧:)如果你需要澄清,请告诉我. (2认同)
  • @leemes是的,我读到它是用于确定范围,但我的猜测是为了确定_what_,我认为这是初始化变量(与身体中的所有变量或其他任何变量相对). (2认同)