Wie funktioniert das Makro Q_FOREACH (= foreach) und warum ist es so komplex?

Lesezeit: 10 Minuten

Benutzer-Avatar
leemes

In Qt gibt es eine foreach Schleife, die mit Makros implementiert wird (Q_FOREACH). Je nach Compiler gibt es unterschiedliche Implementierungen.

Die Definition für GCC ist wie folgt:

#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;}))

… mit Hilfe der Hilfsklasse QForeachContainer die wie folgt definiert ist:

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

Der Behälter in a Q_FOREACH Makro muss eine Klasse sein T die mindestens bieten muss a T::const_iterator Tippe A T.begin() und ein T.end() Methode, wie alle STL-Container sowie die meisten Qt-Container wie QList, QVector, QMap, QHash

Meine Frage ist jetzt: Wie funktioniert dieses Makro?

Eine Sache scheint wirklich seltsam zu sein: Die Variable kommt nur einmal in der Makrodefinition vor. Also zB foreach(QString item, list) hat ein QString item = aber nein item = danach jederzeit… Wie kann die Variable item dann in jedem Schritt geändert werden?

Noch verwirrender ist die folgende Definition von Q_FOREACH für den MS VC++-Compiler:

#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)

Wieso den true : 0 ? ...? Wird das nicht immer ausgewertet 0? Ist der Funktionsaufruf qForeachPointer(container) ausgeführt, auch wenn die Bedingung zuvor ? ist wahr?

Und warum brauchen wir zwei for-Schleifen?

Wäre cool, wenn mir das jemand etwas klarer machen könnte!

Benutzer-Avatar
Seth Carnegie

Die GCC-Version


Der GCC ist wirklich ganz einfach. Zunächst einmal wird es so verwendet:

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

Und das wird erweitert

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
    }

Also erstmal:

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

Das ist das Eigentliche for Schleife. Es richtet ein QForeachContainer um bei der Iteration zu helfen. Das brk Variable wird auf 0 initialisiert. Dann wird die Bedingung getestet:

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

brk ist also null !brk wahr ist, und vermutlich, wenn der Container irgendwelche Elemente hat i (das aktuelle Element) ist nicht gleich e (das letzte Element) noch.

Dann der Körper dieses Äußeren for eingetragen, das heißt:

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

So x ist eingestellt auf *_container_.i Dies ist das aktuelle Element, auf dem sich die Iteration befindet, und es gibt keine Bedingung, also wird diese Schleife vermutlich für immer fortgesetzt. Dann wird der Körper der Schleife eingegeben, das ist unser Code, und es ist nur ein Kommentar, also tut es nichts.

Dann wird der Inkrementteil der inneren Schleife eingegeben, was interessant ist:

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

Es dekrementiert brk das ist also jetzt -1 und bricht aus der Schleife (mit __extension__ wodurch GCC keine Warnungen für die Verwendung von GCC-Erweiterungen ausgibt, wie Sie jetzt wissen).

Dann wird der Inkrementteil der äußeren Schleife eingegeben:

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

die inkrementiert brk wieder und macht es wieder 0, und dann i wird inkrementiert, sodass wir zum nächsten Element gelangen. Der Zustand wird geprüft, und da brk ist jetzt 0 und i vermutlich nicht gleich e doch (wenn wir mehr Elemente haben) wird der Vorgang wiederholt.

Warum haben wir dekrementiert und dann erhöht? brk so wie das? Der Grund dafür ist, dass der Inkrementteil der inneren Schleife nicht ausgeführt wird, wenn wir verwendet haben break im Körper unseres Codes wie folgt:

Q_FOREACH(x, cont)
{
    break;
}

Dann brk wäre immer noch 0, wenn es aus der inneren Schleife ausbricht, und dann würde der Inkrementteil der äußeren Schleife eingegeben werden und es dann auf 1 inkrementieren !brk wäre falsch, und die Bedingung der äußeren Schleife würde als falsch ausgewertet, und foreach würde anhalten.

Der Trick besteht darin, zu erkennen, dass es zwei gibt for Schleifen; das Äußere ist das ganze Leben für jeden, aber das Innere dauert nur für ein Element. Es würde eine Endlosschleife sein, da es keine Bedingung hat, aber es ist eine von beiden breaked aus durch seinen Inkrementteil oder durch a break in dem Code, den Sie angeben. Deshalb x sieht so aus, als wäre es “nur einmal” zugewiesen, aber tatsächlich wird es bei jeder Iteration der äußeren Schleife zugewiesen.

Die VS-Version


Die VS-Version ist etwas komplizierter, da sie das Fehlen der GCC-Erweiterung umgehen muss __typeof__ und Blockausdrücke, und die Version von VS, für die es geschrieben wurde (6), hatte es nicht auto oder andere ausgefallene C++11-Features.

Schauen wir uns eine Beispielerweiterung für das an, was wir zuvor verwendet haben:

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
        }

Das if(0){}else liegt daran, dass VC++ 6 den Umfang von for Variablen falsch und eine im Initialisierungsteil von a deklarierte Variable for Schleife könnte außerhalb der Schleife verwendet werden. Es ist also eine Problemumgehung für einen VS-Fehler. Der Grund, warum sie es getan haben if(0){}else statt nur if(0){...} ist so, dass Sie keine hinzufügen können else nach der Schleife, wie

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

Als zweites schauen wir uns die Initialisierung des Äußeren an for:

const QForeachContainerBase &_container_ = qForeachContainerNew(cont)

Die Definition von QForeachContainerBase ist:

struct QForeachContainerBase {};

Und die Definition von qForeachContainerNew ist

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

Und die Definition von QForeachContainer ist

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

Also um den Mangel auszugleichen __typeof__ (was analog zu der decltype von C++11) müssen wir Polymorphismus verwenden. Das qForeachContainerNew Funktion gibt a zurück QForeachContainer<T> nach Wert, sondern aufgrund der Lebensdauerverlängerung von Provisorien, wenn wir sie in a const QForeachContainer&wir können seine Lebensdauer bis zum Ende des Äußeren verlängern for (eigentlich die if wegen VC6-Fehler). Wir können a speichern QForeachContainer<T> in einem QForeachContainerBase denn ersteres ist eine Unterklasse von letzterem, und wir müssen daraus eine Referenz wie machen QForeachContainerBase& anstelle eines Werts wie QForeachContainerBase um Schnitte zu vermeiden.

Dann zum äußeren Zustand for:

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

Die Definition von qForeachContainer ist

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

Und die Definition von qForeachPointer ist

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

Hier wissen Sie möglicherweise nicht, was vor sich geht, da diese Funktionen irgendwie sinnlos erscheinen. Nun, hier ist, wie sie funktionieren und warum Sie sie brauchen:

Wir haben ein QForeachContainer<T> gespeichert in einem Verweis auf a QForeachContainerBase ohne Möglichkeit, es wieder herauszuholen (das können wir sehen). Wir müssen es irgendwie in den richtigen Typ umwandeln, und hier kommen die beiden Funktionen ins Spiel. Aber woher wissen wir, in welchen Typ wir es umwandeln müssen?

Eine Regel des ternären Operators x ? y : z ist dass y und z müssen vom gleichen Typ sein. Wir müssen den Typ des Containers kennen, also verwenden wir die qForeachPointer Funktion dazu:

qForeachPointer(cont)

Der Rückgabetyp von qForeachPointer ist T*also verwenden wir die Vorlagentypableitung, um den Typ des Containers abzuleiten.

Das true ? 0 : qForeachPointer(cont) besteht darin, a bestehen zu können NULL Zeiger vom richtigen Typ auf qForeachContainer So weiß es, in welchen Typ der Zeiger umgewandelt werden soll, an den wir es übergeben. Warum verwenden wir dafür den ternären Operator, anstatt es einfach zu tun? qForeachContainer(&_container_, qForeachPointer(cont))? Es soll eine Bewertung vermeiden cont viele Male. Der zweite (eigentlich dritte) Operand to ?: wird nicht ausgewertet, es sei denn, die Bedingung ist falseund da ist die Bedingung true selbst können wir die richtige Art von bekommen cont ohne es zu bewerten.

Das löst das also, und wir verwenden qForeachContainer zu gießen _container_ zum richtigen Typ. Der Aufruf lautet:

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

Und wieder ist die Definition

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

Der zweite Parameter ist immer NULL weil wir es tun true ? 0 was immer zu wertet 0und wir verwenden qForeachPointer, um den Typ abzuleiten Tund verwenden Sie das, um das erste Argument in a umzuwandeln QForeachContainer<T>* damit wir seine Member-Funktionen/-Variablen mit der Bedingung verwenden können (immer noch in der äußeren for):

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

Und condition kehrt zurück:

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

das ist das gleiche wie die GCC-Version oben, außer dass es erhöht wird brk nach Auswertung. So !brk++ wertet zu true und dann brk wird auf 1 erhöht.

Dann betreten wir das Innere for und beginne mit der Initialisierung:

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

Was nur die Variable auf den Iterator setzt i zeigt auf.

Dann die Bedingung:

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

Seit brk 1 ist, wird der Körper der Schleife eingegeben, was unser Kommentar ist:

// stuff

Dann wird das Inkrement eingegeben:

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

Das dekrementiert brk wieder auf 0. Dann wird die Bedingung erneut geprüft:

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

Und brk ist 0, was ist false und die Schleife wird verlassen. Wir kommen zum inkrementellen Teil des Äußeren for:

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

Und das erhöht sich i zum nächsten Element. Dann kommen wir zur Bedingung:

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

Was das prüft brk 0 ist (was es ist) und erhöht es wieder auf 1, und der Vorgang wird wiederholt, wenn i != e.

Das handhabt break im Client-Code nur wenig anders als die GCC-Version, da brk wird nicht dekrementiert, wenn wir verwenden break in unserem Code und es wird immer noch 1 sein, und die condition() wird für die äußere Schleife falsch sein und die äußere Schleife wird break.

Und wie GManNickG in den Kommentaren feststellte, ist dieses Makro dem von Boost sehr ähnlich BOOST_FOREACH über die man lesen kann hier. Da haben Sie es also, ich hoffe, das hilft Ihnen weiter.

  • Vielen Dank! Dies erklärte alles für die GCC-Version. Ich interessiere mich immer sehr für die Hacks, die Qt macht, und ich freue mich darauf, einmal die ganze qtglobal.h zu verstehen 🙂 Ich möchte mich noch einmal für Ihre Zeit bedanken.

    – leemes

    9. Mai 2012 um 19:31 Uhr

  • @leemes Entschuldigung, dass dies für die VS-Version so lange dauert, ich arbeite jedoch daran. Es ist schwer, alles im Kopf zu behalten, diese Antwort ist wirklich lang.

    – Seth Carnegie

    9. Mai 2012 um 19:33 Uhr

  • @leemes ok, los geht’s 🙂 Lass es mich wissen, wenn du eine Klärung brauchst.

    – Seth Carnegie

    9. Mai 2012 um 19:53 Uhr


  • @leemes ja, ich habe gelesen, dass es für das Scoping war, aber meine Vermutung war, für das Scoping wasund ich denke, es ist die Initialisierungsvariable (im Gegensatz zu allen Variablen im Körper oder was auch immer).

    – Seth Carnegie

    9. Mai 2012 um 20:00 Uhr

  • @leemes: Alle diese Maschinen stammen von Boost (oder wurden von ihnen inspiriert). FOREACH Makro, über das Sie ausführlich nachlesen können hier.

    – GManNickG

    9. Mai 2012 um 20:32 Uhr


1012280cookie-checkWie funktioniert das Makro Q_FOREACH (= foreach) und warum ist es so komplex?

This website is using cookies to improve the user-friendliness. You agree by using the website further.

Privacy policy