¿Cuál es la forma correcta de usar el rango de C ++ 11 para?

¿Cuál es la forma correcta de usar el rango de C ++ 11 for ?

¿Qué syntax debería usarse? for (auto elem : container) , o for (auto& elem : container) o for (const auto& elem : container) ? ¿O alguna otra?

Comencemos por diferenciar entre observar los elementos en el continuo vs. modificarlos en su lugar.

Observando los elementos

Consideremos un ejemplo simple:

 vector v = {1, 3, 5, 7, 9}; for (auto x : v) cout << x << ' '; 

El código anterior imprime los elementos ( int s) en el vector :

 1 3 5 7 9 

Ahora considere otro caso, en el que los elementos vectoriales no son simplemente enteros simples, sino instancias de una clase más compleja, con 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 utilizamos la syntax anterior for (auto x : v) {...} con esta nueva clase:

 vector v = {1, 3, 5, 7, 9}; cout << "\nElements:\n"; for (auto x : v) { cout << x << ' '; } 

el resultado es algo así como:

 [... copy constructor calls for vector 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 el intervalo para iteraciones de bucle.
Esto se debe a que estamos capturando los elementos del contenedor por valor (la parte auto x 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 stack, con costosos viajes al administrador de memoria, etc. Esto es inútil si solo queremos observar los elementos en un contenedor.

Entonces, hay una mejor syntax disponible: capture by const reference , es decir, const auto& :

 vector 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 initialization ...] Elements: 1 3 5 7 9 

Sin ninguna llamada falsa (y potencialmente costosa) de constructor de copia.

Por lo tanto, cuando se observan elementos en un contenedor (es decir, para un acceso de solo lectura), la siguiente syntax está bien para los tipos simples de bajo costo , como int , double , etc .:

 for (auto elem : container) 

De lo contrario, la captura por referencias de referencias es mejor en el caso general , para evitar llamadas de constructores de copias inútiles (y potencialmente costosas):

 for (const auto& elem : container) 

Modificar los elementos en el contenedor

Si queremos modificar los elementos en un contenedor utilizando el rango for , lo anterior for (auto elem : container) y for (const auto& elem : container) syntax 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 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 << ' '; 

El resultado es solo la secuencia inicial:

 1 3 5 7 9 

En cambio, un bash de usar for (const auto& x : v) simplemente no se puede comstackr.

g ++ genera un mensaje de error como este:

 TestRangeFor.cpp:138:11: error: assignment of read-only reference 'x' x *= 10; ^ 

El enfoque correcto en este caso es capturar por referencia no const :

 vector 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 

Esto for (auto& elem : container) syntax for (auto& elem : container) también funciona para tipos más complejos, por ejemplo, considerando un vector :

 vector 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 , y queremos invertir el estado lógico booleano de sus elementos, usando la syntax anterior:

 vector v = {true, false, false, true}; for (auto& x : v) x = !x; 

El código anterior no puede comstackrse.

g ++ muestra un mensaje de error similar a esto:

 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 plantilla std::vector está especializada para bool , con una implementación que empaqueta los bool 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), el vector usa un patrón llamado "iterador proxy" . Un "iterador de proxy" es un iterador que, cuando se desreferencia, no genera un bool & ordinario, sino que devuelve (por valor) un objeto temporal , que es una clase de proxy convertible en bool . (Consulte también esta pregunta y las respuestas relacionadas aquí en StackOverflow.)

Para modificar en su lugar los elementos de vector , se debe usar un nuevo tipo de syntax (usando auto&& ):

 for (auto&& x : v) x = !x; 

El siguiente código funciona bien:

 vector 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 productos:

 false true true false 

Tenga en cuenta que la syntax for (auto&& elem : container) también funciona en los otros casos de iteradores ordinarios (no proxy) (por ejemplo, para un vector o un vector ).

(Como nota al margen, la syntax 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 líneas guía:

  1. Para observar los elementos, use la siguiente syntax:

     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 

  2. Para modificar los elementos en su lugar, use:

     for (auto& elem : container) // capture by (non-const) reference 
    • Si el contenedor utiliza "iteradores proxy" (como std::vector ), use:

       for (auto&& elem : container) // capture by && 

Por supuesto, si hay una necesidad de hacer una copia local del elemento dentro del cuerpo del bucle, capturar por valor ( for (auto elem : container) ) es una buena opción.


Notas adicionales sobre el código genérico

En código genérico , ya que no podemos hacer suposiciones sobre el tipo genérico T es barato copiar, en el modo de observación es seguro usarlo siempre for (const auto& elem : container) .
(Esto no provocará copias inútiles potencialmente costosas, funcionará muy bien también para tipos de bajo costo como int , y también para contenedores que usan proxy-iterators, como std::vector .)

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 también funcionará bien para contenedores que usan iteradores comunes no proxy, como std::vector o std::vector ).

Por lo tanto, en el código genérico , se pueden proporcionar las siguientes pautas:

  1. Para observar los elementos, use:

     for (const auto& elem : container) 
  2. Para modificar los elementos en su lugar, use:

     for (auto&& elem : container) 

No hay una forma correcta de usar for (auto elem : container) , o for (auto& elem : container) o for (const auto& elem : container) . Usted solo expresa lo que quiere.

Permítanme dar más detalles sobre eso. Vamos a dar un paseo.

 for (auto elem : container) ... 

Este es azúcar sintáctico para:

 for(auto it = container.begin(); it != container.end(); ++it) { // Observe that this is a copy by value. auto elem = *it; } 

Puede usar este si su contenedor contiene elementos que son baratos de copiar.

 for (auto& elem : container) ... 

Este es azúcar sintáctico para:

 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; } 

Utilice esto cuando quiera escribir directamente en los elementos del contenedor, por ejemplo.

 for (const auto& elem : container) ... 

Este es azúcar sintáctico para:

 for(auto it = container.begin(); it != container.end(); ++it) { // You just want to read stuff, no modification const auto& elem = *it; } 

Como dice el comentario, solo por leer. Y eso es todo, todo es “correcto” cuando se usa correctamente.

El medio correcto es siempre

 for(auto&& elem : container) 

Esto garantizará la preservación de toda la semántica.

Si bien la motivación inicial del bucle Range-For podría haber sido fácil de iterar sobre los elementos de un contenedor, la syntax es lo suficientemente genérica como para ser útil incluso para objetos que no son puramente contenedores.

El requisito sintáctico para el for-loop es que range_expression soporte begin() y end() como cualquier función, ya sea como funciones miembro del tipo que evalúa o como funciones no miembro, lo que toma una instancia del tipo.

Como un ejemplo artificial, uno puede generar un rango de números e iterar sobre el rango utilizando la siguiente clase.

 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; }; 

Con la siguiente función main ,

 #include  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; } 

uno obtendría la siguiente salida.

 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