Quelle est la manière correcte d'utiliser la fonction de plage de C++11 ? for
?
Quelle syntaxe faut-il utiliser ? for (auto elem : container)
, ou for (auto& elem : container)
o for (const auto& elem : container)
? Ou d'autres ?
Quelle est la manière correcte d'utiliser la fonction de plage de C++11 ? for
?
Quelle syntaxe faut-il utiliser ? for (auto elem : container)
, ou for (auto& elem : container)
o for (const auto& elem : container)
? Ou d'autres ?
Pour en observant les éléments, utilisez la syntaxe suivante :
for (const auto& elem : container) // capture by const reference
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 :
for (auto elem : container) // capture by value
Pour modification de les éléments en place, utiliser :
for (auto& elem : container) // capture by (non-const) reference
Si le conteneur utilise "itérateurs proxy" (comme std::vector<bool>
), utiliser :
for (auto&& elem : container) // capture by &&
Bien sûr, s'il y a un besoin de faire un copie locale de l'élément à l'intérieur du corps de la boucle, en capturant par valeur ( for (auto elem : container)
) est un bon choix.
Commençons par faire la différence entre en observant les éléments du conteneur contre modification de les mettre en place.
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 ( int
) dans le vector
:
1 3 5 7 9
Considérons maintenant un autre cas, dans lequel les éléments du vecteur ne sont pas 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;
}
int Get() const
{
return m_data;
}
private:
int m_data;
};
ostream& operator<<(ostream& os, const X& x)
{
os << x.Get();
return os;
}
Si nous utilisons la méthode ci-dessus for (auto x : v) {...}
syntaxe avec cette nouvelle classe :
vector<X> v = {1, 3, 5, 7, 9};
cout << "\nElements:\n";
for (auto x : v)
{
cout << x << ' ';
}
le résultat est quelque chose comme :
[... copy constructor calls for vector<X> initialization ...] Elements: X copy ctor. 1 X copy ctor. 3 X copy ctor. 5 X copy ctor. 7 X copy ctor. 9
Comme on peut le lire sur la sortie, constructeur de copie sont effectués pendant les itérations de la boucle for basée sur l'intervalle.
C'est parce que nous sommes capturer les éléments du conteneur par valeur (le auto x
partie en for (auto x : v)
).
C'est inefficace par exemple, si ces éléments sont des instances de std::string
, des allocations de mémoire de tas peuvent être effectuées, avec des déplacements coûteux vers le gestionnaire de mémoire, etc. Ceci est inutile si nous voulons simplement observez les éléments d'un conteneur.
Une meilleure syntaxe est donc disponible : capture par const
référence c'est-à-dire const auto&
:
vector<X> v = {1, 3, 5, 7, 9};
cout << "\nElements:\n";
for (const auto& x : v)
{
cout << x << ' ';
}
Maintenant la sortie est :
[... copy constructor calls for vector<X> initialization ...] Elements: 1 3 5 7 9
Sans appel intempestif (et potentiellement coûteux) au constructeur de copie.
Alors, quand en observant éléments dans un conteneur (c'est-à-dire pour un accès en lecture seule), la syntaxe suivante convient pour de simples cheap-to-copy comme int
, double
etc :
for (auto elem : container)
Sinon, capturer par const
La référence est meilleure dans le cas général , afin d'éviter les appels inutiles (et potentiellement coûteux) au constructeur de copie :
for (const auto& elem : container)
Si nous voulons modifier les éléments d'un conteneur en utilisant la méthode des plages for
, les for (auto elem : container)
y for (const auto& elem : container)
les syntaxes sont fausses.
En fait, dans le premier cas, elem
stocke un copie de l'élément d'origine, de sorte que les modifications qui lui sont apportées sont simplement perdues et ne sont pas stockées de manière persistante dans le conteneur. 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 est juste la séquence initiale :
1 3 5 7 9
Au lieu de cela, une tentative d'utiliser for (const auto& x : v)
ne parvient pas à compiler.
g++ affiche un message d'erreur semblable à celui-ci :
TestRangeFor.cpp:138:11: error: assignment of read-only reference 'x' x *= 10; ^
Dans ce cas, l'approche correcte consiste à capturer par des moyens autres que l'électricité. const
référence :
vector<int> v = {1, 3, 5, 7, 9};
for (auto& x : v)
x *= 10;
for (auto x : v)
cout << x << ' ';
Le résultat est (comme prévu) :
10 30 50 70 90
Ce site for (auto& elem : container)
La syntaxe fonctionne également pour des types plus complexes, par exemple, en considérant un 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 (const auto& x : v)
cout << x << ' ';
le résultat est :
Hi Bob! Hi Jeff! Hi Connie!
Supposons que nous ayons un vector<bool>
et nous voulons inverser l'état logique booléen 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 se compile pas.
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::vector
Le modèle est spécialisé pour bool
avec une mise en œuvre qui paquets le site bool
s pour optimiser l'espace (chaque valeur booléenne est stockée dans un bit, soit huit bits "booléens" dans un octet).
Pour cette raison (puisqu'il n'est pas possible de renvoyer une référence à un seul bit), vector<bool>
utilise une méthode dite "itérateur de proxy" modèle. Un "proxy iterator" est un itérateur qui, lorsqu'il est déréférencé, fait no donner un ordinaire bool &
mais renvoie à la place (par valeur) un objet temporaire , qui est un classe proxy convertible en bool
. (Voir aussi 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 bien :
vector<bool> v = {true, false, false, true};
// Invert boolean status
for (auto&& x : v) // <-- note use of "auto&&" for proxy iterators
x = !x;
// Print new element values
cout << boolalpha;
for (const auto& x : v)
cout << x << ' ';
et les sorties :
false true true false
Notez que le for (auto&& elem : container)
La syntaxe fonctionne également dans les autres cas d'itérateurs ordinaires (non proxy) (par exemple pour un vector<int>
ou un vector<string>
).
(Soit dit en passant, la syntaxe "observation" susmentionnée de la fonction for (const auto& elem : container)
fonctionne bien aussi pour le cas de l'itérateur de proxy).
La discussion ci-dessus peut être résumée dans les lignes directrices suivantes :
Pour en observant les éléments, utilisez la syntaxe suivante :
for (const auto& elem : container) // capture by const reference
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 :
for (auto elem : container) // capture by value
Pour modification de les éléments en place, utiliser :
for (auto& elem : container) // capture by (non-const) reference
Si le conteneur utilise "itérateurs proxy" (comme std::vector<bool>
), utiliser :
for (auto&& elem : container) // capture by &&
Bien sûr, s'il y a un besoin de faire un copie locale de l'élément à l'intérieur du corps de la boucle, en capturant par valeur ( for (auto elem : container)
) est un bon choix.
Sur code générique puisque nous ne pouvons pas faire d'hypothèses sur le type générique T
être bon marché à copier, en en observant il est prudent de toujours utiliser for (const auto& elem : container)
.
(Cela ne déclenchera pas de copies inutiles potentiellement coûteuses, et fonctionnera très bien pour les types de copies bon marché tels que int
et également pour les conteneurs utilisant des itérateurs proxy, tels que std::vector<bool>
.)
En outre, dans modification de si nous voulons code générique pour fonctionner également dans le cas de proxy-itérateurs, la meilleure option consiste à for (auto&& elem : container)
.
(Cela fonctionnera aussi très bien pour les conteneurs utilisant des itérateurs non-proxy ordinaires, comme par exemple std::vector<int>
o std::vector<string>
.)
Ainsi, en code générique les lignes directrices suivantes peuvent être fournies :
Pour en observant les éléments, utiliser :
for (const auto& elem : container)
Pour modification de les éléments en place, utiliser :
for (auto&& elem : container)
Je suppose que vous ne voyez pas le cas où vous avez réellement besoin d'une copie à l'intérieur de la boucle ?
Il n'y a pas manière correcte à utiliser for (auto elem : container)
ou for (auto& elem : container)
o for (const auto& elem : container)
. Vous exprimez simplement ce que vous voulez.
Permettez-moi de développer ce point. Faisons une promenade.
for (auto elem : container) ...
Celui-ci est du sucre syntaxique :
for(auto it = container.begin(); it != container.end(); ++it) {
// Observe that this is a copy by value.
auto elem = *it;
}
Vous pouvez l'utiliser si votre conteneur contient des éléments faciles à copier.
for (auto& elem : container) ...
Celui-ci est du sucre syntaxique :
for(auto it = container.begin(); it != container.end(); ++it) {
// Now you're directly modifying the elements
// because elem is an lvalue reference
auto& elem = *it;
}
Utilisez cette option lorsque vous souhaitez écrire directement dans les éléments du conteneur, par exemple.
for (const auto& elem : container) ...
Celui-ci est du sucre syntaxique :
for(auto it = container.begin(); it != container.end(); ++it) {
// You just want to read stuff, no modification
const auto& elem = *it;
}
Comme le dit le commentaire, juste pour la lecture. Et c'est à peu près tout, tout est "correct" lorsqu'il est utilisé correctement.
Mais qu'en est-il si le conteneur ne renvoie que des références modifiables et que je veux indiquer clairement que je ne souhaite pas les modifier dans la boucle ? Ne devrais-je pas alors utiliser auto const &
pour que mon intention soit claire ?
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 purement des conteneurs.
L'exigence syntaxique pour la boucle for est que range_expression
soutien begin()
y end()
soit comme des fonctions -- soit comme des fonctions membres du type sur lequel elle est évaluée, soit comme des fonctions non membres qui prennent une instance du type.
À titre d'exemple, on peut générer une plage de nombres et itérer sur cette plage à l'aide de la classe suivante.
struct Range
{
struct Iterator
{
Iterator(int v, int s) : val(v), step(s) {}
int operator*() const
{
return val;
}
Iterator& operator++()
{
val += step;
return *this;
}
bool operator!=(Iterator const& rhs) const
{
return (this->val < rhs.val);
}
int val;
int step;
};
Range(int l, int h, int s=1) : low(l), high(h), step(s) {}
Iterator begin() const
{
return Iterator(low, step);
}
Iterator end() const
{
return Iterator(high, 1);
}
int low, high, step;
};
Avec les éléments suivants main
fonction,
#include <iostream>
int main()
{
Range r1(1, 10);
for ( auto item : r1 )
{
std::cout << item << " ";
}
std::cout << std::endl;
Range r2(1, 20, 2);
for ( auto item : r2 )
{
std::cout << item << " ";
}
std::cout << std::endl;
Range r3(1, 20, 3);
for ( auto item : r3 )
{
std::cout << item << " ";
}
std::cout << std::endl;
}
on obtiendrait le résultat suivant.
1 2 3 4 5 6 7 8 9
1 3 5 7 9 11 13 15 17 19
1 4 7 10 13 16 19
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.
7 votes
Les mêmes considérations que pour les arguments de fonction s'appliquent.
3 votes
En fait, cela n'a pas grand-chose à voir avec la portée pour. On peut dire la même chose de tout
auto (const)(&) x = <expr>;
.3 votes
@MatthieuM : Cela a lot à faire avec la gamme basée pour, bien sûr ! Prenons le cas d'un débutant qui voit plusieurs syntaxes et ne peut pas choisir la forme à utiliser. Le but des "Q&R" était d'essayer de faire la lumière, et d'expliquer les différences de certains cas (et de discuter des cas qui compilent bien mais qui sont plutôt inefficaces à cause de copies profondes inutiles, etc.)
2 votes
@Mr.C64 : En ce qui me concerne, cela a plus à voir avec
auto
en général, qu'avec le for basé sur l'intervalle ; vous pouvez parfaitement utiliser le for basé sur l'intervalle sans aucune erreur de calcul.auto
!for (int i: v) {}
est parfaitement bien. Bien sûr, la plupart des points que vous soulevez dans votre réponse peut avoir plus à voir avec le type que avecauto
... mais d'après la question, il n'est pas clair où se situe le point sensible. Personnellement, je me battrais pour enleverauto
de la question ; ou peut-être rendre explicite le fait que si vous utilisez desauto
ou nommer explicitement le type, la question est axée sur la valeur/référence.1 votes
@MatthieuM. : Je suis ouvert pour changer le titre ou éditer la question sous une forme qui pourrait les rendre plus clairs... Encore une fois, mon objectif était de discuter de plusieurs options pour les syntaxes for basées sur l'intervalle (montrer le code qui compile mais qui est inefficace, le code qui ne compile pas, etc.) et d'essayer d'offrir quelques conseils à quelqu'un (surtout au niveau débutant) qui approche les boucles for basées sur l'intervalle de C++11.
1 votes
@Mr.C64 : Je comprends (maintenant), ma seule suggestion est la suivante no à utiliser
auto
. Lorsque vous enseignez une nouvelle matière, il est plus facile d'enseigner les concepts un par un. Vous pouvez ensuite réintroduireauto
plus tard dans la réponse (par exemple, dans votre dernier chapitre sur le code générique).