¿Por qué obtener helper de std :: tuple return rvalue reference en lugar de value

Si observa get , la función auxiliar para std::tuple , notará la siguiente sobrecarga:

 template constexpr std::tuple_element_t<I, tuple >&& get( tuple&& t ); 

En otras palabras, devuelve una referencia rvalue cuando la tupla de entrada es una referencia rvalue en sí misma. ¿Por qué no devolver por valor, llamando move en el cuerpo de la función? Mi argumento es el siguiente: el retorno de get estará vinculado a una referencia o a un valor (podría no estar vinculado a nada, supongo, pero esto no debería ser un caso de uso común). Si está vinculado a un valor, entonces de todos modos se producirá una construcción de movimiento. Entonces no pierdes nada volviendo por valor. Si se vincula a una referencia, devolver una referencia de valor puede ser inseguro. Para mostrar un ejemplo:

 struct Hello { Hello() { std::cerr << "Constructed at : " << this << std::endl; } ~Hello() { std::cerr << "Destructed at : " << this << std::endl; } double m_double; }; struct foo { Hello m_hello; Hello && get() && { return std::move(m_hello); } }; int main() { const Hello & x = foo().get(); std::cerr << x.m_double; } 

Cuando se ejecuta, este progtwig imprime:

 Constructed at : 0x7ffc0e12cdc0 Destructed at : 0x7ffc0e12cdc0 0 

En otras palabras, x es inmediatamente una referencia que cuelga. Mientras que si acabas de escribir foo así:

 struct foo { Hello m_hello; Hello get() && { return std::move(m_hello); } }; 

Este problema no se produciría. Además, si usas foo así:

 Hello x(foo().get()); 

No parece que haya una sobrecarga adicional, ya sea que devuelva por valor o por valor de referencia. He probado código como este, y parece que solo realizará una construcción de un solo movimiento de manera consistente. Por ejemplo, si agrego un miembro:

  Hello(Hello && ) { std::cerr << "Moved" << std::endl; } 

Y construyo x como antes, mi progtwig solo imprime “Movido” una vez sin importar si devuelvo por valor o por valor de referencia.

¿Hay una buena razón por la que me pierdo, o esto es un descuido?

Nota: aquí hay una buena pregunta relacionada: ¿ Valor de retorno o referencia de valor? . Parece decir que el retorno del valor es generalmente preferible en esta situación, pero el hecho de que aparezca en la STL me hace sentir curioso si la STL ha ignorado este razonamiento, o si tienen razones propias propias que pueden no ser tan aplicables. generalmente.

Edición: alguien ha sugerido que esta pregunta es un duplicado de ¿Hay algún caso en el que sea útil devolver una referencia de RValue (&&)? . Este no es el caso; esta respuesta sugiere devolver por referencia de valor como una forma de evitar la copia de miembros de datos. Como lo explico en detalle más arriba, la copia se realizará independientemente de si devuelve por valor o referencia de valor siempre que llame primero a move .

Su ejemplo de cómo se puede usar para crear una referencia colgante es muy interesante, pero es importante aprender la lección correcta del ejemplo.

Considere un ejemplo mucho más simple, que no tiene ningún && ninguna parte:

 const int &x = vector(1) .front(); 

.front() devuelve una & referencia al primer elemento del nuevo vector construido. El vector se destruye inmediatamente, por supuesto, y queda una referencia que cuelga.

La lección que se debe aprender es que el uso de const -reference no extiende, en general, la vida útil. Se extiende la vida útil de las no referencias . Si el lado derecho de = es una referencia, entonces usted mismo debe responsabilizarse de sus vidas.

Este siempre ha sido el caso, por lo que no tendría sentido para tuple::get a hacer algo diferente. tuple::get puede devolver una referencia, al igual que vector::front siempre ha sido.

Hablas de mover y copiar constructores y de velocidad. La solución más rápida es no utilizar constructores en absoluto. Imagina una función para concatenar dos vectores:

 vector concat(const vector &l_, const vector &r) { vector l(l_); l.insert(l.end(), r.cbegin(), r.cend()); return l; } 

Esto permitiría una sobrecarga adicional optimizada:

 vector&& concat(vector&& l, const vector &r) { l.insert(l.end(), r.cbegin(), r.cend()); return l; } 

Esta optimización mantiene el número de construcciones a un mínimo

  vector a{1,2,3}; vector b{3,4,5}; vector c = concat( concat( concat( concat(vector(), a) , b) , a , b); 

La línea final, con cuatro llamadas a concat , solo tendrá dos construcciones: el valor de inicio ( vector() ) y la construcción de movimiento en c . Podría tener 100 llamadas anidadas para concat allí, sin ninguna construcción adicional.

Entonces, regresar por && puede ser más rápido. Porque, sí, los movimientos son más rápidos que las copias, pero aún es más rápido si puedes evitar ambos.

En resumen, está hecho para la velocidad. Considere usar una serie anidada de get una tupla dentro de una tupla dentro de una tupla. Además, le permite trabajar con tipos que no tienen ni copiar ni mover constructores.

Y esto no introduce nuevos riesgos con respecto a la vida. El vector().front() “problema” no es nuevo.