Lo que entiendo es que esto no debería hacerse, pero creo que he visto ejemplos que hacen algo como esto (el código de nota no es necesariamente correcto sintácticamente, pero la idea está ahí)
typedef struct{
int a,b;
}mystruct;
Y luego aquí hay una función
mystruct func(int c, int d){
mystruct retval;
retval.a = c;
retval.b = d;
return retval;
}
Entendí que siempre deberíamos devolver un puntero a una estructura malloc'ed si queremos hacer algo como esto, pero estoy seguro de que he visto ejemplos que hacen algo como esto. ¿Es esto correcto? Personalmente, siempre devuelvo un puntero a una estructura mal ubicada o simplemente hago un pase por referencia a la función y modifico los valores allí. (Porque tengo entendido que una vez que finaliza el alcance de la función, se puede sobrescribir la pila que se usó para asignar la estructura).
Agreguemos una segunda parte a la pregunta: ¿Esto varía según el compilador? Si es así, ¿cuál es el comportamiento de las últimas versiones de compiladores para escritorio: gcc, g ++ y Visual Studio?
¿Pensamientos al respecto?
Respuestas:
Es perfectamente seguro y no está mal hacerlo. Además: no varía según el compilador.
Por lo general, cuando (como su ejemplo) su estructura no es demasiado grande, diría que este enfoque es incluso mejor que devolver una estructura malloc'ed (
malloc
es una operación costosa).fuente
Es perfectamente seguro.
Regresas por valor. Lo que llevaría a un comportamiento indefinido es si regresara por referencia.
//safe mystruct func(int c, int d){ mystruct retval; retval.a = c; retval.b = d; return retval; } //undefined behavior mystruct& func(int c, int d){ mystruct retval; retval.a = c; retval.b = d; return retval; }
El comportamiento de su fragmento es perfectamente válido y definido. No varía según el compilador. ¡Está bien!
No deberías. Debe evitar la memoria asignada dinámicamente cuando sea posible.
Esta opción es perfectamente válida. Es cuestión de elección. En general, hace esto si desea devolver algo más de la función, mientras modifica la estructura original.
Esto está mal. Quiero decir, es algo correcto, pero devuelve una copia de la estructura que crea dentro de la función. Teóricamente . En la práctica, RVO puede ocurrir y probablemente ocurrirá. Obtenga más información sobre la optimización del valor de retorno. Esto significa que, aunque
retval
parece estar fuera de alcance cuando finaliza la función, en realidad podría estar construida en el contexto de llamada, para evitar la copia adicional. Esta es una optimización que el compilador puede implementar libremente.fuente
De hecho, la vida útil del
mystruct
objeto en su función termina cuando abandona la función. Sin embargo, está pasando el objeto por valor en la declaración de devolución. Esto significa que el objeto se copia de la función a la función que llama. El objeto original se ha ido, pero la copia sigue viva.fuente
No solo es seguro devolver a
struct
en C (o aclass
en C ++, dondestruct
-s son en realidadclass
-es conpublic:
miembros predeterminados ), sino que una gran cantidad de software lo está haciendo.Por supuesto, al devolver un
class
en C ++, el lenguaje especifica que se llamaría a algún destructor o constructor en movimiento, pero hay muchos casos en los que el compilador podría optimizarlo.Además, la ABI de Linux x86-64 especifica que devolver a
struct
con doslong
valores escalares (por ejemplo, punteros, o ) se realiza a través de registros (%rax
&%rdx
) por lo que es muy rápido y eficiente. Entonces, para ese caso particular, probablemente sea más rápido devolver tales camposstruct
de dos escalares que hacer cualquier otra cosa (por ejemplo, almacenarlos en un puntero pasado como argumento).Devolver un campo de dos escalares
struct
es mucho más rápido quemalloc
-regresarlo y devolver un puntero.fuente
Es perfectamente legal, pero con estructuras grandes hay dos factores que deben tenerse en cuenta: la velocidad y el tamaño de la pila.
fuente
Un tipo de estructura puede ser el tipo del valor devuelto por una función. Es seguro porque el compilador creará una copia de la estructura y devolverá la copia, no la estructura local en la función.
typedef struct{ int a,b; }mystruct; mystruct func(int c, int d){ mystruct retval; cout << "func:" <<&retval<< endl; retval.a = c; retval.b = d; return retval; } int main() { cout << "main:" <<&(func(1,2))<< endl; system("pause"); }
fuente
La seguridad depende de cómo se implementó la estructura en sí. Me encontré con esta pregunta mientras implementaba algo similar, y aquí está el problema potencial.
El compilador, al devolver el valor, realiza algunas operaciones (entre posiblemente otras):
mystruct(const mystruct&)
(this
es una variable temporal fuera de la funciónfunc
asignada por el propio compilador)~mystruct
en la variable que se asignó dentrofunc
mystruct::operator=
si el valor devuelto se asigna a otra cosa con=
~mystruct
en la variable temporal utilizada por el compiladorAhora, si
mystruct
es tan simple como la descrita aquí todo está bien, pero si tiene puntero (comochar*
) o la gestión de memoria más complicado, entonces todo depende de la formamystruct::operator=
,mystruct(const mystruct&)
y~mystruct
se implementan. Por lo tanto, sugiero precauciones al devolver estructuras de datos complejas como valor.fuente
Es perfectamente seguro devolver una estructura como lo ha hecho.
Sin embargo, con base en esta declaración: debido a que tengo entendido que una vez que finaliza el alcance de la función, cualquier pila que se usó para asignar la estructura se puede sobrescribir, solo imaginaría un escenario en el que cualquiera de los miembros de la estructura se asignó dinámicamente ( malloc'ed o new'ed), en cuyo caso, sin RVO, los miembros asignados dinámicamente serán destruidos y la copia devuelta tendrá un miembro apuntando a basura.
fuente
También estaré de acuerdo con sftrabbit, la vida realmente termina y el área de la pila se limpia, pero el compilador es lo suficientemente inteligente como para garantizar que todos los datos se recuperen en registros o de alguna otra manera.
A continuación se ofrece un ejemplo sencillo de confirmación (tomado del ensamblaje del compilador Mingw)
_func: push ebp mov ebp, esp sub esp, 16 mov eax, DWORD PTR [ebp+8] mov DWORD PTR [ebp-8], eax mov eax, DWORD PTR [ebp+12] mov DWORD PTR [ebp-4], eax mov eax, DWORD PTR [ebp-8] mov edx, DWORD PTR [ebp-4] leave ret
Puede ver que el valor de b se ha transmitido a través de edx. mientras que el eax predeterminado contiene el valor de a.
fuente
No es seguro devolver una estructura. Me encanta hacerlo yo mismo, pero si alguien agrega un constructor de copia a la estructura devuelta más adelante, se llamará al constructor de copia. Esto puede ser inesperado y puede romper el código. Este error es muy difícil de encontrar.
Tuve una respuesta más elaborada, pero al moderador no le gustó. Entonces, a su costo, mi propina es corta.
fuente
De hecho lo hace, como descubrí a mi dolor: http://sourceforge.net/p/mingw-w64/mailman/message/33176880/
Estaba usando gcc en win32 (MinGW) para llamar a interfaces COM que devolvían estructuras. Resulta que MS lo hace de manera diferente a GNU, por lo que mi programa (gcc) se bloqueó con una pila rota.
Podría ser que MS tenga el terreno más alto aquí, pero todo lo que me importa es la compatibilidad ABI entre MS y GNU para construir en Windows.
Puede encontrar algunos mensajes en una lista de correo de Wine sobre cómo parece hacerlo MS.
fuente
Nota: esta respuesta solo se aplica a c ++ 11 en adelante. No existe "C / C ++", son lenguajes diferentes.
No, no existe ningún peligro en devolver un objeto local por valor, y se recomienda hacerlo. Sin embargo, creo que hay un punto importante que falta en todas las respuestas aquí. Muchos otros han dicho que la estructura se copia o se coloca directamente mediante RVO. Sin embargo, esto no es del todo correcto. Intentaré explicar exactamente qué cosas pueden suceder al devolver un objeto local.
Mover semántica
Desde c ++ 11, hemos tenido referencias rvalue que son referencias a objetos temporales que pueden ser robados de forma segura. Como ejemplo, std :: vector tiene un constructor de movimiento así como un operador de asignación de movimiento. Ambos tienen una complejidad constante y simplemente copian el puntero a los datos del vector que se está moviendo. No entraré en más detalles sobre la semántica de movimientos aquí.
Debido a que un objeto creado localmente dentro de una función es temporal y sale del alcance cuando la función regresa, un objeto devuelto nunca se copia con c ++ 11 en adelante. Se llama al constructor de movimiento en el objeto que se devuelve (o no, se explica más adelante). Esto significa que si devolviera un objeto con un constructor de copia costoso pero un constructor de movimiento económico, como un vector grande, solo la propiedad de los datos se transfiere del objeto local al objeto devuelto, lo cual es barato.
Tenga en cuenta que en su ejemplo específico, no hay diferencia entre copiar y mover el objeto. Los constructores de movimiento y copia predeterminados de su estructura dan como resultado las mismas operaciones; copiando dos enteros. Sin embargo, esto es al menos tan rápido que cualquier otra solución porque toda la estructura cabe en un registro de CPU de 64 bits (corríjame si me equivoco, no conozco muchos registros de CPU).
RVO y NRVO
RVO significa optimización del valor de retorno y es una de las pocas optimizaciones que hacen los compiladores y que puede tener efectos secundarios. Desde c ++ 17, se requiere RVO. Cuando se devuelve un objeto sin nombre, se construye directamente en el lugar donde la persona que llama asigna el valor devuelto. No se llama ni al constructor de copia ni al constructor de movimiento. Sin RVO, el objeto sin nombre se construiría primero localmente, luego se movería construido en la dirección devuelta y luego se destruiría el objeto sin nombre local.
Ejemplo donde se requiere RVO (c ++ 17) o probable (antes de c ++ 17):
auto function(int a, int b) -> MyStruct { // ... return MyStruct{a, b}; }
NRVO significa Optimización del valor de retorno con nombre y es lo mismo que RVO, excepto que se realiza para un objeto con nombre local a la función llamada. Esto todavía no está garantizado por el estándar (c ++ 20) pero muchos compiladores aún lo hacen. Tenga en cuenta que incluso con objetos locales con nombre, en el peor de los casos se mueven cuando se devuelven.
Conclusión
El único caso en el que debería considerar no devolver por valor es cuando tiene un objeto con nombre, muy grande (como en su tamaño de pila). Esto se debe a que NRVO aún no está garantizado (a partir de c ++ 20) e incluso mover el objeto sería lento. Mi recomendación, y la recomendación en las Pautas básicas de Cpp es siempre preferir devolver objetos por valor (si hay múltiples valores de retorno, use struct (o tupla)), donde la única excepción es cuando el objeto es costoso de mover. En ese caso, use un parámetro de referencia que no sea constante.
NUNCA es una buena idea devolver un recurso que tiene que ser liberado manualmente desde una función en c ++. Nunca hagas eso. Al menos use un std :: unique_ptr, o cree su propia estructura local o no local con un destructor que libere su recurso ( RAII ) y devuelva una instancia de eso. Entonces también sería una buena idea definir el constructor de movimiento y el operador de asignación de movimiento si el recurso no tiene su propia semántica de movimiento (y eliminar el constructor de copia / asignación).
fuente