tipos programas programa lenguaje estructuras ejemplos datos comandos c++ c++11 foreach

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



programa en c++ (4)

Comencemos por diferenciar entre observar los elementos en el continuo vs. modificarlos 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 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 sintaxis anterior for (auto x : v) {...} con esta nueva clase:

vector<X> 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<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 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 pila, 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 sintaxis disponible: capture by const reference , 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 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 sintaxis 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) sintaxis 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 << '' '';

El resultado es solo la secuencia inicial:

1 3 5 7 9

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

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

Esto for (auto& elem : container) sintaxis for (auto& elem : container) 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 puede compilarse.

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<bool> 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<bool> , se debe usar un nuevo tipo de sintaxis (usando auto&& ):

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

false true true false

Tenga en cuenta que la sintaxis for (auto&& elem : container) también funciona en los otros casos de iteradores ordinarios (no proxy) (por ejemplo, para un vector<int> o un vector<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 líneas guía:

  1. 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

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

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)

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

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


El medio correcto es siempre

for(auto&& elem : container)

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


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.


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

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