¿Para qué es la forma correcta de usar el rango basado en C ++ 11?


212

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

¿Qué sintaxis se debe usar? for (auto elem : container)o for (auto& elem : container)o for (const auto& elem : container)? O alguna otra?


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

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 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 (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 constreferencia 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, 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:

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

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

  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 ints, doubles, 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 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:

  1. Para observar los elementos, use:

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

    for (auto&& elem : container)

77
¿Ningún consejo para contextos genéricos? :(
R. Martinho Fernandes

11
¿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 reference
    auto& elem = *it;

}

Use esto cuando quiera escribir directamente a los elementos en el 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 para leer. Y eso es todo, todo es "correcto" cuando se usa correctamente.


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


66
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 &para 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 ++.
Jack Aidley

66
Esta propuesta de evolución del lenguaje está de acuerdo con esta respuesta "pobre": open-std.org/jtc1/sc22/wg21/docs/papers/2014/n3853.htm
Luc Hermitte

1

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.

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 mainfunción,

#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 el siguiente resultado.

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 
Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.