c++ - ¿Por qué se llama al constructor de copia en lugar del constructor de movimiento cuando se devuelve?
c++11 return (3)
Ahora estoy devolviendo esta clase a través de lvalue como este:
MyClass func() { return MyClass(); }
No, la expresión devuelta es un xvalue (un tipo de rvalue), que se usa para inicializar el resultado de retorno por valor (las cosas son un poco más complicadas desde C ++ 17, pero esto sigue siendo la esencia de esto; además, estás en C ++ 11).
En este caso, se llama al constructor de movimiento cuando se devuelve el objeto de clase y todo funciona como se espera.
En efecto; un rvalue inicializará una referencia de rvalue y, por lo tanto, todo puede coincidir con los constructores de movimientos.
Cuando cambio el código de arriba:
... ahora la expresión es
MyClass() << 5
, que tiene el tipo
MyClass&
.
Esto nunca es un valor.
Es un valor
Es una expresión que se refiere a un objeto existente.
Entonces, sin un
std::move
explícito, se usará para
copiar-inicializar
el resultado.
Y, dado que se elimina su constructor de copia, eso no puede funcionar.
Me sorprende que el ejemplo se compile, ya que no se puede usar un temporal para inicializar una referencia de valor l (el primer argumento de su operador), aunque se sabe que algunas cadenas de herramientas (MSVS) aceptan esto como una extensión.
luego devolvería
std::move(MyClass() << 5);
¿trabajo?
Sí, así lo creo.
Sin embargo, esto es muy extraño de ver y hace que el lector vuelva a verificar para asegurarse de que no haya referencias pendientes. Esto sugiere que hay una mejor manera de lograr esto que resulta en un código más claro:
MyClass func()
{
MyClass m;
m << 5;
return m;
}
Ahora todavía se está moviendo (porque
esa es una regla especial cuando se
return
variables locales
) sin ninguna travesura extraña.
Y, como beneficio adicional, la llamada
<<
es completamente compatible con el estándar.
Digamos que tengo la clase
MyClass
con un constructor de movimiento correcto y cuyo constructor de copia se elimina.
Ahora estoy regresando esta clase así:
MyClass func()
{
return MyClass();
}
En este caso, se llama al constructor de movimiento cuando se devuelve el objeto de clase y todo funciona como se espera.
Ahora digamos que
MyClass
tiene una implementación del operador
<<
:
MyClass& operator<<(MyClass& target, const int& source)
{
target.add(source);
return target;
}
Cuando cambio el código de arriba:
MyClass func()
{
return MyClass() << 5;
}
Recibo el error del compilador, que no se puede acceder al constructor de copia porque se eliminó. Pero, ¿por qué se utiliza el constructor de copia en este caso?
Su
operator<<
toma su primer parámetro como una referencia no constante.
No puede enlazar una referencia no constante a una temporal.
Pero
MyClass()
devuelve la instancia recién creada como temporal.
Además, mientras
func
devuelve un valor, el
operator<<
devuelve una referencia.
Entonces, ¿qué más puede hacer sino hacer una copia para devolver?
Su operador regresa por
MyClass&
.
Por lo tanto, está devolviendo un lvalue, no un rvalue que se puede mover automáticamente.
Puede evitar la copia confiando en las garantías estándar relativas a NRVO.
MyClass func()
{
MyClass m;
m << 5;
return m;
}
Esto ocultará el objeto por completo o lo moverá. Todo a causa de que es un objeto de función local.
Otra opción, dado que está intentando llamar al
operator<<
en un valor de r, es proporcionar una sobrecarga que se ocupe de las referencias de valor.
MyClass&& operator<<(MyClass&& target, int i) {
target << i; // Reuse the operator you have, here target is an lvalue
return std::move(target);
}
Eso hará que
MyClass() << 5
esté bien formada (vea la otra respuesta para saber por qué no lo está), y devolverá un xvalor a partir del cual se puede construir el objeto de retorno.
Aunque tal y la sobrecarga para el
operator<<
no se ve comúnmente.