¿Cuál es la ventaja de utilizar referencias de reenvío en bucles for basados ​​en rangos?

115

const auto&Sería suficiente si quiero realizar operaciones de solo lectura. Sin embargo, me he encontrado

for (auto&& e : v)  // v is non-const

un par de veces recientemente. Esto me hace preguntarme:

¿Es posible que en algunos casos poco conocidos exista algún beneficio de rendimiento al usar referencias de reenvío, en comparación con auto&oconst auto& ?

( shared_ptres sospechoso de casos oscuros de esquina)


Actualización Dos ejemplos que encontré en mis favoritos:

¿Alguna desventaja de usar la referencia constante al iterar sobre tipos básicos?
¿Puedo iterar fácilmente sobre los valores de un mapa usando un bucle for basado en rango?

Concéntrese en la pregunta: ¿por qué querría usar auto && en bucles for basados ​​en rango?

Ali
fuente
4
¿ Realmente lo ve "a menudo"?
Lightness Races in Orbit
2
No estoy seguro de que haya suficiente contexto en su pregunta para medir qué tan "loco" es el lugar donde lo está viendo.
Lightness Races in Orbit
2
@LightnessRacesinOrbit En resumen: ¿por qué querría usar auto&&en bucles for basados ​​en rangos?
Ali

Respuestas:

94

La única ventaja que puedo ver es cuando el iterador de secuencia devuelve una referencia de proxy y necesita operar en esa referencia de una manera no constante. Por ejemplo, considere:

#include <vector>

int main()
{
    std::vector<bool> v(10);
    for (auto& e : v)
        e = true;
}

Esto no se compila porque rvalue vector<bool>::referencedevuelto por iteratorno se vinculará a una referencia lvalue no constante. Pero esto funcionará:

#include <vector>

int main()
{
    std::vector<bool> v(10);
    for (auto&& e : v)
        e = true;
}

Dicho todo esto, no codificaría de esta manera a menos que supieras que necesitas satisfacer tal caso de uso. Es decir, no haría esto gratuitamente porque hace que la gente se pregunte qué estás haciendo. Y si lo hiciera, no estaría de más incluir un comentario sobre por qué:

#include <vector>

int main()
{
    std::vector<bool> v(10);
    // using auto&& so that I can handle the rvalue reference
    //   returned for the vector<bool> case
    for (auto&& e : v)
        e = true;
}

Editar

Este último caso mío debería ser una plantilla para que tenga sentido. Si sabe que el bucle siempre está manejando una referencia de proxy, entonces autofuncionaría tan bien como auto&&. Pero cuando el bucle a veces manejaba referencias no proxy y, a veces, referencias proxy, entonces creo que auto&&se convertiría en la solución de elección.

Howard Hinnant
fuente
3
Por otro lado, no hay ninguna desventaja explícita , ¿verdad? (Aparte de las personas potencialmente confusa, que no creo que es mucho vale la pena mencionar, en lo personal.)
Ildjarn
7
Otro término para escribir código que confunde innecesariamente a las personas es: escribir código confuso. Es mejor hacer su código lo más simple posible, pero no más simple. Esto ayudará a mantener la cuenta atrás de errores. Dicho esto, a medida que && se vuelva más familiar, tal vez dentro de 5 años la gente esperará un modismo de && automático (asumiendo que en realidad no hace daño). No sé si eso pasará o no. Pero lo simple es a los ojos del espectador, y si está escribiendo para algo más que usted mismo, tenga en cuenta a sus lectores.
Howard Hinnant
7
Prefiero const auto&cuando quiero que el compilador me ayude a comprobar que no modifico accidentalmente los elementos de la secuencia.
Howard Hinnant
31
Personalmente, me gusta usar auto&&en código genérico donde necesito modificar los elementos de la secuencia. Si no lo hago, me quedaré auto const&.
Xeo
7
@Xeo: +1 C ++ sigue evolucionando gracias a entusiastas como usted, que constantemente experimentan y buscan mejores formas de hacer las cosas. Gracias. :-)
Howard Hinnant
26

El uso de referencias universalesauto&& o con un bucle basado en rango tiene la ventaja de que captura lo que obtiene. Para la mayoría de los tipos de iteradores, probablemente obtendrá una o una para algún tipo . El caso interesante es donde la desreferenciación de un iterador produce un temporal: C ++ 2011 tiene requisitos relajados y los iteradores no son necesariamente necesarios para producir un valor l. El uso de referencias universales coincide con el reenvío de argumentos en :forT&T const&Tstd::for_each()

template <typename InIt, typename F>
F std::for_each(InIt it, InIt end, F f) {
    for (; it != end; ++it) {
        f(*it); // <---------------------- here
    }
    return f;
}

El objeto de función fpuede tratar T&, T const&y Tde manera diferente. ¿Por qué debería forser diferente el cuerpo de un bucle basado en rango? Por supuesto, para aprovechar realmente haber deducido el tipo usando referencias universales, necesitaría transmitirlas de manera correspondiente:

for (auto&& x: range) {
    f(std::forward<decltype(x)>(x));
}

Por supuesto, usar std::forward()significa que acepta cualquier valor devuelto desde el que se moverá. Si objetos como este tienen mucho sentido en el código que no es de plantilla, no lo sé (¿todavía?). Puedo imaginar que el uso de referencias universales puede ofrecer más información al compilador para hacer lo correcto. En el código con plantilla, se mantiene al margen de tomar decisiones sobre lo que debería suceder con los objetos.

Dietmar Kühl
fuente
9

Prácticamente siempre uso auto&&. ¿Por qué ser mordido por un caso extremo cuando no es necesario? También es más corto de escribir, y simplemente lo encuentro más ... transparente. Cuando lo usa auto&& x, entonces sabe que xes exactamente *it, siempre.

Perrito
fuente
29
Mi problema es que te estás rindiendo constcon auto&&si es const auto&suficiente. La pregunta se refiere a los casos de esquina en los que me pueden picar. ¿Cuáles son los casos de esquina que aún no han sido mencionados por Dietmar o Howard?
Ali
2
Aquí hay una manera de ser mordido si lo usa auto&&. Si el tipo que está capturando debe moverse en el cuerpo del bucle (por ejemplo) y se cambia a una fecha posterior para que se resuelva en un const&tipo, su código seguirá funcionando silenciosamente, pero sus movimientos se convertirán en copias. Este código será muy engañoso. Sin embargo, si especifica explícitamente el tipo como una referencia de valor r, quien haya cambiado el tipo de contenedor obtendrá un error de compilación porque realmente desea que estos objetos se muevan y no se copien ...
cyberbisson
@cyberbisson Me acabo de encontrar con esto: ¿cómo puedo hacer cumplir una referencia rvalue sin especificar explícitamente el tipo? Algo como for (std::decay_t<decltype(*v.begin())>&& e: v)? Supongo que hay una mejor manera ...
Jerry Ma
@JerryMa Depende de lo que usted no sabe sobre el tipo. Como se mencionó anteriormente, auto&&dará una "referencia universal", por lo que cualquier otra cosa le dará un tipo más específico. Si vse asume que es un vector, podría hacerlo decltype(v)::value_type&&, que es lo que supongo que deseaba tomando el resultado de operator*en un tipo de iterador. También puede hacer decltype(begin(v))::value_type&&para verificar los tipos del iterador en lugar del contenedor. Sin embargo, si tenemos tan poca información sobre el tipo, podríamos considerar que es un poco más claro ir con auto&&...
cyberbisson