2009-04-15 12 views
35

Compare las siguientes dos piezas de código, la primera utilizando una referencia a un objeto grande, y la segunda tiene el objeto grande como valor de retorno. El énfasis en un "objeto grande" se refiere al hecho de que las copias repetidas del objeto, innecesariamente, son ciclos desperdiciados.Devolución de objetos grandes en funciones

Usando una referencia a un objeto grande:

void getObjData(LargeObj& a) 
{ 
    a.reset() ; 
    a.fillWithData() ; 
} 

int main() 
{ 
    LargeObj a ; 
    getObjData(a) ; 
} 

Utilizando el objeto grande como valor de retorno:

LargeObj getObjData() 
{ 
    LargeObj a ; 
    a.fillWithData() ; 
    return a ; 
} 

int main() 
{ 
    LargeObj a = getObjData() ; 
} 

El primer fragmento de código no exige que la copia del objeto grande.

En el segundo fragmento, el objeto se crea dentro de la función y, por lo tanto, en general, se necesita una copia al devolver el objeto. En este caso, sin embargo, en main() el objeto se está declarando. ¿El compilador creará primero un objeto construido por defecto, luego copiará el objeto devuelto por getObjData(), o será tan eficiente como el primer fragmento?

Creo que el segundo fragmento es más fácil de leer, pero me temo que es menos eficiente.

Edit: Normalmente, estoy pensando en los casos LargeObj como clases de contenedor genérico que, por el bien de la discusión, contiene miles de objetos dentro de ellos. Por ejemplo,

typedef std::vector<HugeObj> LargeObj ; 

tan modificación directa/adición de métodos para LargeObj no es una solución directamente accesible.

Respuesta

40

El segundo enfoque es más idiomático y expresivo. Al leer el código, está claro que la función no tiene precondiciones en el argumento (no tiene un argumento) y que realmente creará un objeto dentro. El primer enfoque no es tan claro para el lector ocasional. La llamada implica que el objeto se cambiará (pase por referencia) pero no está tan claro si existen condiciones previas en el objeto pasado.

Sobre las copias. El código que publicaste no usa el operador de asignación, sino que más bien copia la construcción.C++ define el return value optimization que se implementa en todos los compiladores principales. Si no está seguro de que puede ejecutar el siguiente fragmento en su compilador:

#include <iostream> 
class X 
{ 
public: 
    X() { std::cout << "X::X()" << std::endl; } 
    X(X const &) { std::cout << "X::X(X const &)" << std::endl; } 
    X& operator=(X const &) { std::cout << "X::operator=(X const &)" << std::endl; } 
}; 
X f() { 
    X tmp; 
    return tmp; 
} 
int main() { 
    X x = f(); 
} 

con g ++ obtendrá una sola línea X :: X(). El compilador se reserva el espacio en la pila para el objeto x, entonces llama a la función que construye el tmp sobre x (de hecho tmpesx. Las operaciones dentro f() . (pasa por referencia) se aplican directamente sobre x, siendo equivalente a su primer fragmento de código

Si no se está usando el constructor de copia (habías escrito: X x; x = f();) entonces woul d crear tanto x y tmp y aplicar el operador de asignación, produciendo una salida de tres líneas: X :: X()/X :: X()/X :: operator =. Por lo tanto, podría ser un poco menos eficiente en los casos.

+1

Gracias por la gran explicación y el código para "probar" la optimización del valor de retorno. Puedo confirmar que el compilador de Microsoft Visual Studio 2008 arroja el mismo resultado que g ++, es decir, el conjunto optimizado de operaciones. – swongu

+0

Creo que Visual Studio solo habilitará RVO en/O2 o superior (por ejemplo, no en código de depuración y no solo en/O). – Bklyn

+5

Lo comprobé con VS2008, y descubrí que/Od (desactivado) llama a X :: X() y X :: X (X const &), mientras que/O1/O2/Ox solo llama a X :: X(). Entonces, con la optimización desactivada, la copia se realiza cuando se devuelve fuera de f(). – swongu

3

Sí, en el segundo caso hará una copia del objeto, posiblemente dos veces, una para devolver el valor de la función y nuevamente para asignarla a la copia local en main. Algunos compiladores optimizarán la segunda copia, pero en general puede suponer que se realizará al menos una copia.

Sin embargo, aún puede utilizar el segundo enfoque para mayor claridad, incluso si los datos en el objeto son grandes sin sacrificar el rendimiento con el uso adecuado de punteros inteligentes. Eche un vistazo al conjunto de clases de punteros inteligentes en potencia. De esta forma, los datos internos solo se asignan una vez y nunca se copian, incluso cuando el objeto externo sí lo está.

-1

Su primer fragmento es especialmente útil cuando hace cosas como tener getObjData() implementado en una DLL, llamarlo desde otra DLL, y las dos DLL se implementan en diferentes idiomas o diferentes versiones del compilador para el mismo idioma. La razón es porque cuando se compilan en compiladores diferentes, a menudo usan diferentes montones. Debe asignar y desasignar memoria desde el mismo montón, de lo contrario dañará la memoria. </windows>

Pero si no haces algo por el estilo, yo normalmente devuelva un puntero (o puntero inteligente) a la memoria de su función reserva:

LargeObj* getObjData() 
{ 
    LargeObj* ret = new LargeObj; 
    ret->fillWithData() ; 
    return ret; 
} 

... a menos que tenga una razón específica No a.

+0

LOL @ el downvote. –

+0

Probablemente no, por alguna razón, pensé que OP lo hizo por un nuevo objeto de pila. –

11

Utilice el segundo enfoque. Puede parecer que es menos eficiente, pero el estándar de C++ permite que las copias sean evadidas. Esta optimización se llama Named Return Value Optimization y se implementa en la mayoría de los compiladores actuales.

0

Como alternativa, puede evitar este problema al permitir que el objeto obtenga sus propios datos, i. mi. haciendo getObjData() una función miembro de LargeObj. Dependiendo de lo que realmente está haciendo, esta puede ser una buena forma de hacerlo.

+0

Debería haber mencionado que en los casos que estoy considerando, LargeObj es algún tipo de clase de contenedor estándar con una gran cantidad de entradas (como std :: vector <>) y no es una clase que pueda modificar. – swongu

+0

En ese caso, pasaría por referencia. Para mí devolver una copia de un contenedor grande simplemente se siente muy mal. Otra forma sería tener una clase contenedora, que el contenedor y getObjData() como miembros. – Dima

2

La forma de evitar cualquier copia es proporcionar un constructor especial. Si puede volver a escribir el código por lo que parece:

LargeObj getObjData() 
{ 
    return LargeObj(fillsomehow()); 
} 

Si fillsomehow() devuelve los datos (tal vez una "gran cadena" luego tener un constructor que toma un "gran cadena" Si usted tiene. Tal constructor, entonces el compilador muy probablemente construirá un solo objeto y no hará ninguna copia para realizar la devolución. Por supuesto, si esto es útil en la vida real depende de su problema particular.

+0

Muéstremelo :-) –

0

Dependiendo de qué tan grande el objeto realmente es y con qué frecuencia ocurre la operación, no se empantanen demasiado en eficiencia cuando no tendrá ningún efecto discernible de cualquier forma. La optimización a expensas de un código limpio y legible solo debería ser feliz. es cuando se determina que es necesario.

0

Lo más probable es que algunos ciclos se desperdicien cuando regrese por copia. Si vale la pena preocuparse depende de qué tan grande es realmente el objeto y con qué frecuencia invocas este código.

Pero me gustaría señalar que si LargeObj es una clase grande y no trivial, a continuación, en cualquier caso, su constructor vacíos se deben inicializar a un estado conocido:

LargeObj::LargeObj() : 
m_member1(), 
m_member2(), 
... 
{} 

que los residuos de una algunos ciclos también. Vuelva a escribir el código como

LargeObj::LargeObj() 
{ 
    // (The body of fillWithData should ideally be re-written into 
    // the initializer list...) 
    fillWithData() ; 
} 

int main() 
{ 
    LargeObj a ; 
} 

probablemente sería un ganar-ganar para usted: usted tendría los casos LargeObj conseguir inicializan en estados conocidos y útiles, y usted tendría un menor número de ciclos desperdiciados.

Si no siempre quiere usar fillWithData() en el constructor, puede pasar un indicador al constructor como argumento.

ACTUALIZACIÓN (de su edición de comentarios &): Semánticamente, si vale la pena crear un typedef para LargeObj - es decir, para darle un nombre, en lugar de hacer referencia a él simplemente como typedef std::vector<HugeObj> - entonces ya se encuentra en el camino de darle su propia semántica conductual. Podría, por ejemplo, definirlo como

class LargeObj : public std::vector<HugeObj> { 
    // constructor that fills the object with data 
    LargeObj() ; 
    // ... other standard methods ... 
}; 

Solo usted puede determinar si esto es apropiado para su aplicación. Mi punto es que a pesar de que LargeObj es "en su mayoría" un contenedor, aún puede darle un comportamiento de clase si hacerlo funciona para su aplicación.

+0

Debería haber mencionado que en los casos que estoy considerando, LargeObj es algún tipo de clase de contenedor estándar con una gran cantidad de entradas (como std :: vector <>) y no es algo que puedo agregar un constructor a. – swongu

+0

Eso no excluye agregar un constructor - ver mi actualización. –

+0

Derivar públicamente de contenedores STL generalmente es desaprobado. La falta de destructores virtuales es una de las razones, google puede darle más. – avakar

2

Una solución algo idiomática sería:

std::auto_ptr<LargeObj> getObjData() 
{ 
    std::auto_ptr<LargeObj> a(new LargeObj); 
    a->fillWithData(); 
    return a; 
} 

int main() 
{ 
    std::auto_ptr<LargeObj> a(getObjData()); 
} 
Cuestiones relacionadas