2012-05-07 14 views
20

Tengo una pregunta muy básica en C++. ¿Cómo evitar la copia al devolver un objeto?C++: evitar la copia con la declaración "return"

Aquí es un ejemplo:

std::vector<unsigned int> test(const unsigned int n) 
{ 
    std::vector<unsigned int> x; 
    for (unsigned int i = 0; i < n; ++i) { 
     x.push_back(i); 
    } 
    return x; 
} 

Según entiendo cómo funciona C++, esta función crear 2 vectores: la local (x), y la copia de x que será devuelto. ¿Hay alguna manera de evitar la copia? (y no quiero devolver un puntero a un objeto, sino el objeto en sí)

Muchas gracias.

EDIT: pregunta adicional de acuerdo con las primeras respuestas: ¿cuál sería la sintaxis de esa función usando "mover semántica"?

+0

semántica de movimiento: http://www2.research.att.com/~bs/C++0xFAQ.html#rval – chris

+0

No necesariamente creará una copia. NRVO o movimientos simbólicos pueden prevenir eso. –

+1

Puede confiar en su compilador para realizar magia de NRVO o usar explícitamente la semántica de movimiento. –

Respuesta

14

Este programa puede aprovechar la optimización del valor de retorno con nombre (NRVO). Vea aquí: http://en.wikipedia.org/wiki/Copy_elision

En C++ 11 hay constructores de movimiento y asignación que también son baratos. Puede leer un tutorial aquí: http://thbecker.net/articles/rvalue_references/section_01.html

+1

Debe tenerse en cuenta que no todos los compiladores harán esto e incluso aquellos que lo hagan no lo harán todo el tiempo. Puede que valga la pena investigar en IFF, el objeto es grande, está anotando copias, y el perfil muestra que es un cuello de botella significativo. –

13

Named Return Value Optimization hará el trabajo para usted ya el compilador trata de eliminar redundante constructor de copia y Destructor llama mientras lo usa.

std::vector<unsigned int> test(const unsigned int n){ 
    std::vector<unsigned int> x; 
    return x; 
} 
... 
std::vector<unsigned int> y; 
y = test(10); 

con la optimización del valor de retorno:

  1. y se crea
  2. x se crea
  3. x se le asigna en Y
  4. x se destruye

(en el caso desea probarlo usted mismo para una comprensión más profunda, consulte this example of mine)

o incluso mejor, al igual que Matthieu M. señaló, si se llama a test dentro de la misma línea en la que se declara y, también puede evitar la construcción de objetos redundantes y asignación redundante así (x se construirá dentro de la memoria, donde y se almacenarán):

std::vector<unsigned int> y = test(10); 

comprobar su respuesta para una mejor comprensión de esa situación (también encontrará que este tipo de optimización no siempre se puede aplicar).

O se podría modificar el código para pasar la referencia del vector para su función, lo que sería semánticamente más correcto, evitando copiar:

void test(std::vector<unsigned int>& x){ 
    // use x.size() instead of n 
    // do something with x... 
} 
... 
std::vector<unsigned int> y; 
test(y); 
+0

Ah, ya veo. De hecho, ignoré el hecho de que primero construiste un 'y' predeterminado antes de asignarlo. Su secuencia de eventos es correcta en este caso, aunque recomendaría que simplemente inicialice 'y' para evitar crear dos objetos donde uno sería suficiente. Perdón por el ruido. –

+1

@MatthieuM .: Aunque aprecio tu punto. Comprueba mi respuesta ahora :) – LihO

+2

No puedo votar dos veces :( –

-5

En primer lugar, se podría declarar el tipo de retorno a std :: vector & en cuyo caso se devolverá una referencia en lugar de una copia.

También podría definir un puntero, construir un puntero dentro del cuerpo de su método y luego devolver ese puntero (o una copia de ese puntero para que sea correcto).

Finalmente, muchos compiladores de C++ pueden optimizar el valor de retorno (http://en.wikipedia.org/wiki/Return_value_optimization) eliminando el objeto temporal en algunos casos.

+4

Lástima que la referencia sería ilegal de usar (UB). -1 por un mal consejo. – Mankarse

1

Haciendo referencia funcionaría.

Void(vector<> &x) { 

} 
31

Parece haber cierta confusión sobre cómo funciona el RVO (Return Value Optimization).

Un ejemplo sencillo:

#include <iostream> 

struct A { 
    int a; 
    int b; 
    int c; 
    int d; 
}; 

A create(int i) { 
    A a = {i, i+1, i+2, i+3 }; 
    std::cout << &a << "\n"; 
    return a; 
} 

int main(int argc, char*[]) { 
    A a = create(argc); 
    std::cout << &a << "\n"; 
} 

y su salida al ideone:

0xbf928684 
0xbf928684 

Sorprendente?

realidad, que es el efecto de RVO: el objeto a ser devuelto se construye directamente en lugar en la persona que llama.

¿Cómo?

Tradicionalmente, la persona que llama (main aquí) se reserva espacio en la pila para obtener el valor de retorno: la ranura de retorno ; el destinatario (create aquí) pasa (de alguna manera) la dirección de la ranura de devolución para copiar su valor de retorno. A continuación, el destinatario asigna su propio espacio para la variable local en la que genera el resultado, como para cualquier otra variable local, y luego lo copia en el espacio de retorno en la instrucción return.

RVO se activa cuando el compilador deduce del código que la variable se puede construir directamente en el ranura de retorno con semántica equivalente (la regla de si-si).

Tenga en cuenta que esta es una optimización tan común que está explícitamente incluida en la lista blanca por el estándar y el compilador no tiene que preocuparse por los posibles efectos secundarios del constructor de copia (o movimiento).

¿Cuándo?

El compilador es más probable que utilice reglas simples, tales como:

// 1. works 
A unnamed() { return {1, 2, 3, 4}; } 

// 2. works 
A unique_named() { 
    A a = {1, 2, 3, 4}; 
    return a; 
} 

// 3. works 
A mixed_unnamed_named(bool b) { 
    if (b) { return {1, 2, 3, 4}; } 

    A a = {1, 2, 3, 4}; 
    return a; 
} 

// 4. does not work 
A mixed_named_unnamed(bool b) { 
    A a = {1, 2, 3, 4}; 

    if (b) { return {4, 3, 2, 1}; } 

    return a; 
} 

En este último caso (4), la optimización no se puede aplicar cuando A se devuelve porque el compilador no puede construir a en el ranura de devolución, ya que puede necesitarlo para otra cosa (dependiendo de la condición booleana b).

Una simple regla de oro es de esta manera que:

RVO debe aplicarse si no hay otro candidato a la ranura de retorno ha sido declarado con anterioridad a la declaración return.

+0

+1 por señalar que no solo podemos evitar copiar, sino construir de objeto redundante y asignación redundante también;) – LihO

+0

Re caso (4), depende del compilador (qué tan inteligente es) y los detalles del código. Por ejemplo, con el código concreto que muestra, un compilador inteligente puede observar que la inicialización de 'a' no tiene efectos secundarios, y que esa declaración se puede bajar debajo de' if'. –

+0

@ Cheersandhth.-Alf: Exacto, la regla de si-si todavía se encuentra obviamente. Sin embargo, en el caso general (constructor fuera de línea), esto solo sería deducible con LTO habilitado. –

Cuestiones relacionadas