¿Es seguro devolver una estructura en C o C ++?

83

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?

jzepeda
fuente
33
"Lo que entiendo es que esto no debería hacerse" dice quién? Lo hago todo el tiempo. También tenga en cuenta que el typedef no es necesario en C ++, y que no existe tal cosa como "C / C ++".
PlasmaHH
4
La pregunta parece no estar dirigida a c ++.
Captain Giraffe
4
@PlasmaHH Copiar estructuras grandes alrededor puede ser ineficaz. Es por eso que uno debe tener cuidado y pensar mucho antes de devolver una estructura por valor, especialmente si la estructura tiene un constructor de copia costoso y el compilador no es bueno en la optimización del valor de retorno. Recientemente realicé una optimización en una aplicación que estaba gastando una parte significativa de su tiempo en constructores de copias para algunas estructuras grandes que un programador había decidido devolver por valor en todas partes. La ineficiencia nos estaba costando alrededor de $ 800,000 en hardware de centro de datos adicional que necesitábamos comprar.
Crashworks
8
@Crashworks: Felicitaciones, espero que su jefe le haya dado un aumento.
PlasmaHH
6
@Crashworks: seguro que es malo devolver siempre por valor sin pensar, pero en situaciones en las que es lo natural, normalmente no hay una alternativa segura que no requiera también que se haga una copia, por lo que devolver por valor es la mejor solución, ya que lo hace. no necesita ninguna asignación de montón. A menudo ni siquiera habrá una copia, el uso de una buena elisión de copia del compilador debería saltar cuando sea posible y en C ++ 11, la semántica de movimientos puede eliminar aún más la copia profunda. Ambos mecanismos no funcionarán correctamente si hace algo más que devolver por valor.
izquierda rotonda alrededor del

Respuestas:

76

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 ( malloces una operación costosa).

Pablo Santa Cruz
fuente
3
¿Seguiría siendo seguro si uno de los campos fuera un char *? Ahora habría puntero en la estructura
jzepeda
3
@ user963258 en realidad, eso depende de cómo implementes el constructor de copia y el destructor.
Luchian Grigore
2
@PabloSantaCruz Esa es una pregunta delicada. Si se tratara de una pregunta de examen, el examinador podría esperar un "no" como respuesta, si es necesario considerar la propiedad.
Captain Giraffe
2
@CaptainGiraffe: cierto. Como OP no aclaró esto, y sus ejemplos eran básicamente C , asumí que era más una pregunta de C que de C ++ .
Pablo Santa Cruz
2
@Kos ¿Algunos compiladores no tienen NRVO? De que año También a tener en cuenta: en C ++ 11, incluso si no tiene NRVO, invocará la semántica de movimiento en su lugar.
David
71

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!

Personalmente, siempre devuelvo un puntero a una estructura malloc'ed

No deberías. Debe evitar la memoria asignada dinámicamente cuando sea posible.

o simplemente pase por referencia a la función y modifique los valores allí.

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.

Porque 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

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 retvalparece 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.

Luchian Grigore
fuente
3
+1 por mencionar RVO. Esta importante optimización hace que este patrón sea factible para objetos con constructores de copia costosos, como contenedores STL.
Kos
1
Vale la pena mencionar que aunque el compilador es libre de realizar la optimización del valor de retorno, no hay garantía de que lo haga. Esto no es algo con lo que pueda contar, solo esperanza.
Watcom
-1 para "evitar la memoria asignada dinámicamente cuando sea posible". Esto tiende a ser una regla newb y con frecuencia da como resultado un código en el que se devuelven GRANDES cantidades de datos (y desconocen por qué las cosas funcionan lentamente) cuando un simple puntero puede ahorrar mucho tiempo. La regla correcta es devolver estructuras o punteros basados ​​en la velocidad , el uso y la claridad .
Lloyd Sargent
10

De hecho, la vida útil del mystructobjeto 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.

Joseph Mansfield
fuente
9

No solo es seguro devolver a structen C (o a classen C ++, donde struct-s son en realidad class-es con public:miembros predeterminados ), sino que una gran cantidad de software lo está haciendo.

Por supuesto, al devolver un classen 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 structcon 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 campos structde dos escalares que hacer cualquier otra cosa (por ejemplo, almacenarlos en un puntero pasado como argumento).

Devolver un campo de dos escalares structes mucho más rápido que malloc-regresarlo y devolver un puntero.

Basile Starynkevitch
fuente
5

Es perfectamente legal, pero con estructuras grandes hay dos factores que deben tenerse en cuenta: la velocidad y el tamaño de la pila.

ebutusov
fuente
1
¿Ha oído hablar de la optimización del valor de retorno?
Luchian Grigore
Sí, pero estamos hablando en general de devolver estructura por valor, y hay casos en los que el compilador no puede realizar RVO.
ebutusov
3
Yo diría que solo se preocupe por la copia adicional después de haber realizado algunos perfiles.
Luchian Grigore
4

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");
}
haberdar
fuente
4

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):

  1. Llama al constructor de copia mystruct(const mystruct&)( thises una variable temporal fuera de la función funcasignada por el propio compilador)
  2. llama al destructor ~mystructen la variable que se asignó dentrofunc
  3. llamadas mystruct::operator=si el valor devuelto se asigna a otra cosa con=
  4. llama al destructor ~mystructen la variable temporal utilizada por el compilador

Ahora, si mystructes tan simple como la descrita aquí todo está bien, pero si tiene puntero (como char*) o la gestión de memoria más complicado, entonces todo depende de la forma mystruct::operator=, mystruct(const mystruct&)y ~mystructse implementan. Por lo tanto, sugiero precauciones al devolver estructuras de datos complejas como valor.

Filippo
fuente
Solo es cierto antes de c ++ 11.
Björn Sundin
4

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.

maress
fuente
La pila solo se usa temporalmente para la operación de copia. Por lo general, la pila se reservaría antes de la llamada, y la función llamada coloca los datos a devolver en la pila, y luego la persona que llama extrae estos datos de la pila y los almacena donde sea que se asigne. Así que no te preocupes.
Thomas Tempelmann
3

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.

cerebro peligroso
fuente
2

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.

Igor Polk
fuente
“No es seguro devolver una estructura. […] Se llamará al constructor de la copia ”. - Hay una diferencia entre seguro e ineficiente . Devolver una estructura es definitivamente seguro. Incluso entonces, lo más probable es que el compilador elide la llamada a la copia ctor a medida que se crea la estructura en la pila del llamador.
phg
2

Agreguemos una segunda parte a la pregunta: ¿Esto varía según el compilador?

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.

Si es así, ¿cuál es el comportamiento de las últimas versiones de compiladores para escritorios: gcc, g ++ y Visual Studio?

Puede encontrar algunos mensajes en una lista de correo de Wine sobre cómo parece hacerlo MS.

effbiae
fuente
Sería más útil si le diera un puntero a la lista de correo de Wine a la que se refiere.
Jonathan Leffler
Devolver estructuras está bien. COM especifica una interfaz binaria; si alguien no implementa COM correctamente, eso sería un error.
MM
1

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).

Björn Sundin
fuente
hechos interesantes Creo que golang tiene algo similar.
Mox