c++ return rvalue-reference stdmove

c++ - ¿Debo devolver una referencia de valor(por std:: move''ing)?



return rvalue-reference (4)

Una publicación de C ++ Next dijo que

A compute(…) { A v; … return v; }

Si A tiene un constructor accesible para copiar o mover, el compilador puede optar por ignorar la copia. De lo contrario, si A tiene un constructor de movimiento, v se mueve. De lo contrario, si A tiene un constructor de copia, v se copia. De lo contrario, se emite un error de tiempo de compilación.

Pensé que siempre debería devolver el valor sin std::move porque el compilador podría encontrar la mejor opción para los usuarios. Pero en otro ejemplo de la entrada del blog.

Matrix operator+(Matrix&& temp, Matrix&& y) { temp += y; return std::move(temp); }

Aquí el std::move es necesario porque y debe ser tratado como un valor dentro de la función.

Ah, mi cabeza casi explota después de estudiar esta publicación de blog. Hice lo mejor que pude para entender el razonamiento, pero cuanto más estudiaba, más confundido estaba. ¿Por qué deberíamos devolver el valor con la ayuda de std::move ?


En C ++ 17 y posteriores.

C ++ 17 cambió la definición de las categorías de valor para garantizar el tipo de elision de copia que describe (consulte: ¿Cómo funciona la elision de copia garantizada? ). Por lo tanto, si escribe su código en C ++ 17 o posterior, no debe devolverlo por std::move() ''ing en este caso.


Entonces, digamos que tienes:

A compute() { A v; … return v; }

Y que estás haciendo:

A a = compute();

Hay dos transferencias (copiar o mover) que están involucradas en esta expresión. Primero, el objeto denotado por v en la función debe transferirse al resultado de la función, es decir, el valor donado por la expresión compute() . Llamemos a esa Transferencia 1. Luego, este objeto temporal se transfiere para crear el objeto denotado por - Transferencia 2.

En muchos casos, tanto la Transferencia 1 como la 2 pueden ser eliminadas por el compilador: el objeto v se construye directamente en la ubicación de a y no es necesaria la transferencia. El compilador debe utilizar la Optimización de valor de retorno con nombre para la Transferencia 1 en este ejemplo, porque el objeto que se devuelve tiene el nombre. Sin embargo, si deshabilitamos la opción de copiar / mover, cada transferencia implica una llamada al constructor de copia de A o al constructor de movimiento. En la mayoría de los compiladores modernos, el compilador verá que v está a punto de ser destruido y primero lo moverá al valor de retorno. Entonces este valor de retorno temporal se moverá a a . Si A no tiene un constructor de movimiento, en su lugar se copiará para ambas transferencias.

Ahora veamos:

A compute(A&& v) { return v; }

El valor que estamos devolviendo proviene de la referencia que se pasa a la función. El compilador no solo asume que v es temporal y que está bien moverse de él 1 . En este caso, la Transferencia 1 será una copia. Entonces, la Transferencia 2 será un movimiento, está bien porque el valor devuelto sigue siendo temporal (no devolvimos una referencia). Pero como sabemos que hemos tomado un objeto del que podemos movernos, porque nuestro parámetro es una referencia de valor, podemos decirle explícitamente al compilador que trate v como un temporal con std::move :

A compute(A&& v) { return std::move(v); }

Ahora tanto la Transferencia 1 como la Transferencia 2 serán movidas.

1 La razón por la que el compilador no trata automáticamente v , definido como A&& , como un valor es uno de seguridad. No es solo demasiado estúpido para averiguarlo. Una vez que un objeto tiene un nombre, se lo puede consultar varias veces a lo largo de su código. Considerar:

A compute(A&& a) { doSomething(a); doSomethingElse(a); }

Si a se tratara automáticamente como un rvalor, doSomething tendría la libertad de desgarrar sus entrañas, lo que significa que el pasar a doSomethingElse puede no ser válido. Incluso si doSomething tomó su argumento por valor, el objeto se movería y, por lo tanto, sería inválido en la siguiente línea. Para evitar este problema, las referencias de rvalue con nombre son lvalues. Eso significa que cuando se llama doSomething , en el peor de los casos se copia, si no se toma solo por lvalue reference, aún será válido en la siguiente línea.

Corresponde al autor de compute decir, "está bien, ahora permito que se mueva este valor, porque sé con certeza que es un objeto temporal". Haces esto diciendo std::move(a) . Por ejemplo, podría darle a doSomething una copia y luego permitir que doSomethingElse mueva de ella:

A compute(A&& a) { doSomething(a); doSomethingElse(std::move(a)); }


La primera toma ventaja de NVRO, que es incluso mejor que moverse. Ninguna copia es mejor que una barata.

El segundo no puede aprovecharse de NVRO. Suponiendo que no hay elección, la return temp; llamaría al constructor de copia y return std::move(temp); Llamaría al constructor de movimientos. Ahora, creo que cualquiera de estos tiene el mismo potencial para ser eliminado y, por lo tanto, debería ir con el más barato si no lo hace, que es usar std::move .


Un movimiento implícito de resultados de función solo es posible para objetos automáticos. Un parámetro de referencia rvalue no denota un objeto automático, por lo tanto, debe solicitar el movimiento explícitamente en ese caso.