¿Hay algún inconveniente para pasar las estructuras por valor en C, en lugar de pasar un puntero?

¿Hay algún inconveniente para pasar las estructuras por valor en C, en lugar de pasar un puntero?

Si la estructura es grande, obviamente existe el aspecto ejecutado de copiar muchos datos, pero para una estructura más pequeña, básicamente debería ser lo mismo que pasar varios valores a una función.

Es quizás incluso más interesante cuando se usa como valores de retorno. C solo tiene valores de retorno únicos de las funciones, pero a menudo necesita varios. Entonces, una solución simple es ponerlos en una estructura y devolver eso.

¿Hay alguna razón a favor o en contra de esto?

Dado que puede que no sea obvio para todos de lo que estoy hablando aquí, daré un ejemplo simple.

Si estás progtwigndo en C, tarde o temprano comenzarás a escribir funciones que se ven así:

void examine_data(const char *ptr, size_t len) { ... } char *p = ...; size_t l = ...; examine_data(p, l); 

Esto no es un problema El único problema es que debe acordar con su compañero de trabajo en qué orden deben estar los parámetros para que use la misma convención en todas las funciones.

¿Pero qué sucede cuando quieres devolver el mismo tipo de información? Por lo general, obtienes algo como esto:

 char *get_data(size_t *len); { ... *len = ...datalen...; return ...data...; } size_t len; char *p = get_data(&len); 

Esto funciona bien, pero es mucho más problemático. Un valor de retorno es un valor de retorno, excepto que en esta implementación no lo es. No hay forma de saber a partir de lo anterior que la función get_data no puede ver a qué apunta. Y no hay nada que haga que el comstackdor compruebe que un valor realmente se devuelve a través de ese puntero. Entonces, el próximo mes, cuando alguien más modifica el código sin entenderlo correctamente (¿porque no leyó la documentación?) Se rompe sin que nadie lo note, o comienza a fallar al azar.

Entonces, la solución que propongo es la estructura simple

 struct blob { char *ptr; size_t len; } 

Los ejemplos se pueden reescribir así:

 void examine_data(const struct blob data) { ... use data.tr and data.len ... } struct blob = { .ptr = ..., .len = ... }; examine_data(blob); struct blob get_data(void); { ... return (struct blob){ .ptr = ...data..., .len = ...len... }; } struct blob data = get_data(); 

Por alguna razón, creo que la mayoría de la gente instintivamente haría que examine_data tomara un puntero a un struct blob, pero no veo por qué. Todavía obtiene un puntero y un número entero, es mucho más claro que vayan juntos. Y en el caso get_data es imposible equivocarse en la forma descrita anteriormente, ya que no hay un valor de entrada para la longitud, y debe haber una longitud devuelta.

Para estructuras pequeñas (p. Ej., Punto, rect), pasar por valor es perfectamente aceptable. Pero, aparte de la velocidad, hay otra razón por la que debe tener cuidado al pasar / devolver estructuras grandes por valor: espacio de stack.

Una gran cantidad de progtwigción en C es para sistemas integrados, donde la memoria es primordial, y los tamaños de stack pueden medirse en KB o incluso Bytes … Si está pasando o devolviendo estructuras por valor, se colocarán copias de esas estructuras en la stack, lo que puede causar la situación que este sitio lleva el nombre después de …

Si veo una aplicación que parece tener un uso excesivo de la stack, las estructuras pasadas por valor es una de las cosas que busco primero.

Una razón para no hacer esto que no se ha mencionado es que esto puede causar un problema donde la compatibilidad binaria importa.

Dependiendo del comstackdor utilizado, las estructuras se pueden pasar a través de la stack o registros dependiendo de las opciones / implementación del comstackdor

Ver: http://gcc.gnu.org/onlinedocs/gcc/Code-Gen-Options.html

-fpcc-struct-return

-freg-struct-return

Si dos comstackdores no están de acuerdo, las cosas pueden explotar. Huelga decir que las razones principales para no hacer esto se ilustran son el consumo de la stack y las razones de rendimiento.

Para responder realmente a esta pregunta, es necesario profundizar en el terreno de la asamblea:

(El siguiente ejemplo usa gcc en x86_64. Cualquiera puede agregar otras architectures como MSVC, ARM, etc.)

Tengamos nuestro progtwig de ejemplo:

 // foo.c typedef struct { double x, y; } point; void give_two_doubles(double * x, double * y) { *x = 1.0; *y = 2.0; } point give_point() { point a = {1.0, 2.0}; return a; } int main() { return 0; } 

Comstackrlo con optimizaciones completas

 gcc -Wall -O3 foo.c -o foo 

Mire la asamblea:

 objdump -d foo | vim - 

Esto es lo que obtenemos:

 0000000000400480 : 400480: 48 ba 00 00 00 00 00 mov $0x3ff0000000000000,%rdx 400487: 00 f0 3f 40048a: 48 b8 00 00 00 00 00 mov $0x4000000000000000,%rax 400491: 00 00 40 400494: 48 89 17 mov %rdx,(%rdi) 400497: 48 89 06 mov %rax,(%rsi) 40049a: c3 retq 40049b: 0f 1f 44 00 00 nopl 0x0(%rax,%rax,1) 00000000004004a0 : 4004a0: 66 0f 28 05 28 01 00 movapd 0x128(%rip),%xmm0 4004a7: 00 4004a8: 66 0f 29 44 24 e8 movapd %xmm0,-0x18(%rsp) 4004ae: f2 0f 10 05 12 01 00 movsd 0x112(%rip),%xmm0 4004b5: 00 4004b6: f2 0f 10 4c 24 f0 movsd -0x10(%rsp),%xmm1 4004bc: c3 retq 4004bd: 0f 1f 00 nopl (%rax) 

Excluyendo los pads nopl , give_two_doubles() tiene 27 bytes mientras que give_point() tiene 29 bytes. Por otro lado, give_point() produce una instrucción menos que give_two_doubles()

Lo que es interesante es que notamos que el comstackdor ha sido capaz de optimizar mov en las variantes SSE2 más rápidas movapd y movsd . Además, give_two_doubles() mueve los datos hacia adentro y hacia afuera desde la memoria, lo que hace que las cosas give_two_doubles() despacio.

Aparentemente, gran parte de esto puede no ser aplicable en entornos integrados (que es donde el campo de juego para C es la mayoría del tiempo actualmente). No soy un asistente de assembly, ¡así que cualquier comentario sería bienvenido!

La solución simple será devolver un código de error como un valor de retorno y todo lo demás como un parámetro en la función,
Este parámetro puede ser una estructura, por supuesto, pero no ve ninguna ventaja particular pasando esto por valor, solo envió un puntero.
Pasar estructura por valor es peligroso, debe tener mucho cuidado con lo que está pasando, recuerde que no hay constructor de copia en C, si uno de los parámetros de la estructura es un puntero, se copiará el valor del puntero, podría ser muy confuso y difícil mantener.

Solo para completar la respuesta (crédito completo para Roddy ) el uso de la stack es otra razón por la que no pasa la estructura por valor, créame que el desbordamiento de la stack de depuración es real PITA.

Reproducir para comentar:

Al pasar estructura por puntero, significa que alguna entidad tiene una propiedad sobre este objeto y tiene un conocimiento completo de qué y cuándo se debe liberar. Al pasar struct por valor, se crean referencias ocultas a los datos internos de struct (punteros a otras estructuras, etc.) que es difícil de mantener (¿pero por qué?).

Diría que pasar estructuras (no demasiado grandes) por valor, como parámetros y como valores devueltos, es una técnica perfectamente legítima. Uno tiene que tener cuidado, por supuesto, que la estructura es un tipo de POD, o la semántica de copia está bien especificada.

Actualización: Lo siento, tenía mi límite de pensamiento en C ++. Recuerdo un momento en que no era legal en C devolver una estructura de una función, pero esto probablemente ha cambiado desde entonces. Aún diría que es válida siempre y cuando todos los comstackdores que espera usar respalden la práctica.

Creo que su pregunta ha resumido bastante bien las cosas.

Otra ventaja de pasar estructuras por valor es que la propiedad de la memoria es explícita. No hay dudas sobre si la estructura es del montón, y quién tiene la responsabilidad de liberarla.

Aquí hay algo que nadie mencionó:

 void examine_data(const char *c, size_t l) { c[0] = 'l'; // compiler error } void examine_data(const struct blob blob) { blob.ptr[0] = 'l'; // perfectly legal, quite likely to blow up at runtime } 

Los miembros de una const struct son const , pero si ese miembro es un puntero (como char * ), se convierte en char *const lugar de const char * que realmente queremos. Por supuesto, podríamos suponer que la const es documentación de intención, y que cualquiera que viole esto está escribiendo un código incorrecto (que lo son), pero eso no es lo suficientemente bueno para algunos (especialmente aquellos que solo pasaron cuatro horas rastreando la causa de un choque).

La alternativa podría ser hacer una struct const_blob { const char *c; size_t l } struct const_blob { const char *c; size_t l } y lo uso, pero es bastante desordenado, entra en el mismo problema de esquema de nomenclatura que tengo con los punteros de typedef de tipo. Por lo tanto, la mayoría de la gente se limita a tener dos parámetros (o, más probablemente en este caso, usar una biblioteca de cadenas).

Una cosa que la gente aquí ha olvidado mencionar hasta ahora (o lo he pasado por alto) es que las estructuras generalmente tienen un relleno.

 struct { short a; char b; short c; char d; } 

Cada char es de 1 byte, cada short tiene 2 bytes. ¿Qué tan grande es la estructura? No, no son 6 bytes. Al menos no en los sistemas más comúnmente utilizados. En la mayoría de los sistemas será 8. El problema es que la alineación no es constante, depende del sistema, por lo que la misma estructura tendrá diferente alineación y diferentes tamaños en diferentes sistemas.

No solo ese relleno absorberá aún más tu stack, sino que también agrega la incertidumbre de no poder predecir el relleno por adelantado, a menos que sepas cómo funciona el sistema y luego observa cada estructura que tienes en tu aplicación y calcula el tamaño para ello. Pasar un puntero requiere una cantidad predecible de espacio; no hay incertidumbre. El tamaño de un puntero es conocido para el sistema, siempre es igual, independientemente de cómo se ve la estructura y los tamaños de puntero siempre se eligen de forma que estén alineados y no necesiten relleno.

La página 150 de PC Assembly Tutorial en http://www.drpaulcarter.com/pcasm/ tiene una explicación clara sobre cómo C permite que una función devuelva una estructura:

C también permite que se use un tipo de estructura como el valor de retorno de una función. Obviamente, no se puede devolver una estructura en el registro EAX. Diferentes comstackdores manejan esta situación de manera diferente. Una solución común que utilizan los comstackdores es reescribir internamente la función como una que toma un puntero de estructura como parámetro. El puntero se usa para poner el valor de retorno en una estructura definida fuera de la rutina llamada.

Uso el siguiente código C para verificar la statement anterior:

 struct person { int no; int age; }; struct person create() { struct person jingguo = { .no = 1, .age = 2}; return jingguo; } int main(int argc, const char *argv[]) { struct person result; result = create(); return 0; } 

Use “gcc -S” para generar el ensamblaje de esta pieza de código C:

  .file "foo.c" .text .globl create .type create, @function create: pushl %ebp movl %esp, %ebp subl $16, %esp movl 8(%ebp), %ecx movl $1, -8(%ebp) movl $2, -4(%ebp) movl -8(%ebp), %eax movl -4(%ebp), %edx movl %eax, (%ecx) movl %edx, 4(%ecx) movl %ecx, %eax leave ret $4 .size create, .-create .globl main .type main, @function main: pushl %ebp movl %esp, %ebp subl $20, %esp leal -8(%ebp), %eax movl %eax, (%esp) call create subl $4, %esp movl $0, %eax leave ret .size main, .-main .ident "GCC: (Ubuntu 4.4.3-4ubuntu5) 4.4.3" .section .note.GNU-stack,"",@progbits 

La stack antes de llamar crea:

  +---------------------------+ ebp | saved ebp | +---------------------------+ ebp-4 | age part of struct person | +---------------------------+ ebp-8 | no part of struct person | +---------------------------+ ebp-12 | | +---------------------------+ ebp-16 | | +---------------------------+ ebp-20 | ebp-8 (address) | +---------------------------+ 

La stack justo después de llamar crea:

  +---------------------------+ | ebp-8 (address) | +---------------------------+ | return address | +---------------------------+ ebp,esp | saved ebp | +---------------------------+ 

Solo quiero señalar que una ventaja de pasar sus estructuras por valor es que un comstackdor de optimización puede optimizar mejor su código.