Comment la macro FOREACH (= foreach) Q fonctionne-t-elle et pourquoi est-elle si complexe?

dans Qt, il y a une boucle foreach qui est implémentée en utilisant des macros ( Q_FOREACH ). Il existe différentes implémentations, selon le compilateur.

la définition de pour GCC est la suivante:

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

... en utilisant la classe helper QForeachContainer qui est défini comme suit:

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

le conteneur dans une macro Q_FOREACH doit être une classe T qui au moins doit fournir un type T::const_iterator , un T.begin() et une méthode T.end() , comme le font tous les conteneurs STL ainsi que la plupart des conteneurs QT comme QList , QVector , QMap , QHash , ...

Ma question est maintenant: Comment cette macro?

une chose semble vraiment étrange: la variable n'apparaît qu'une seule fois dans la définition macro. Ainsi par exemple foreach(QString item, list) a un QString item = mais aucun item = après à tout moment... Comment la variable item peut-elle alors être changée à chaque étape?

encore plus confuse est la définition suivante de Q_FOREACH pour le compilateur 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)

pourquoi true : 0 ? ... ? Ça n'est pas toujours évalué en 0 ? Est-ce que l'appel de fonction qForeachPointer(container) est exécuté même si la condition avant ? est vraie?

Et pourquoi avons-nous besoin de deux pour-les boucles?

ce serait cool si quelqu'un pouvait rendre les choses un peu plus claires pour moi!

32
demandé sur leemes 2012-05-09 22:41:21

1 réponses

la version GCC


le GCC est vraiment très simple. Tout d'abord, il est utilisé comme ceci:

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

et qui sera étendu à

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
    }

Alors tout d'abord:

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

c'est la boucle réelle for . Il met en place un QForeachContainer pour aider avec l'itération. La variable brk est intitialisée à 0. Puis la condition est vérifiée:

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

brk est égal à zéro, donc !brk est vrai, et on peut supposer que si le conteneur a des éléments i (l'élément courant) n'est pas égal à e (le dernier élément) pour le moment.

puis le corps de cette extérieure for est entré, qui est:

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

So x est défini à *_container_.i qui est l'élément courant sur lequel l'itération est effectuée, et il n'y a aucune condition donc probablement cette boucle va continuer pour toujours. Puis le corps de la boucle est entré, ce qui est notre code, et c'est juste un commentaire pour qu'il ne fasse rien.

puis la partie incrémentielle de la boucle intérieure est entrée, ce qui est intéressant:

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

Il décrémente brk de sorte que maintenant, -1, et les pauses de la boucle (avec __extension__ , ce qui rend GCC émettent pas de mises en garde pour l'utilisation de GCC extensions, comme vous maintenant savoir.)

puis la partie incrémentielle de la boucle extérieure est entrée:

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

qui augmente brk à nouveau et le rend 0 à nouveau, et puis i est incrémenté de sorte que nous arrivons à l'élément suivant. La condition est vérifiée, et puisque brk est maintenant 0 et i n'égale probablement pas e pourtant (si nous avons plus d'éléments) le processus est répété.

Pourquoi avons-nous décrété et puis incrémenter brk comme ça? La raison en est que la partie incrémentielle de la boucle interne ne sera pas exécutée si nous avons utilisé break dans le corps de notre code, comme ceci:

Q_FOREACH(x, cont)
{
    break;
}

puis brk serait encore 0 quand il sort de la boucle intérieure, et alors la partie incrémentielle de la boucle extérieure serait entré et incrémenter à 1, puis !brk serait faux et l'état de la boucle extérieure évaluerait à faux, et l'avant - pourrait s'arrêter.

le truc est de réaliser qu'il y a deux for Boucles; la durée de vie de l'extérieur est l'avant-main entière, mais l'intérieur ne dure que pour un élément. Il serait une boucle infinie puisqu'il n'a pas de condition, mais il est soit break ed de par sa partie incrémentielle, ou par un break dans le code que vous lui fournissez. C'est pourquoi x semble comme il est assigné à "une seule fois" mais en fait il est assigné à chaque itération de la boucle externe.

the VS version


la version VS est un peu plus compliquée parce qu'elle doit contourner l'absence de L'extension GCC __typeof__ et des expressions-blocs, et la version de VS qu'elle a été écrite pour (6) n'avait pas auto ou d'autres fonctionnalités fancy C++11.

regardons un exemple d'expansion pour ce que nous avons utilisé plus tôt:

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
        }

le if(0){}else est parce que VC++ 6 a fait le cadrage de for variables fausses et une variable déclarée dans la partie d'initialisation d'une boucle for pourrait être utilisée en dehors de la boucle. Donc c'est un contournement pour un bug VS. La raison pour laquelle ils ont fait if(0){}else au lieu de juste if(0){...} est donc que vous ne pouvez pas ajouter un else après la boucle, comme

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

deuxièmement, regardons l'initialisation de l'extérieur for :

const QForeachContainerBase &_container_ = qForeachContainerNew(cont)

la définition de QForeachContainerBase est:

struct QForeachContainerBase {};

et la définition de qForeachContainerNew est

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

et la définition de QForeachContainer est

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

donc pour compenser le manque de __typeof__ (qui analogue au decltype de C++11) nous devons utiliser le polymorphisme. La fonction qForeachContainerNew renvoie un QForeachContainer<T> en valeur mais en raison de extension de durée de vie des temporaires15191800920", si nous le stockons dans un const QForeachContainer& , nous pouvons prolonger sa durée de vie jusqu'à la fin de l'extérieur for (en fait le if en raison du bug de VC6). Nous pouvons stocker un QForeachContainer<T> dans un QForeachContainerBase parce que le premier est une sous-classe du second, et nous devons en faire un renvoi comme QForeachContainerBase& au lieu d'une valeur comme QForeachContainerBase pour éviter de trancher.

Puis pour l'état de l'extérieur for :

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

la définition de qForeachContainer est

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

et la définition de qForeachPointer est

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

c'est là que vous pourriez ne pas être au courant de ce qui se passe puisque ces fonctions semblent un peu inutiles. Voici comment ils fonctionnent et pourquoi vous en avez besoin:

nous avons un QForeachContainer<T> stocké dans une référence à un QForeachContainerBase sans aucun moyen de le retirer (que nous pouvons voir). Nous avons la lancer au bon type en quelque sorte, et c'est là que les deux fonctions sont en. Mais comment savoir à quel type le lancer?

une règle de l'opérateur ternaire x ? y : z est que y et z doivent être du même type. Nous avons besoin de connaître le type de conteneur, donc nous utilisons la fonction qForeachPointer pour faire cela:

qForeachPointer(cont)

le le type de déclaration qForeachPointer est T* , donc nous utilisons la déduction de type de modèle pour déduire le type du conteneur.

le true ? 0 : qForeachPointer(cont) est d'être en mesure de passer un NULL pointeur du type droit à qForeachContainer de sorte qu'il saura quel type de mouler le pointeur que nous lui donnons. Pourquoi utiliser l'opérateur ternaire pour cela, au lieu de simplement faire qForeachContainer(&_container_, qForeachPointer(cont)) ? C'est pour éviter d'évaluer cont plusieurs fois. Le deuxième (en fait troisième) opérande pour ?: n'est pas évalué à moins que la condition soit false , et puisque la condition est true elle-même, nous pouvons obtenir le bon type de cont sans l'évaluer.

donc ça résout ça, et on utilise qForeachContainer pour lancer _container_ vers le bon type. L'appel est:

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

et encore une fois la définition est

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

le second paramètre sera toujours NULL parce que nous faisons true ? 0 qui évalue toujours à 0 , et nous utilisons qForeachPointer pour déduire le type T , et utiliser cela pour lancer le premier argument à un QForeachContainer<T>* de sorte que nous pouvons utiliser ses fonctions/variables membres avec la condition (toujours dans l'extérieur for ):

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

et condition retourne:

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

qui est le même que la version GCC ci-dessus sauf qu'il incréments brk après évaluation. Ainsi !brk++ est évalué à true et puis brk est augmenté à 1.

ensuite nous entrons dans le for intérieur et commençons par l'initialisation:

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

qui ne définit la variable qu'à ce que l'itérateur i pointe.

puis la condition:

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

depuis brk est 1, le corps de la boucle est entrée, ce qui est notre commentaire:

// stuff

puis l'incrément est entré:

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

qui décrète brk retour à 0. Puis la condition est vérifiée à nouveau:

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

et brk est 0 qui est false et la boucle est sortie. Nous arrivons à la partie d'accroissement de l'extérieur for :

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

et que les incréments i à l'élément suivant. Puis nous arrivons à la condition:

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

qui vérifie que brk est 0 (ce qu'il est) et l'incrémente à 1 encore, et le processus est répété si i != e .

cela traite break en code client seulement un peu différemment que la version GCC, puisque brk ne sera pas décrémenté si nous utilisons break dans notre code et il sera encore 1, et le condition() sera être faux pour la boucle extérieure et la boucle extérieure sera break .

et comme GManNickG a déclaré dans les commentaires, cette macro est un peu comme BOOST_FOREACH de Boost que vous pouvez lire sur ici . Donc là vous l'avez, l'espérance qui vous aide.

71
répondu Seth Carnegie 2017-12-28 10:11:02