La même considération s'applique que pour les arguments de fonction.
Maxim Egorushkin
3
En fait, cela n'a pas grand-chose à voir avec la plage basée sur. On peut en dire autant de tout auto (const)(&) x = <expr>;.
Matthieu M.
2
@MatthieuM: Cela a beaucoup à voir avec la gamme pour, bien sûr! Considérons un débutant qui voit plusieurs syntaxes et ne peut pas choisir la forme à utiliser. Le point de "Q & A" était d'essayer de faire la lumière et d'expliquer les différences de certains cas (et discuter des cas qui compilent bien mais sont un peu inefficaces en raison de copies profondes inutiles, etc.).
Mr.C64
2
@ Mr.C64: En ce qui me concerne, cela a plus à voir avec auto, en général, qu'avec la gamme basée sur; vous pouvez parfaitement l'utiliser sans aucune base auto! for (int i: v) {}est parfaitement bien. Bien sûr, la plupart des points que vous soulevez dans votre réponse peuvent avoir plus à voir avec le type qu'avec auto... mais d'après la question, il n'est pas clair où se situe le point douloureux. Personnellement, je me battrais pour retirer autode la question; ou peut-être explicite que si vous utilisez autoou nommez explicitement le type, la question se concentre sur la valeur / référence.
Matthieu M.
1
@MatthieuM .: Je suis ouvert à changer le titre ou à éditer la question sous une forme qui pourrait les rendre plus claires ... Encore une fois, mon objectif était de discuter de plusieurs options pour les syntaxes basées sur une plage (montrant le code qui compile mais est inefficace, code qui ne parvient pas à être compilé, etc.) et essayant d'offrir des conseils à quelqu'un (en particulier au niveau débutant) approchant les boucles basées sur la plage C ++ 11.
Mr.C64
Réponses:
390
Commençons à différencier l' observation des éléments du conteneur et leur modification sur place.
Observer les éléments
Prenons un exemple simple:
vector<int> v ={1,3,5,7,9};for(auto x : v)
cout << x <<' ';
Le code ci-dessus imprime les éléments intdans vector:
13579
Considérons maintenant un autre cas, dans lequel les éléments vectoriels ne sont pas seulement de simples entiers, mais des instances d'une classe plus complexe, avec un constructeur de copie personnalisé, etc.
// A sample test class, with custom copy semantics.class X
{public:
X(): m_data(0){}
X(int data): m_data(data){}~X(){}
X(const X& other): m_data(other.m_data){ cout <<"X copy ctor.\n";}
X&operator=(const X& other){
m_data = other.m_data;
cout <<"X copy assign.\n";return*this;}intGet()const{return m_data;}private:int m_data;};
ostream&operator<<(ostream& os,const X& x){
os << x.Get();return os;}
Si nous utilisons la for (auto x : v) {...}syntaxe ci-dessus avec cette nouvelle classe:
vector<X> v ={1,3,5,7,9};
cout <<"\nElements:\n";for(auto x : v){
cout << x <<' ';}
la sortie est quelque chose comme:
[... copy constructor calls forvector<X> initialization ...]Elements:
X copy ctor.1 X copy ctor.3 X copy ctor.5 X copy ctor.7 X copy ctor.9
Comme il peut être lu à partir de la sortie, des appels de constructeur de copie sont effectués pendant les itérations de boucle basées sur la plage.
En effet, nous capturons les éléments du conteneur par valeur
(la auto xpartie dans for (auto x : v)).
Il s'agit d' un code inefficace , par exemple, si ces éléments sont des instances de std::string, des allocations de mémoire peuvent être effectuées, avec des déplacements coûteux vers le gestionnaire de mémoire, etc. Cela est inutile si nous voulons simplement observer les éléments dans un conteneur.
Ainsi, une meilleure syntaxe est disponible: capture par constréférence , c'est const auto&-à- dire :
vector<X> v ={1,3,5,7,9};
cout <<"\nElements:\n";for(constauto& x : v){
cout << x <<' ';}
Sans appel de constructeur de copie parasite (et potentiellement coûteux).
Ainsi, lorsque l' observation des éléments dans un récipient ( par exemple pour l' accès en lecture seule), la syntaxe suivante est très bien pour de simples pas cher-à-copie types, comme int, double, etc .:
for(auto elem : container)
Sinon, la capture par constréférence est meilleure dans le cas général , pour éviter les appels de constructeur de copie inutiles (et potentiellement coûteux):
for(constauto& elem : container)
Modification des éléments du conteneur
Si nous voulons modifier les éléments d'un conteneur à l'aide de la plage for, ce qui précède for (auto elem : container)et les for (const auto& elem : container)
syntaxes sont incorrects.
En fait, dans le premier cas, elemstocke une copie de l'élément d'origine, donc les modifications qui y sont apportées sont juste perdues et ne sont pas stockées de manière persistante dans le conteneur, par exemple:
vector<int> v ={1,3,5,7,9};for(auto x : v)// <-- capture by value (copy)
x *=10;// <-- a local temporary copy ("x") is modified,// *not* the original vector element.for(auto x : v)
cout << x <<' ';
La sortie n'est que la séquence initiale:
13579
Au lieu de cela, une tentative d'utilisation for (const auto& x : v)échoue simplement à la compilation.
g ++ affiche un message d'erreur quelque chose comme ceci:
TestRangeFor.cpp:138:11: error: assignment of read-only reference 'x'
x *=10;^
L'approche correcte dans ce cas est la capture par non- constréférence:
vector<int> v ={1,3,5,7,9};for(auto& x : v)
x *=10;for(auto x : v)
cout << x <<' ';
La sortie est (comme prévu):
1030507090
Cette for (auto& elem : container)syntaxe fonctionne également pour les types plus complexes, par exemple en considérant a vector<string>:
vector<string> v ={"Bob","Jeff","Connie"};// Modify elements in place: use "auto &"for(auto& x : v)
x ="Hi "+ x +"!";// Output elements (*observing* --> use "const auto&")for(constauto& x : v)
cout << x <<' ';
la sortie est:
HiBob!HiJeff!HiConnie!
Le cas particulier des itérateurs proxy
Supposons que nous ayons un vector<bool>, et que nous voulons inverser l'état booléen logique de ses éléments, en utilisant la syntaxe ci-dessus:
vector<bool> v ={true,false,false,true};for(auto& x : v)
x =!x;
Le code ci-dessus ne parvient pas à compiler.
g ++ affiche un message d'erreur similaire à celui-ci:
TestRangeFor.cpp:168:20: error: invalid initialization of non-const reference of
type 'std::_Bit_reference&' from an rvalue of type 'std::_Bit_iterator::referen
ce {aka std::_Bit_reference}'for(auto& x : v)^
Le problème est que std::vectorgabarit est spécialisé pour bool, avec une mise en oeuvre que des packs les bools à optimiser l' espace (chaque valeur booléenne est stockée dans un bit, huit bits « booléen » à un octet).
Pour cette raison (puisqu'il n'est pas possible de renvoyer une référence à un seul bit),
vector<bool>utilise un modèle dit "itérateur proxy" . Un "itérateur proxy" est un itérateur qui, lorsqu'il est déréférencé, ne donne pas un objet ordinaire bool &, mais renvoie à la place (par valeur) un objet temporaire , qui est une classe proxy convertible enbool . (Voir également cette question et les réponses associées ici sur StackOverflow.)
Pour modifier en place les éléments de vector<bool>, un nouveau type de syntaxe (utilisant auto&&) doit être utilisé:
for(auto&& x : v)
x =!x;
Le code suivant fonctionne correctement:
vector<bool> v ={true,false,false,true};// Invert boolean statusfor(auto&& x : v)// <-- note use of "auto&&" for proxy iterators
x =!x;// Print new element values
cout << boolalpha;for(constauto& x : v)
cout << x <<' ';
et sorties:
falsetruetruefalse
Notez que la for (auto&& elem : container)syntaxe fonctionne également dans les autres cas d'itérateurs ordinaires (non proxy) (par exemple pour a vector<int>ou a vector<string>).
(En remarque, la syntaxe "d'observation" susmentionnée for (const auto& elem : container)fonctionne également pour le cas de l'itérateur proxy.)
Résumé
La discussion ci-dessus peut être résumée dans les lignes directrices suivantes:
Pour observer les éléments, utilisez la syntaxe suivante:
for(constauto& elem : container)// capture by const reference
Si les objets sont bon marché à copier (comme ints, doubles, etc.), il est possible d'utiliser une forme légèrement simplifiée:
for(auto elem : container)// capture by value
Pour modifier les éléments en place, utilisez:
for(auto& elem : container)// capture by (non-const) reference
Si le conteneur utilise des "itérateurs proxy" (comme std::vector<bool>), utilisez:
for(auto&& elem : container)// capture by &&
Bien sûr, s'il est nécessaire de faire une copie locale de l'élément à l'intérieur du corps de la boucle, la capture par value ( for (auto elem : container)) est un bon choix.
Notes supplémentaires sur le code générique
Dans le code générique , puisque nous ne pouvons pas faire d'hypothèses sur le type Tbon marché à copier, en mode observation , il est sûr de toujours l'utiliser for (const auto& elem : container).
(Cela ne déclenchera pas de copies inutiles potentiellement coûteuses, fonctionnera très bien également pour les types bon marché comme int, et également pour les conteneurs utilisant des itérateurs proxy, comme std::vector<bool>.)
De plus, en mode modification , si nous voulons que le code générique fonctionne également dans le cas des proxy-itérateurs, la meilleure option est for (auto&& elem : container).
(Cela fonctionnera très bien également pour les conteneurs utilisant des itérateurs non proxy ordinaires, comme std::vector<int>ou std::vector<string>.)
Ainsi, dans le code générique , les directives suivantes peuvent être fournies:
Pourquoi ne pas toujours utiliser auto&&? Y a-t-il un const auto&&?
Martin Ba
1
Je suppose que vous manquez le cas où vous avez réellement besoin d'une copie à l'intérieur de la boucle?
juanchopanza
6
"Si le conteneur utilise des" itérateurs proxy "" - et vous savez qu'il utilise des "itérateurs proxy" (ce qui pourrait ne pas être le cas dans le code générique). Je pense donc que le meilleur est en effet auto&&, car il couvre auto&tout aussi bien.
Christian Rau
5
Merci, c'était une très bonne "introduction au cours intensif" de la syntaxe et quelques conseils pour la plage basée sur, pour un programmeur C #. +1.
AndrewJacksonZA
17
Il n'y a pas de bonne façon d'utiliser for (auto elem : container)ou for (auto& elem : container)ou for (const auto& elem : container). Vous exprimez simplement ce que vous voulez.
Permettez-moi de développer cela. Prenons une promenade.
for(auto elem : container)...
Celui-ci est du sucre syntaxique pour:
for(auto it = container.begin(); it != container.end();++it){// Observe that this is a copy by value.auto elem =*it;}
Vous pouvez utiliser celui-ci si votre conteneur contient des éléments dont la copie est bon marché.
for(auto& elem : container)...
Celui-ci est du sucre syntaxique pour:
for(auto it = container.begin(); it != container.end();++it){// Now you're directly modifying the elements// because elem is an lvalue referenceauto& elem =*it;}
Utilisez-le lorsque vous souhaitez écrire directement sur les éléments du conteneur, par exemple.
for(constauto& elem : container)...
Celui-ci est du sucre syntaxique pour:
for(auto it = container.begin(); it != container.end();++it){// You just want to read stuff, no modificationconstauto& elem =*it;}
Comme le dit le commentaire, juste pour la lecture. Et c'est tout, tout est "correct" lorsqu'il est utilisé correctement.
J'avais l'intention de donner quelques conseils, avec des exemples de codes à compiler (mais étant inefficaces), ou à ne pas compiler, et à expliquer pourquoi, et à essayer de proposer des solutions.
Mr.C64
2
@ Mr.C64 Oh, je suis désolé - je viens de remarquer que c'est une de ces questions de type FAQ. Je suis nouveau sur ce site. Mes excuses! Votre réponse est excellente, je l'ai votée positivement - mais je voulais également fournir une version plus concise pour ceux qui veulent l'essentiel . J'espère que je n'interviens pas.
1
@ Mr.C64 quel est le problème avec OP répondant également à la question? C'est juste une autre réponse valable.
mfontanini
1
@mfontanini: Il n'y a absolument aucun problème si quelqu'un poste une réponse, encore mieux que la mienne. Le but final est de donner une contribution de qualité à la communauté (en particulier pour les débutants qui peuvent se sentir un peu perdus devant les différentes syntaxes et les différentes options qu'offre C ++).
Mr.C64
4
Le bon moyen est toujours
for(auto&& elem : container)
Cela garantira la préservation de toute la sémantique.
Mais que se passe-t-il si le conteneur ne renvoie que des références modifiables et que je tiens à préciser que je ne souhaite pas les modifier dans la boucle? Ne devrais-je pas ensuite utiliser auto const &pour clarifier mon intention?
RedX
@RedX: Qu'est-ce qu'une "référence modifiable"?
Courses de légèreté en orbite le
2
@RedX: Les références ne sont jamais constet elles ne sont jamais modifiables. Quoi qu'il en soit, ma réponse est oui, je le ferais .
Courses de légèreté en orbite le
4
Bien que cela puisse fonctionner, je pense que ce sont de mauvais conseils par rapport à l'approche plus nuancée et réfléchie donnée par l'excellente et complète réponse de M. C64 donnée ci-dessus. Réduire au plus petit dénominateur commun n'est pas à quoi sert le C ++.
Bien que la motivation initiale de la boucle range-for ait pu être la facilité d'itération sur les éléments d'un conteneur, la syntaxe est suffisamment générique pour être utile même pour des objets qui ne sont pas uniquement des conteneurs.
La condition syntaxique pour la boucle for est cette range_expressionprise en charge begin()et en end()tant que fonctions - soit en tant que fonctions membres du type auquel elle est évaluée, soit en tant que fonctions non membres qui prennent une instance du type.
À titre d'exemple artificiel, on peut générer une plage de nombres et parcourir la plage en utilisant la classe suivante.
auto (const)(&) x = <expr>;
.auto
, en général, qu'avec la gamme basée sur; vous pouvez parfaitement l'utiliser sans aucune baseauto
!for (int i: v) {}
est parfaitement bien. Bien sûr, la plupart des points que vous soulevez dans votre réponse peuvent avoir plus à voir avec le type qu'avecauto
... mais d'après la question, il n'est pas clair où se situe le point douloureux. Personnellement, je me battrais pour retirerauto
de la question; ou peut-être explicite que si vous utilisezauto
ou nommez explicitement le type, la question se concentre sur la valeur / référence.Réponses:
Commençons à différencier l' observation des éléments du conteneur et leur modification sur place.
Observer les éléments
Prenons un exemple simple:
Le code ci-dessus imprime les éléments
int
dansvector
:Considérons maintenant un autre cas, dans lequel les éléments vectoriels ne sont pas seulement de simples entiers, mais des instances d'une classe plus complexe, avec un constructeur de copie personnalisé, etc.
Si nous utilisons la
for (auto x : v) {...}
syntaxe ci-dessus avec cette nouvelle classe:la sortie est quelque chose comme:
Comme il peut être lu à partir de la sortie, des appels de constructeur de copie sont effectués pendant les itérations de boucle basées sur la plage.
En effet, nous capturons les éléments du conteneur par valeur (la
auto x
partie dansfor (auto x : v)
).Il s'agit d' un code inefficace , par exemple, si ces éléments sont des instances de
std::string
, des allocations de mémoire peuvent être effectuées, avec des déplacements coûteux vers le gestionnaire de mémoire, etc. Cela est inutile si nous voulons simplement observer les éléments dans un conteneur.Ainsi, une meilleure syntaxe est disponible: capture par
const
référence , c'estconst auto&
-à- dire :Maintenant, la sortie est:
Sans appel de constructeur de copie parasite (et potentiellement coûteux).
Ainsi, lorsque l' observation des éléments dans un récipient ( par exemple pour l' accès en lecture seule), la syntaxe suivante est très bien pour de simples pas cher-à-copie types, comme
int
,double
, etc .:Sinon, la capture par
const
référence est meilleure dans le cas général , pour éviter les appels de constructeur de copie inutiles (et potentiellement coûteux):Modification des éléments du conteneur
Si nous voulons modifier les éléments d'un conteneur à l'aide de la plage
for
, ce qui précèdefor (auto elem : container)
et lesfor (const auto& elem : container)
syntaxes sont incorrects.En fait, dans le premier cas,
elem
stocke une copie de l'élément d'origine, donc les modifications qui y sont apportées sont juste perdues et ne sont pas stockées de manière persistante dans le conteneur, par exemple:La sortie n'est que la séquence initiale:
Au lieu de cela, une tentative d'utilisation
for (const auto& x : v)
échoue simplement à la compilation.g ++ affiche un message d'erreur quelque chose comme ceci:
L'approche correcte dans ce cas est la capture par non-
const
référence:La sortie est (comme prévu):
Cette
for (auto& elem : container)
syntaxe fonctionne également pour les types plus complexes, par exemple en considérant avector<string>
:la sortie est:
Le cas particulier des itérateurs proxy
Supposons que nous ayons un
vector<bool>
, et que nous voulons inverser l'état booléen logique de ses éléments, en utilisant la syntaxe ci-dessus:Le code ci-dessus ne parvient pas à compiler.
g ++ affiche un message d'erreur similaire à celui-ci:
Le problème est que
std::vector
gabarit est spécialisé pourbool
, avec une mise en oeuvre que des packs lesbool
s à optimiser l' espace (chaque valeur booléenne est stockée dans un bit, huit bits « booléen » à un octet).Pour cette raison (puisqu'il n'est pas possible de renvoyer une référence à un seul bit),
vector<bool>
utilise un modèle dit "itérateur proxy" . Un "itérateur proxy" est un itérateur qui, lorsqu'il est déréférencé, ne donne pas un objet ordinairebool &
, mais renvoie à la place (par valeur) un objet temporaire , qui est une classe proxy convertible enbool
. (Voir également cette question et les réponses associées ici sur StackOverflow.)Pour modifier en place les éléments de
vector<bool>
, un nouveau type de syntaxe (utilisantauto&&
) doit être utilisé:Le code suivant fonctionne correctement:
et sorties:
Notez que la
for (auto&& elem : container)
syntaxe fonctionne également dans les autres cas d'itérateurs ordinaires (non proxy) (par exemple pour avector<int>
ou avector<string>
).(En remarque, la syntaxe "d'observation" susmentionnée
for (const auto& elem : container)
fonctionne également pour le cas de l'itérateur proxy.)Résumé
La discussion ci-dessus peut être résumée dans les lignes directrices suivantes:
Pour observer les éléments, utilisez la syntaxe suivante:
Si les objets sont bon marché à copier (comme
int
s,double
s, etc.), il est possible d'utiliser une forme légèrement simplifiée:Pour modifier les éléments en place, utilisez:
Si le conteneur utilise des "itérateurs proxy" (comme
std::vector<bool>
), utilisez:Bien sûr, s'il est nécessaire de faire une copie locale de l'élément à l'intérieur du corps de la boucle, la capture par value (
for (auto elem : container)
) est un bon choix.Notes supplémentaires sur le code générique
Dans le code générique , puisque nous ne pouvons pas faire d'hypothèses sur le type
T
bon marché à copier, en mode observation , il est sûr de toujours l'utiliserfor (const auto& elem : container)
.(Cela ne déclenchera pas de copies inutiles potentiellement coûteuses, fonctionnera très bien également pour les types bon marché comme
int
, et également pour les conteneurs utilisant des itérateurs proxy, commestd::vector<bool>
.)De plus, en mode modification , si nous voulons que le code générique fonctionne également dans le cas des proxy-itérateurs, la meilleure option est
for (auto&& elem : container)
.(Cela fonctionnera très bien également pour les conteneurs utilisant des itérateurs non proxy ordinaires, comme
std::vector<int>
oustd::vector<string>
.)Ainsi, dans le code générique , les directives suivantes peuvent être fournies:
Pour observer les éléments, utilisez:
Pour modifier les éléments en place, utilisez:
la source
auto&&
? Y a-t-il unconst auto&&
?auto&&
, car il couvreauto&
tout aussi bien.Il n'y a pas de bonne façon d'utiliser
for (auto elem : container)
oufor (auto& elem : container)
oufor (const auto& elem : container)
. Vous exprimez simplement ce que vous voulez.Permettez-moi de développer cela. Prenons une promenade.
Celui-ci est du sucre syntaxique pour:
Vous pouvez utiliser celui-ci si votre conteneur contient des éléments dont la copie est bon marché.
Celui-ci est du sucre syntaxique pour:
Utilisez-le lorsque vous souhaitez écrire directement sur les éléments du conteneur, par exemple.
Celui-ci est du sucre syntaxique pour:
Comme le dit le commentaire, juste pour la lecture. Et c'est tout, tout est "correct" lorsqu'il est utilisé correctement.
la source
Le bon moyen est toujours
Cela garantira la préservation de toute la sémantique.
la source
auto const &
pour clarifier mon intention?const
et elles ne sont jamais modifiables. Quoi qu'il en soit, ma réponse est oui, je le ferais .Bien que la motivation initiale de la boucle range-for ait pu être la facilité d'itération sur les éléments d'un conteneur, la syntaxe est suffisamment générique pour être utile même pour des objets qui ne sont pas uniquement des conteneurs.
La condition syntaxique pour la boucle for est cette
range_expression
prise en chargebegin()
et enend()
tant que fonctions - soit en tant que fonctions membres du type auquel elle est évaluée, soit en tant que fonctions non membres qui prennent une instance du type.À titre d'exemple artificiel, on peut générer une plage de nombres et parcourir la plage en utilisant la classe suivante.
Avec la
main
fonction suivante ,on obtiendrait la sortie suivante.
la source