41 votes

Comment itérer sur des variables non constantes en C++ ?

#include <initializer_list>

struct Obj {
    int i;
};

Obj a, b;

int main() {
    for(Obj& obj : {a, b}) {
        obj.i = 123;   
    }
}

Ce code ne compile pas parce que les valeurs de l'option initializer_list {a, b} sont considérés comme const Obj& et ne peut être lié à la référence non-const. obj .

Existe-t-il un moyen simple de faire fonctionner une construction similaire, c'est-à-dire d'itérer sur des valeurs qui se trouvent dans des variables différentes, comme par exemple a y b ici.

51voto

francesco Points 2163

Cela ne fonctionne pas car dans {a,b} vous faites une copie de a y b . Une solution possible serait de faire de la variable de la boucle un pointeur, prenant les adresses de a y b :

#include <initializer_list>

struct Obj {
    int i;
};

Obj a, b;

int main() {
    for(auto obj : {&a, &b}) {
        obj->i = 123;   
    }
}

Voir en direct

Remarque : il est généralement préférable d'utiliser auto comme il pourrait éviter les conversions implicites silencieuses

45voto

ruohola Points 15945

La raison pour laquelle cela ne fonctionne pas est que les éléments sous-jacents de la std::initializer_list sont copiés à partir de a y b et sont de type const Obj Vous essayez donc essentiellement de lier une valeur constante à une référence mutable.

On peut essayer de résoudre ce problème en utilisant :

for (auto obj : {a, b}) {
    obj.i = 123;
}

mais on s'apercevrait vite que les valeurs réelles de i dans les objets a y b n'a pas changé. La raison en est que lorsque l'on utilise auto ici, le type de la variable de la boucle obj deviendra Obj donc vous ne faites que boucler sur des copies de a y b .

La façon dont cela devrait être corrigé est que vous pouvez utiliser std::ref (défini dans le <functional> ), pour que les éléments de la liste des initialisateurs soient de type std::reference_wrapper<Obj> . Ce qui est implicitement convertible en Obj& Vous pouvez donc le garder comme type de la variable de la boucle :

#include <functional>
#include <initializer_list>
#include <iostream>

struct Obj {
    int i;
};

Obj a, b;

int main()
{
    for (Obj& obj : {std::ref(a), std::ref(b)}) { 
        obj.i = 123;
    }
    std::cout << a.i << '\n';
    std::cout << b.i << '\n';
}

Sortie :

123
123

Une autre façon de procéder serait de faire en sorte que la boucle utilise const auto& y std::reference_wrapper<T>::get . Nous pouvons utiliser une référence constante ici, parce que le reference_wrapper n'est pas modifié, seule la valeur qu'il enveloppe l'est :

for (const auto& obj : {std::ref(a), std::ref(b)}) { 
    obj.get().i = 123;
}

mais je pense que, parce que l'utilisation auto force ici l'utilisation de .get() Cette méthode est assez lourde et il est préférable d'utiliser la première méthode pour résoudre ce problème.


Il pourrait sembler plus simple de faire cela en utilisant des pointeurs bruts dans la boucle comme @francesco l'a fait dans sa réponse, mais j'ai l'habitude d'éviter les pointeurs bruts autant que possible, et dans ce cas, je crois simplement que l'utilisation de références rend le code plus clair et plus propre.

5voto

Jacob Manaker Points 185

Si la copie a y b est le comportement souhaité, vous pouvez utiliser un tableau temporaire plutôt qu'une liste d'initialisation :

#include <initializer_list>

struct Obj {
    int i;
} a, b;

int main() {
    typedef Obj obj_arr[];
    for(auto &obj : obj_arr{a, b}) {
        obj.i = 123;   
    }
}

Cela fonctionne même si Obj n'a qu'un constructeur de mouvement.

-3voto

Matt McNabb Points 14273

C'est un peu laid, mais vous pouvez le faire depuis C++17 :

#define APPLY_TUPLE(func, t) std::apply( [](auto&&... e) { ( func(e), ...); }, (t))

static void f(Obj& x)
{
    x.i = 123;
}

int main() 
{
    APPLY_TUPLE(f, std::tie(a, b));
}

Cela fonctionnerait même si les objets ne sont pas tous du même type, en faisant en sorte que f un ensemble de surcharges. Vous pouvez également avoir f est un lambda local (éventuellement surchargé). Lien vers l'inspiration .

El std::tie évite le problème dans le code original car il génère un tuple de références. Malheureusement, std::begin n'est pas défini pour les tuples dont tous les membres sont du même type (ce serait bien !), nous ne pouvons donc pas utiliser la boucle for basée sur les rangs. Mais std::apply est la prochaine couche de généralisation.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X