¿Cómo "devolver un objeto" en C ++?

167

Sé que el título suena familiar ya que hay muchas preguntas similares, pero estoy pidiendo un aspecto diferente del problema (sé la diferencia entre tener cosas en la pila y ponerlas en el montón).

En Java siempre puedo devolver referencias a objetos "locales"

public Thing calculateThing() {
    Thing thing = new Thing();
    // do calculations and modify thing
    return thing;
}

En C ++, para hacer algo similar tengo 2 opciones

(1) Puedo usar referencias siempre que necesite "devolver" un objeto

void calculateThing(Thing& thing) {
    // do calculations and modify thing
}

Entonces úsalo así

Thing thing;
calculateThing(thing);

(2) O puedo devolver un puntero a un objeto asignado dinámicamente

Thing* calculateThing() {
    Thing* thing(new Thing());
    // do calculations and modify thing
    return thing;
}

Entonces úsalo así

Thing* thing = calculateThing();
delete thing;

Usando el primer enfoque, no tendré que liberar memoria manualmente, pero para mí hace que el código sea difícil de leer. El problema con el segundo enfoque es que tendré que recordarlo delete thing;, lo que no se ve muy bien. No quiero devolver un valor copiado porque es ineficiente (creo), así que aquí vienen las preguntas

  • ¿Hay una tercera solución (que no requiera copiar el valor)?
  • ¿Hay algún problema si me quedo con la primera solución?
  • ¿Cuándo y por qué debería usar la segunda solución?
phunehehe
fuente
32
+1 por hacer una pregunta agradable.
Kangkan
1
Para ser muy pedante, es un poco impreciso decir que "las funciones devuelven algo". Más correctamente, evaluar una llamada de función produce un valor . El valor siempre es un objeto (a menos que sea una función nula). La distinción es si el valor es un glvalue o un prvalue, que está determinado por si el tipo de retorno declarado es una referencia o no.
Kerrek SB

Respuestas:

107

No quiero devolver un valor copiado porque es ineficiente

Pruébalo.

Busque RVO y NRVO, y en C ++ 0x move-semántica. En la mayoría de los casos en C ++ 03, un parámetro de salida es solo una buena manera de hacer que su código sea feo, y en C ++ 0x realmente se estaría lastimando al usar un parámetro de salida.

Simplemente escriba código limpio, devuelva por valor. Si el rendimiento es un problema, perfílelo (deje de adivinar) y encuentre lo que puede hacer para solucionarlo. Es probable que no devuelva cosas de las funciones.


Dicho esto, si estás empeñado en escribir así, probablemente quieras hacer el parámetro out. Evita la asignación dinámica de memoria, que es más segura y generalmente más rápida. Requiere que tenga alguna forma de construir el objeto antes de llamar a la función, lo que no siempre tiene sentido para todos los objetos.

Si desea utilizar la asignación dinámica, lo menos que se puede hacer es ponerla en un puntero inteligente. (De todos modos, esto debe hacerse todo el tiempo). Entonces no te preocupes por eliminar nada, las cosas son seguras, etc. ¡El único problema es que es más lento que regresar por valor de todos modos!

GManNickG
fuente
10
@phunehehe: No hay ningún punto especulando, debe perfilar su código y averiguarlo. (Sugerencia: no.) Los compiladores son muy inteligentes, no van a perder el tiempo copiando cosas si no es necesario. Incluso si la copia cuesta algo, aún debe esforzarse por obtener un buen código sobre un código rápido; Un buen código es fácil de optimizar cuando la velocidad se convierte en un problema. No tiene sentido poner código desagradable para algo que no tienes idea es un problema; especialmente si realmente lo ralentizas o no obtienes nada. Y si está utilizando C ++ 0x, la semántica de movimiento hace que esto no sea un problema.
GManNickG
1
@GMan, re: RVO: en realidad esto solo es cierto si la persona que llama y la persona que llama están en la misma unidad de compilación, que en el mundo real no es la mayor parte del tiempo. Entonces, te decepcionará si tu código no está todo diseñado (en cuyo caso todo estará en una unidad de compilación) o si tienes alguna optimización de tiempo de enlace (GCC solo lo tiene desde 4.5).
Alex B
2
@Alex: los compiladores están mejorando cada vez más en la optimización de las unidades de traducción. (VC lo hace para varios lanzamientos ahora.)
sbi
9
@ Alex B: Esto es basura completa. Muchas convenciones de llamadas muy comunes hacen que la persona que llama sea responsable de asignar espacio para grandes valores de retorno y la persona que llama responsable de su construcción. RVO funciona felizmente en unidades de compilación incluso sin optimizaciones de tiempo de enlace.
CB Bailey
66
@Charles, ¡al comprobar que parece correcto! Retiro mi declaración claramente desinformada.
Alex B
41

Simplemente cree el objeto y devuélvalo

Thing calculateThing() {
    Thing thing;
    // do calculations and modify thing
     return thing;
}

Creo que te harás un favor si te olvidas de la optimización y solo escribes código legible (necesitarás ejecutar un generador de perfiles más adelante, pero no optimices previamente).

Amir Rachum
fuente
2
Thing thing();declara una función local y devuelve a Thing.
dreamlax
2
Cosa () declara que una función devuelve una cosa. No hay ningún objeto Thing construido en su cuerpo funcional.
CB Bailey
@dreamlax @Charles @GMan Un poco tarde, pero arreglado.
Amir Rachum
¿Cómo funciona esto en C ++ 98? ¡Recibo errores en el intérprete de CINT y me preguntaba si se debe a C ++ 98 o al propio CINT ...!
xcorat
16

Solo devuelve un objeto como este:

Thing calculateThing() 
{
   Thing thing();
   // do calculations and modify thing
   return thing;
}

Esto invocará el constructor de copia en Things, por lo que es posible que desee hacer su propia implementación de eso. Me gusta esto:

Thing(const Thing& aThing) {}

Esto podría funcionar un poco más lento, pero podría no ser un problema en absoluto.

Actualizar

El compilador probablemente optimizará la llamada al constructor de la copia, por lo que no habrá sobrecarga adicional. (Como dreamlax señaló en el comentario).

Martin Ingvar Kofoed Jensen
fuente
9
Thing thing();declara una función local que devuelve un Thing, también, el estándar permite que el compilador omita el constructor de copia en el caso que usted presentó; cualquier compilador moderno probablemente lo hará.
dreamlax
1
Trae un buen punto para implementar el constructor de copia, especialmente si se necesita una copia profunda.
mbadawi23
+1 por declarar explícitamente sobre el constructor de la copia, aunque como dice @dreamlax el compilador probablemente "optimizará" el código de retorno para las funciones evitando una llamada no realmente necesaria al constructor de la copia.
jose.angel.jimenez
En 2018, en VS 2017, está tratando de usar el constructor de movimientos. Si el constructor de movimiento se elimina y el constructor de copia no, no se compilará.
Andrew
11

¿Intentó utilizar punteros inteligentes (si Thing es un objeto realmente grande y pesado), como auto_ptr:


std::auto_ptr<Thing> calculateThing()
{
  std::auto_ptr<Thing> thing(new Thing);
  // .. some calculations
  return thing;
}


// ...
{
  std::auto_ptr<Thing> thing = calculateThing();
  // working with thing

  // auto_ptr frees thing 
}
demetrios
fuente
44
auto_ptrs están en desuso; usar shared_ptro en su unique_ptrlugar.
MBraedley
Solo voy a agregar esto aquí ... He estado usando c ++ durante años, aunque no profesionalmente con c ++ ... He decidido intentar no usar más punteros inteligentes, son un desastre absoluto y causan todo tipo de problemas, que tampoco ayudan a acelerar mucho el código. Prefiero simplemente copiar datos y administrar los punteros yo mismo, utilizando RAII. Por lo tanto, le aconsejo que, si puede, evite los punteros inteligentes.
Andrew
8

Una forma rápida de determinar si se llama a un constructor de copias es agregar el registro al constructor de copias de su clase:

MyClass::MyClass(const MyClass &other)
{
    std::cout << "Copy constructor was called" << std::endl;
}

MyClass someFunction()
{
    MyClass dummy;
    return dummy;
}

Llamada someFunction; el número de líneas "Copiar constructor se llamó" que obtendrá variará entre 0, 1 y 2. Si no obtiene ninguna, entonces su compilador ha optimizado el valor de retorno (lo que está permitido). Si obtiene no reciben 0, y el constructor de copia es ridículamente caro, a continuación, buscar formas alternativas para devolver las instancias de sus funciones.

dreamlax
fuente
1

En primer lugar, tiene un error en el código, quiere decir que lo tiene Thing *thing(new Thing());, y solo return thing;.

  • Uso shared_ptr<Thing>. Lo dejó como si fuera un puntero. Se eliminará cuando la última referencia aThing contenido quede fuera de alcance.
  • La primera solución es muy común en bibliotecas ingenuas. Tiene algo de rendimiento y sobrecarga sintáctica, evítela si es posible
  • Use la segunda solución solo si puede garantizar que no se lanzarán excepciones, o cuando el rendimiento sea absolutamente crítico (estará interactuando con C o ensamblaje antes de que esto sea relevante).
Matt Joiner
fuente
0

Estoy seguro de que un experto en C ++ vendrá con una mejor respuesta, pero personalmente me gusta el segundo enfoque. El uso de punteros inteligentes ayuda con el problema de olvidar deletey, como usted dice, parece más limpio que tener que crear un objeto de antemano (y aún tener que eliminarlo si desea asignarlo en el montón).

EMP
fuente