web-dev-qa-db-fra.com

Comment itérer sur des variables non const 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 se compile pas car les valeurs de initializer_list{a, b} sont considérés comme const Obj&, et ne peut pas être lié à la référence non constante obj.

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

41
tmlen

Cela ne fonctionne pas car dans {a,b} vous faites une copie de a et b. Une solution possible serait de faire de la variable de boucle un pointeur, en prenant les adresses de a et 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, car cela pourrait éviter les conversions implicites silencieuses

51
francesco

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

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

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

mais remarquera bientôt que les valeurs réelles de i dans les objets a et b n'ont pas changé. La raison en est que lorsque vous utilisez auto ici, le type de la variable de boucle obj deviendra Obj, donc vous êtes simplement en boucle des copies de a et b.

La manière réelle de résoudre ce problème est que vous pouvez utiliser std::ref (défini dans l'en-tête <functional> ), pour créer les éléments dans la liste des initialiseurs doit être de type std::reference_wrapper<Obj> . Ceci est implicitement convertible en Obj&, Vous pouvez donc le conserver comme type de variable de 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';
}

Production:

123
123

Une autre façon de procéder est de faire en sorte que la boucle utilise const auto& Et std::reference_wrapper<T>::get . Nous pouvons utiliser une référence constante ici, car le reference_wrapper N'est pas modifié, juste la valeur qu'il encapsule:

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

mais je pense que, parce que l'utilisation de auto force ici l'utilisation de .get(), c'est assez lourd et la première méthode est le moyen préférable de résoudre ce problème.


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

44
ruohola

Si la copie de a et 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 déplacement.

5
Jacob Manaker