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 int
dans vector
:
1 3 5 7 9
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;
}
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 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 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 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 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 const
référence , c'est const auto&
-à- dire :
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 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 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):
for (const auto& 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, 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:
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:
1 3 5 7 9
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- const
ré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):
10 30 50 70 90
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 (const auto& x : v)
cout << x << ' ';
la sortie est:
Hi Bob! Hi Jeff! Hi Connie!
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::vector
gabarit est spécialisé pour bool
, avec une mise en oeuvre que des packs les bool
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 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 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 sorties:
false true true false
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 (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 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 T
bon 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:
Pour observer les éléments, utilisez:
for (const auto& elem : container)
Pour modifier les éléments en place, utilisez:
for (auto&& elem : container)