Comencemos por diferenciar entre observar los elementos en el contenedor y modificar en su lugar.
Observando los elementos
Consideremos un ejemplo simple:
vector<int> v = {1, 3, 5, 7, 9};
for (auto x : v)
cout << x << ' ';
El código anterior imprime los elementos int
en vector
:
1 3 5 7 9
Ahora considere otro caso, en el que los elementos vectoriales no son simples enteros, sino instancias de una clase más compleja, con un constructor de copia personalizado, 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 usamos la for (auto x : v) {...}
sintaxis anterior con esta nueva clase:
vector<X> v = {1, 3, 5, 7, 9};
cout << "\nElements:\n";
for (auto x : v)
{
cout << x << ' ';
}
la salida es algo como:
[... 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
Como se puede leer desde la salida, las llamadas al constructor de copia se realizan durante iteraciones de bucle basadas en rango.
Esto se debe a que estamos capturando los elementos del contenedor por valor
(la auto x
parte en for (auto x : v)
).
Este es un código ineficiente , por ejemplo, si estos elementos son instancias de std::string
, se pueden hacer asignaciones de memoria de montón, con viajes costosos al administrador de memoria, etc. Esto es inútil si solo queremos observar los elementos en un contenedor.
Entonces, hay una mejor sintaxis disponible: captura por const
referencia , es decir const auto&
:
vector<X> v = {1, 3, 5, 7, 9};
cout << "\nElements:\n";
for (const auto& x : v)
{
cout << x << ' ';
}
Ahora la salida es:
[... copy constructor calls for vector<X> initialization ...]
Elements:
1 3 5 7 9
Sin ninguna llamada de constructor de copia espuria (y potencialmente costosa).
Por lo tanto, cuando la observación de los elementos en un recipiente (es decir, para el acceso de sólo lectura), la siguiente sintaxis está muy bien para simples -barato-a copia tipos, como int
, double
, etc .:
for (auto elem : container)
De lo contrario, la captura por const
referencia es mejor en el caso general , para evitar llamadas de constructor de copias inútiles (y potencialmente caras):
for (const auto& elem : container)
Modificar los elementos en el contenedor
Si queremos modificar los elementos en un contenedor utilizando un rango for
, las
sintaxis for (auto elem : container)
y las anteriores for (const auto& elem : container)
son incorrectas.
De hecho, en el primer caso, elem
almacena una copia del elemento original, por lo que las modificaciones realizadas se pierden y no se almacenan de forma persistente en el contenedor, por ejemplo:
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 salida es solo la secuencia inicial:
1 3 5 7 9
En cambio, un intento de usar for (const auto& x : v)
simplemente no se compila.
g ++ genera un mensaje de error similar a este:
TestRangeFor.cpp:138:11: error: assignment of read-only reference 'x'
x *= 10;
^
El enfoque correcto en este caso es capturar por no const
referencia:
vector<int> v = {1, 3, 5, 7, 9};
for (auto& x : v)
x *= 10;
for (auto x : v)
cout << x << ' ';
El resultado es (como se esperaba):
10 30 50 70 90
Esta for (auto& elem : container)
sintaxis también funciona para tipos más complejos, por ejemplo, considerando 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 << ' ';
la salida es:
Hi Bob! Hi Jeff! Hi Connie!
El caso especial de los iteradores proxy
Supongamos que tenemos un vector<bool>
, y queremos invertir el estado lógico booleano de sus elementos, usando la sintaxis anterior:
vector<bool> v = {true, false, false, true};
for (auto& x : v)
x = !x;
El código anterior no se compila.
g ++ genera un mensaje de error similar a este:
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)
^
El problema es que la std::vector
plantilla está especializada para bool
, con una implementación que empaqueta el bool
s para optimizar el espacio (cada valor booleano se almacena en un bit, ocho bits "booleanos" en un byte).
Debido a eso (dado que no es posible devolver una referencia a un solo bit),
vector<bool>
utiliza el llamado patrón "iterador proxy" . Un "iterador proxy" es un iterador que, cuando se desreferencia, no produce un ordinario bool &
, sino que devuelve (por valor) un objeto temporal , que es una clase proxy convertiblebool
. (Véase también esta pregunta y respuestas relacionadas aquí en StackOverflow.)
Para modificar en su lugar los elementos de vector<bool>
, se auto&&
debe usar un nuevo tipo de sintaxis (usando ):
for (auto&& x : v)
x = !x;
El siguiente código funciona 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 << ' ';
y salidas:
false true true false
Tenga en cuenta que la for (auto&& elem : container)
sintaxis también funciona en los otros casos de iteradores (no proxy) ordinarios (por ejemplo, para un vector<int>
o unavector<string>
).
(Como nota al margen, la sintaxis de "observación" mencionada anteriormente for (const auto& elem : container)
funciona bien también para el caso del iterador proxy).
Resumen
La discusión anterior se puede resumir en las siguientes pautas:
Para observar los elementos, use la siguiente sintaxis:
for (const auto& elem : container) // capture by const reference
Si los objetos son baratos de copiar (como int
s, double
s, etc.), es posible usar una forma ligeramente simplificada:
for (auto elem : container) // capture by value
Para modificar los elementos en su lugar, use:
for (auto& elem : container) // capture by (non-const) reference
Si el contenedor usa "iteradores proxy" (como std::vector<bool>
), use:
for (auto&& elem : container) // capture by &&
Por supuesto, si es necesario hacer una copia local del elemento dentro del cuerpo del bucle, la captura por valor ( for (auto elem : container)
) es una buena opción.
Notas adicionales sobre código genérico
En el código genérico , dado que no podemos hacer suposiciones sobre que el tipo genérico T
sea barato de copiar, en modo de observación es seguro usarlo siempre for (const auto& elem : container)
.
(Esto no desencadenará copias inútiles potencialmente costosas, funcionará bien también para tipos baratos de copiar int
, como también para contenedores que usan iteradores proxy, como std::vector<bool>
).
Además, en el modo de modificación , si queremos que el código genérico funcione también en el caso de los iteradores proxy, la mejor opción es for (auto&& elem : container)
.
(Esto funcionará bien también para contenedores que usan iteradores no proxy normales, como std::vector<int>
o std::vector<string>
).
Entonces, en código genérico , se pueden proporcionar las siguientes pautas:
Para observar los elementos, use:
for (const auto& elem : container)
Para modificar los elementos en su lugar, use:
for (auto&& elem : container)