18

Las referencias en C++ me desconciertan. :)C++: devolver por referencia y copiar constructores

La idea básica es que estoy tratando de devolver un objeto desde una función. Me gustaría hacerlo sin devolver un puntero (porque entonces tendría que hacerlo manualmente delete), y sin llamar al constructor de copias, si es posible (por eficiencia, naturalmente agregado: y también porque me pregunto si no puede evitar escribir un constructor de copia).

Así que, en definitiva, aquí están las opciones para hacer esto que he encontrado:

  • El tipo de retorno de la función puede ser o bien la propia clase (MyClass fun() { ... }) o una referencia a la clase (MyClass& fun() { ... }) .
  • La función puede construir la variable en la línea de retorno (return MyClass(a,b,c);) o devolver una variable existente (MyClass x(a,b,c); return x;).
  • El código que recibe la variable también puede tener una variable de cualquier tipo: (MyClass x = fun(); o MyClass& x = fun();)
  • El código que recibe la variable puede crear una nueva variable sobre la marcha (MyClass x = fun();) o asignarla a una ya existente variables (MyClass x; x = fun();)

Y algunas ideas al respecto:

  • parece ser una mala idea tener el tipo de retorno MyClass& debido a que siempre resultan s en la variable que se destruye antes de que se devuelva.
  • El constructor de copia solo parece involucrarse cuando devuelvo una variable existente. Al devolver una variable construida en la línea de retorno, nunca se llama.
  • Cuando asigno el resultado a una variable existente, el destructor también se activa siempre antes de que se devuelva el valor. Además, no se llama a ningún constructor de copia, pero la variable de destino recibe los valores de miembro del objeto devuelto por la función.

Estos resultados son tan inconsistentes que me siento totalmente confundido. Entonces, ¿qué EXACTAMENTE está pasando aquí? ¿Cómo debería construir y devolver correctamente un objeto desde una función?

+2

El motivo por el que no se llama al constructor de copia en el segundo caso se llama optimización de valor de retorno, o RVO. El compilador puede omitir la copia temporal +, incluso si esto cambia el comportamiento del programa. – GManNickG

+0

@ Vilx- ¿Qué tal el regreso a un problema variable existente? Lo he enfrentado pero no pude entender por qué se llama el destructor en un constructor sin copia. ¿Alguna vez has descubierto cuál es la causa de esto? –

+1

@ArthurNunes - Lo siento, no, no he trabajado mucho con C++ y no sé la respuesta. Creo que abandoné este proyecto o lo reescribí de otra manera. (Eso fue hace 5 años, sabes). –

Respuesta

15

La mejor manera de entender la copia en C++ es NO intentar producir un ejemplo artificial e instrumentarlo: el compilador puede eliminar y agregar llamadas al constructor de copias, más o menos como lo ve en forma.

Conclusión: si necesita devolver un valor, devuelva un valor y no se preocupe por ningún "gasto".

+0

OK, acepto esto. Aún así, me pregunto: ¿qué pasa con los objetos que no deberían ser clonados? Como un conector de red, que es en mi caso. ¿Estoy obligado a devolver un puntero? ¿O puedo depender del constructor de copias optimizado? –

+0

@ Vilx- Esa es otra pregunta. Y la respuesta es que tales cosas no deberían copiarse o ser copiables. –

+2

@Vilx: Esto se soluciona en el siguiente estándar de C++ con semántica 'move'. En lugar de hacer una copia y luego perder una para transferir un objeto, simplemente mueve los datos internos; nunca existe más de una copia. – GManNickG

13

Lectura recomendada: Effective C++ por Scott Meyers. Encuentra una muy buena explicación sobre este tema (y mucho más) allí.

En resumen, si devuelve por valor, el constructor de la copia y el destructor estarán implicados por defecto (a menos que el compilador los optimice, eso es lo que sucede en algunos de sus casos).

Si devuelve por referencia (o puntero) una variable que es local (construida en la pila), ocasiona problemas porque el objeto se destruye a su regreso, por lo que tiene una referencia colgante como resultado.

La forma canónica para construir un objeto en una función y volver es por valor, como:

MyClass fun() { 
    return MyClass(a, b, c); 
} 

MyClass x = fun(); 

Si utiliza esto, usted no tiene que preocuparse por problemas de propiedad, referencias pendientes etc.Y lo más probable es que el compilador optimice las llamadas de constructor/destructor adicionales para usted, por lo que no tendrá que preocuparse por el rendimiento.

Es posible devolver por referencia un objeto construido por new (es decir, en el montón): este objeto no se destruirá al regresar de la función. Sin embargo, debe destruirlo explícitamente en algún momento posterior llamando al delete.

También es técnicamente posible almacenar un objeto devuelto por el valor de una referencia, como:

MyClass& x = fun(); 

Sin embargo, que yo sepa no hay mucha razón para hacer esto. Especialmente porque uno puede transmitir fácilmente esta referencia a otras partes del programa que están fuera del alcance actual; sin embargo, el objeto al que se hace referencia en x es un objeto local que se destruirá tan pronto como salga del alcance actual. Entonces este estilo puede conducir a errores desagradables.

+0

¿qué pasa con el uso de const MyClass & x = fun(); la variable no se destruye cuando se devuelve/abandona el alcance. ¿Esta bien? – Mannaggia

2

Básicamente, devolver una referencia solo tiene sentido si el objeto aún existe después de abandonar el método. El compilador te advertirá si devuelves una referencia a algo que está siendo destruido.

Devolver una referencia en lugar de un objeto por valor guarda la copia del objeto que podría ser significativa.

Las referencias son más seguras que los indicadores porque tienen una sÃntica diferente, pero entre bastidores son punteros.

+1

El compilador podría advertirte (y normalmente lo hace), pero los casos complejos no se detectan y no todos los compiladores podrían analizarlo en primer lugar. – Tronic

+0

¿Qué compilador comete errores aquí? ¿Un ejemplo? – user231967

+1

@nodan: Visual Studio 2010 no advierte con: 'int & f (bool f, int & x) {int y = 0; devolver f? x: y; } int main() {int x; int & i = f (falso, x); } ' – GManNickG

9

leer acerca de RVO y NRVO (en una palabra estos dos soportes para la optimización del valor devuelto y con nombre RVO, y son técnicas de optimización utilizados por el compilador para hacer lo que estamos tratando de lograr)

encontrará muchos temas aquí en stackoverflow

0

Usted está colapsada, ya sea con:

1) devolviendo un puntero

MiClase * func() {// algunos stuf volver nuevos MiClase (a, b, c); }

2) devolver una copia del objeto MyClass func() { MyClass retorno (a, b, c); }

La devolución de una referencia no es válida porque el objeto debe destruirse después de salir del ámbito de func, excepto si la función es un miembro de la clase y la referencia es de una variable que es miembro de la clase.

1

Una solución potencial, dependiendo de su caso de uso, es por defecto-construir el objeto fuera de la función, tomar en una referencia a él, e inicializar el objeto referenciado dentro de la función, así:

void initFoo(Foo& foo) 
{ 
    foo.setN(3); 
    foo.setBar("bar"); 
    // ... etc ... 
} 

int main() 
{ 
    Foo foo; 
    initFoo(foo); 

    return 0; 
} 

Ahora, por supuesto, esto no funciona si no es posible (o no tiene sentido) construir de forma predeterminada un objeto Foo y luego inicializarlo más tarde. Si ese es el caso, entonces la única opción real para evitar la construcción de copias es devolver un puntero a un objeto asignado en el montón.

Pero luego piense por qué está tratando de evitar la construcción de copias en primer lugar. ¿El "gasto" de la construcción de copias realmente afecta su programa o es un caso de optimización prematura?

+0

En realidad, es un caso de do-I-really-need-to-write-the-damn-copy-constructor. Menos código, menos errores. : P –

+0

@Vilx: casi nunca deberías necesitarlo. Sus miembros deben copiarse. – GManNickG

+0

A menos que haya alguna memoria asignada dinámicamente que se desasigna en la destrucción. Y no creo que eso sea muy raro, ¿verdad? –

3

Acerca del único momento en que tiene sentido devolver una referencia es si está devolviendo una referencia a un objeto preexistente. Para un ejemplo obvio, casi todas las funciones miembro de iostream devuelven una referencia al iostream. El iostream en sí existe antes de que se llame a cualquiera de las funciones de miembro, y continúa existiendo después de haber sido llamado.

El estándar permite "elisión de copia", lo que significa que no es necesario llamar al constructor de copia cuando devuelve un objeto. Esto viene en dos formas: Name Return Value Optimization (NRVO) y anónimo Return Value Optimization (generalmente solo RVO).

Según lo que dices, tu compilador implementa RVO pero no NRVO, lo que significa que es probablemente un compilador algo más antiguo. La mayoría de los compiladores actuales implementan ambos. El dtor no coincidente en este caso significa que es probablemente algo así como gcc 3.4, aunque no recuerdo la versión con certeza, había uno alrededor que tenía un error como este. Por supuesto, también es posible que su instrumentación no sea del todo correcta, por lo tanto, se está utilizando un controlador que no instrumentó, y se está invocando un dtor coincidente para ese objeto.

Al final, tiene un simple hecho: si necesita devolver un objeto, debe devolver un objeto. En particular, una referencia solo puede dar acceso a una (posiblemente versión modificada de) un objeto existente, pero ese objeto también se debe construir en algún punto. Si puedes modificar algún objeto existente sin causar un problema, está bien y bueno, adelante y hazlo. Si necesita un nuevo objeto diferente e independiente de los que ya tiene, siga adelante y hágalo; crear previamente el objeto y pasar una referencia a él puede hacer que el retorno sea más rápido, pero no ahorrará tiempo en general. La creación del objeto tiene aproximadamente el mismo costo, ya sea que se realice dentro o fuera de la función. Cualquier compilador razonablemente moderno incluirá RVO, por lo que no pagará ningún costo adicional por crearlo en la función, y luego devolverlo: el compilador automatizará la asignación de espacio para el objeto donde se va a devolver, y tendrá la función construirlo "en su lugar", donde seguirá siendo accesible después de que la función regrese.

+0

En realidad es Visual Studio 2008. :) Aunque es una compilación de depuración, muchas optimizaciones están desactivadas. –

+0

Ah, eso explicaría la falta de NRVO de todos modos. No estoy tan seguro acerca del dtor no emparejado, usted * podría * tener un error en el compilador, pero probablemente sea más probable que haya un ctor no instrumentado. –

0

No es una respuesta directa, sino una sugerencia viable: también puede devolver un puntero, envuelto en un auto_ptr o smart_ptr. Entonces usted tendrá el control sobre a qué constructores y destructores se les llama y cuándo.

3

Si crea un objeto como éste:

MyClass foo(a, b, c); 

entonces será en la pila en el marco de la función. Cuando esa función finaliza, su marco se saca de la pila y todos los objetos en ese marco se destruyen. No hay forma de evitar esto.

Así que si usted quiere devolver un objeto a una persona que llama, sólo opciones son:

  • retorno por valor - se requiere un constructor de copia (pero la llamada al constructor de copia puede ser optimizado a cabo) .
  • Devuelva un puntero y asegúrese de utilizar punteros inteligentes para solucionarlo o eliminarlo con cuidado cuando termine de utilizarlo.

Intentar construir un objeto local y luego devolver una referencia a esa memoria local a un contexto de llamada no es coherente: un alcance llamante no puede acceder a la memoria que es local para el alcance llamado. Esa memoria local solo es válida por la duración de la función que la posee, o, de otra forma, mientras la ejecución permanece en ese ámbito. Debe entender esto para programar en C++.

+0

Sí, pero el "constructor optimizado de copia de salida" me estaba confundiendo. El objeto sería un poco ... desordenado para clonar, lo que conlleva un enchufe de red y todo. –

+0

No necesariamente: un socket es solo un descriptor de archivo en el fondo, por lo que cuando clones el objeto puedes "clonar" el socket de red simplemente copiando el FD. – Tom

Cuestiones relacionadas