Se aplica la misma consideración que para los argumentos de función.
Maxim Egorushkin
3
En realidad, esto tiene poco que ver con el rango basado en. Lo mismo se puede decir de cualquiera auto (const)(&) x = <expr>;.
Matthieu M.
2
@MatthieuM: ¡Esto tiene mucho que ver con el rango basado, por supuesto! Considere un principiante que ve varias sintaxis y no puede elegir qué forma usar. El punto de "Q&A" era tratar de arrojar algo de luz y explicar las diferencias de algunos casos (y discutir casos que se compilan bien pero que son poco eficientes debido a las copias profundas inútiles, etc.).
Mr.C64
2
@ Mr.C64: En lo que a mí respecta, esto tiene más que ver con auto, en general, que con el rango basado en; ¡puedes usar perfectamente basado en rango sin ninguno auto! for (int i: v) {}Está perfectamente bien. Por supuesto, la mayoría de los puntos que plantea en su respuesta pueden tener más que ver con el tipo que con auto... pero a partir de la pregunta no está claro dónde está el punto de dolor. Personalmente, competiría por eliminar autola pregunta; o tal vez explique que si usa autoo nombra explícitamente el tipo, la pregunta se centra en el valor / referencia.
Matthieu M.
1
@MatthieuM .: estoy abierto a cambiar el título o editar la pregunta de alguna forma que pueda aclararlos ... Una vez más, mi enfoque fue discutir varias opciones para sintaxis basadas en rangos (que muestran código que se compila pero es ineficiente, código que no se puede compilar, etc.) y tratando de ofrecer alguna orientación a alguien (especialmente a nivel principiante) que se acerca a C ++ 11 basado en el rango para bucles.
Mr.C64
Respuestas:
390
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 inten vector:
13579
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;}intGet()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 forvector<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 xparte 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 constreferencia , es decir const auto&:
vector<X> v ={1,3,5,7,9};
cout <<"\nElements:\n";for(constauto& x : v){
cout << x <<' ';}
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 constreferencia es mejor en el caso general , para evitar llamadas de constructor de copias inútiles (y potencialmente caras):
for(constauto& 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, elemalmacena 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:
13579
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 constreferencia:
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):
1030507090
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(constauto& x : v)
cout << x <<' ';
la salida es:
HiBob!HiJeff!HiConnie!
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::vectorplantilla está especializada para bool, con una implementación que empaqueta el bools 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 statusfor(auto&& x : v)// <-- note use of "auto&&" for proxy iterators
x =!x;// Print new element values
cout << boolalpha;for(constauto& x : v)
cout << x <<' ';
y salidas:
falsetruetruefalse
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(constauto& elem : container)// capture by const reference
Si los objetos son baratos de copiar (como ints, doubles, 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 Tsea 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:
¿Por qué no usar siempre auto&&? ¿Hay un const auto&&?
Martin Ba
1
¿Supongo que te estás perdiendo el caso en el que realmente necesitas una copia dentro del bucle?
juanchopanza
66
"Si el contenedor usa" iteradores proxy "" , y usted sabe que usa "iteradores proxy" (que podría no ser el caso en el código genérico). Así que creo que lo mejor es auto&&, ya que cubre auto&igualmente bien.
Christian Rau
55
Gracias, esa fue una gran "introducción del curso intensivo" a la sintaxis y algunos consejos para el programa basado en el rango para un programador de C #. +1.
AndrewJacksonZA
17
No hay una forma correcta de usar for (auto elem : container), o for (auto& elem : container)o for (const auto& elem : container). Solo expresas lo que quieres.
Déjame elaborar 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 referenceauto& elem =*it;}
Use esto cuando quiera escribir directamente a los elementos en el contenedor, por ejemplo.
for(constauto& 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 modificationconstauto& elem =*it;}
Como dice el comentario, solo para leer. Y eso es todo, todo es "correcto" cuando se usa correctamente.
Tenía la intención de dar alguna orientación, compilando códigos de muestra (pero siendo ineficientes), o no compilando, y explicando por qué, e intentando proponer algunas soluciones.
Mr.C64
2
@ Mr.C64 Oh, lo siento, me acabo de dar cuenta de que esta es una de esas preguntas frecuentes. Soy nuevo en este sitio. Disculpas! Su respuesta es excelente, la voté positivamente, pero también quería proporcionar una versión más concisa para aquellos que quieren la esencia de la misma . Con suerte, no me estoy entrometiendo.
1
@ Mr.C64, ¿cuál es el problema con OP respondiendo la pregunta también? Es solo otra respuesta válida.
mfontanini
1
@mfontanini: No hay absolutamente ningún problema si alguien publica alguna respuesta, incluso mejor que la mía. El propósito final es dar una contribución de calidad a la comunidad (especialmente para los principiantes que pueden sentirse perdidos frente a las diferentes sintaxis y las diferentes opciones que ofrece C ++).
Mr.C64
4
El medio correcto es siempre
for(auto&& elem : container)
Esto garantizará la preservación de toda la semántica.
Pero, ¿qué sucede si el contenedor solo devuelve referencias modificables y quiero dejar en claro que no deseo modificarlas en el ciclo? ¿No debería usar auto const ¶ aclarar mi intención?
RedX
@RedX: ¿Qué es una "referencia modificable"?
Carreras de ligereza en órbita
2
@RedX: las referencias nunca son const, y nunca son mutables. De todos modos, mi respuesta es sí, lo haría .
Carreras de ligereza en órbita
44
Si bien esto puede funcionar, creo que este es un mal consejo en comparación con el enfoque más matizado y considerado dado por la excelente y completa respuesta del Sr. C64 dada anteriormente. Reducir al mínimo común denominador no es para qué sirve C ++.
Si bien la motivación inicial del bucle de rango para podría haber sido la facilidad 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 simplemente contenedores.
El requisito sintáctico para el bucle for es que el range_expressionsoporte begin()yend() como funciones, ya sea como funciones miembro del tipo al que evalúa o como funciones no miembros que toman una instancia del tipo.
Como ejemplo artificial, uno puede generar un rango de números e iterar sobre el rango usando la siguiente clase.
auto (const)(&) x = <expr>;
.auto
, en general, que con el rango basado en; ¡puedes usar perfectamente basado en rango sin ningunoauto
!for (int i: v) {}
Está perfectamente bien. Por supuesto, la mayoría de los puntos que plantea en su respuesta pueden tener más que ver con el tipo que conauto
... pero a partir de la pregunta no está claro dónde está el punto de dolor. Personalmente, competiría por eliminarauto
la pregunta; o tal vez explique que si usaauto
o nombra explícitamente el tipo, la pregunta se centra en el valor / referencia.Respuestas:
Comencemos por diferenciar entre observar los elementos en el contenedor y modificar en su lugar.
Observando los elementos
Consideremos un ejemplo simple:
El código anterior imprime los elementos
int
envector
: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.
Si usamos la
for (auto x : v) {...}
sintaxis anterior con esta nueva clase:la salida es algo como:
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 enfor (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 decirconst auto&
:Ahora la salida es:
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 .: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):Modificar los elementos en el contenedor
Si queremos modificar los elementos en un contenedor utilizando un rango
for
, las sintaxisfor (auto elem : container)
y las anterioresfor (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:La salida es solo la secuencia inicial:
En cambio, un intento de usar
for (const auto& x : v)
simplemente no se compila.g ++ genera un mensaje de error similar a este:
El enfoque correcto en este caso es capturar por no
const
referencia:El resultado es (como se esperaba):
Esta
for (auto& elem : container)
sintaxis también funciona para tipos más complejos, por ejemplo, considerando unvector<string>
:la salida es:
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:El código anterior no se compila.
g ++ genera un mensaje de error similar a este:
El problema es que la
std::vector
plantilla está especializada parabool
, con una implementación que empaqueta elbool
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 ordinariobool &
, 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>
, seauto&&
debe usar un nuevo tipo de sintaxis (usando ):El siguiente código funciona bien:
y salidas:
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 unvector<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:
Si los objetos son baratos de copiar (como
int
s,double
s, etc.), es posible usar una forma ligeramente simplificada:Para modificar los elementos en su lugar, use:
Si el contenedor usa "iteradores proxy" (como
std::vector<bool>
), use: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 siemprefor (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, comostd::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>
ostd::vector<string>
).Entonces, en código genérico , se pueden proporcionar las siguientes pautas:
Para observar los elementos, use:
Para modificar los elementos en su lugar, use:
fuente
auto&&
? ¿Hay unconst auto&&
?auto&&
, ya que cubreauto&
igualmente bien.No hay una forma correcta de usar
for (auto elem : container)
, ofor (auto& elem : container)
ofor (const auto& elem : container)
. Solo expresas lo que quieres.Déjame elaborar sobre eso. Vamos a dar un paseo.
Este es azúcar sintáctico para:
Puede usar este si su contenedor contiene elementos que son baratos de copiar.
Este es azúcar sintáctico para:
Use esto cuando quiera escribir directamente a los elementos en el contenedor, por ejemplo.
Este es azúcar sintáctico para:
Como dice el comentario, solo para leer. Y eso es todo, todo es "correcto" cuando se usa correctamente.
fuente
El medio correcto es siempre
Esto garantizará la preservación de toda la semántica.
fuente
auto const &
para aclarar mi intención?const
, y nunca son mutables. De todos modos, mi respuesta es sí, lo haría .Si bien la motivación inicial del bucle de rango para podría haber sido la facilidad 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 simplemente contenedores.
El requisito sintáctico para el bucle for es que el
range_expression
soportebegin()
yend()
como funciones, ya sea como funciones miembro del tipo al que evalúa o como funciones no miembros que toman una instancia del tipo.Como ejemplo artificial, uno puede generar un rango de números e iterar sobre el rango usando la siguiente clase.
Con la siguiente
main
función,uno obtendría el siguiente resultado.
fuente