2012-08-05 8 views
38

Considere un tipo T que admita la semántica de movimiento predeterminada. Considera también la función a continuación:Usar std :: mover() al devolver un valor de una función para evitar copiar

T f() { 
    T t; 
    return t; 
} 

T o = f(); 

En el antiguo C++ 03, algunos compiladores no óptimas pueden llamar al constructor de copia dos veces, una para el "objeto de retorno" y otra para o.

En C++ 11, dado que t dentro de f() es un lvalue, esos compiladores pueden llamar al constructor de copia una vez como antes, y luego llamar al constructor de movimientos para o.

¿Es correcto afirmar que la única forma de evitar la primera "copia adicional" es mover t al regresar?

T f() { 
    T t; 
    return std::move(t); 
} 
+0

[aquí] (http://stackoverflow.com/questions/9827183/why-am-i-allowed-to-copy-unique-ptr) es una pregunta similar –

+0

[FAQ relacionado] (http://stackoverflow.com/a/11540204/252000) – fredoverflow

Respuesta

42

No. Cada vez que una variable local en un comunicado return es elegible para la copia elisión, que se une a un valor p re fe ­ ­ rencia, y por lo tanto return t; es idéntica a return std::move(t); en su ejemplo, con respecto a la cual son constructores elegible.

Nota sin embargo, que return std::move(t); previene el compilador de ejercer copia elisión, mientras que return t; no, y por lo tanto este último es el estilo preferido. [Gracias a @Johannes por el cor ­ rect ­ ion.] Si se produce una elisión de copia, la cuestión de si se usa o no la construcción de movimiento se convierte en un punto discutible.

Ver 12.8 (31, 32) en la norma.

Tenga en cuenta también que si tiene un T contra copias accesibles, pero un movimiento constructor de borrado, entonces no habrá return t; com ­ pila, porque el constructor movimiento debe considerarse en primer lugar; que tendría que decir algo a la EF ­ fect de return static_cast<T&>(t); para hacer que funcione:

T f() 
{ 
    T t; 
    return t;     // most likely elided entirely 
    return std::move(t);  // uses T::T(T &&) if defined; error if deleted or inaccessible 
    return static_cast<T&>(t) // uses T::T(T const &) 
} 
+0

Gracias. ¿Qué pasa con la cantidad de llamadas al constructor de movimientos? ¿Pueden ser dos como el número de llamadas para copiar constructores con algunos compiladores complacientes de C++ 03? – Martin

+12

Es * no * idéntico. Solo es idéntico con respecto a si se puede llamar al constructor de movimiento. Si escribe 'return std :: move (t);' el constructor de movimiento * debe * ser llamado si el compilador no sabe lo que hace. Si escribe 'return t;', la llamada del constructor de movimientos puede eludirse incluso si pudiera tener efectos secundarios. –

+0

@ JohannesSchaub-litb: Buen punto, déjame editar eso. –

3

Ok, me gustaría dejar un comentario sobre esta. Esta pregunta (y la respuesta) me hizo creer que no es necesario especificar std::move en la declaración de devolución. Sin embargo, solo me pareció una lección diferente al tratar con mi código.

Por lo tanto, tengo una función (en realidad es una especialización) que toma una temporal y simplemente la devuelve. (La plantilla de función general hace otras cosas, pero la especialización hace la operación de identidad).

template<> 
struct CreateLeaf<A> 
{ 
    typedef A Leaf_t; 
    inline static 
    Leaf_t make(A &&a) { 
    return a; 
    } 
}; 

Ahora, esta versión llama al constructor de copia de A a su regreso. Si cambio la instrucción de retorno a

Leaf_t make(A &&a) { 
    return std::move(a); 
} 

A continuación, el constructor del movimiento A se llama y me puede hacer algunas optimizaciones allí.

Es posible que no coincida al 100% con su pregunta. Pero es falso pensar que return std::move(..) nunca es necesario. Solía ​​pensar así. No más ;-)

+0

Esto es diferente de la pregunta original. La pregunta original era sobre return x, donde x es una variable local. Cuando x es una variable local, return x es mejor porque el compilador tratará x como un valor r dentro de la declaración porque sabe que x es un local. Cuando x es una referencia, el compilador no le dará un tratamiento especial. Y dado que el tipo de la variable "a" en su ejemplo es "A" y "A", debe usar mover para cambiarlo a "A &&". –

8

No. La mejor práctica es directamente return t;.

En caso T clase tiene mover el constructor no se eliminan, y el aviso t es una variable local que return t es elegible para la copia elisión, construcciones que se mueva el objeto devuelto al igual que lo hace return std::move(t);. Sin embargo, return t; sigue siendo elegible para copiar/mover elisión, por lo que la construcción puede omitirse, mientras que return std::move(t) siempre construye el valor de retorno utilizando el constructor de movimientos.

En caso de movimiento del constructor en la clase T se elimina pero constructor de copia disponible, return std::move(t); no se compilará, mientras que todavía return t; compila utilizando constructor de copia. A diferencia de @Kerrek mencionado, t no está vinculado a una referencia rvalue. Hay una resolución de sobrecarga de dos etapas para los valores de retorno que son elegibles para la elisión de copia: intente mover primero, luego copie, y tanto mover como copiar posiblemente se eliminen.

class T 
{ 
public: 
    T() = default; 
    T (T&& t) = delete; 
    T (const T& t) = default; 
}; 

T foo() 
{ 
    T t; 
    return t;     // OK: copied, possibly elided 
    return std::move(t);  // error: move constructor deleted 
    return static_cast<T&>(t); // OK: copied, never elided 
} 

Si la expresión es return lvalue y no es elegible para la copia elisión (muy probablemente va a devolver una variable no local o expresión valor-I) y todavía se desea evitar la copia, std::move será útil. Pero tenga en cuenta que la mejor práctica es hacer que la elisión de copia sea posible.

class T 
{ 
public: 
    T() = default; 
    T (T&& t) = default; 
    T (const T& t) = default; 
}; 

T bar(bool k) 
{ 
    T a, b; 
    return k ? a : b;   // lvalue expression, copied 
    return std::move(k ? a : b); // moved 
    if (k) 
     return a;    // moved, and possibly elided 
    else 
     return b;    // moved, and possibly elided 
} 

12.8 (32) en el estándar describe el proceso.

12,8 [class.copy]

32 Cuando se cumplen los criterios para la elisión de una operación de copia o se cumplirían, salvo por el hecho de que el objeto de origen es un parámetro de función, y el objeto a ser copiado se designa con un lvalue, la resolución de sobrecarga para seleccionar el constructor para la copia se realiza primero como si el objeto fuera designado por un valor r. Si la resolución de sobrecarga falla, o si el tipo del primer parámetro del constructor seleccionado no es una referencia rvalue al tipo del objeto (posiblemente cv-qualified), la resolución de sobrecarga se realiza nuevamente, considerando el objeto como un valor l. [Nota: esta resolución de sobrecarga de dos etapas se debe realizar independientemente de si se producirá elisión de copia. Determina el nombre del constructor que se invocará si no se realiza una elisión, y el constructor seleccionado debe estar accesible incluso si se elimina la llamada. nota -fin]

Cuestiones relacionadas