¿Hay algún inconveniente en pasar estructuras por valor en C, en lugar de pasar un puntero?
Si la estructura es grande, obviamente existe el aspecto de rendimiento 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.
Quizás sea aún 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?
Como podría no ser obvio para todos de lo que estoy hablando aquí, daré un ejemplo simple.
Si está programando en C, tarde o temprano comenzará 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 tiene que estar de acuerdo con su compañero de trabajo en el orden en que 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 por lo anterior que la función get_data no puede ver a qué apunta len. Y no hay nada que haga que el compilador 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 exam_data tomara un puntero a un blob de estructura, pero no veo por qué. Todavía obtiene un puntero y un número entero, es mucho más claro que van juntos. Y en el caso de get_data, es imposible equivocarse de la manera que describí anteriormente, ya que no hay un valor de entrada para la longitud, y debe haber una longitud devuelta.
fuente
void examine data(const struct blob)
es incorrecto.gettimeofday
) usan punteros en su lugar, y la gente lo toma como un ejemplo.Respuestas:
Para estructuras pequeñas (por ejemplo, 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 grandes estructuras por valor: espacio de pila.
Una gran cantidad de programación en C es para sistemas integrados, donde la memoria es muy importante y los tamaños de pila pueden medirse en KB o incluso Bytes ... Si pasa o devuelve estructuras por valor, se colocarán copias de esas estructuras en la pila, lo que puede causar la situación de que este sitio lleva el nombre ...
Si veo una aplicación que parece tener un uso excesivo de la pila, las estructuras pasadas por valor son una de las cosas que busco primero.
fuente
Una razón para no hacer esto que no se ha mencionado es que esto puede causar un problema donde la compatibilidad binaria es importante.
Dependiendo del compilador utilizado, las estructuras se pueden pasar a través de la pila o registros dependiendo de las opciones / implementación del compilador
Ver: http://gcc.gnu.org/onlinedocs/gcc/Code-Gen-Options.html
Si dos compiladores no están de acuerdo, las cosas pueden explotar. Huelga decir que las principales razones para no hacerlo están ilustradas son el consumo de pila y las razones de rendimiento.
fuente
int &bar() { int f; int &j(f); return j;};
Para responder realmente a esta pregunta, uno necesita profundizar en el terreno de la asamblea:
(El siguiente ejemplo usa gcc en x86_64. Cualquiera puede agregar otras arquitecturas como MSVC, ARM, etc.)
Tengamos nuestro programa de ejemplo:
Compílalo con optimizaciones completas
Mira la asamblea:
Esto es lo que obtenemos:
Excluyendo los
nopl
pads,give_two_doubles()
tiene 27 bytes mientras quegive_point()
tiene 29 bytes. Por otra parte,give_point()
produce una instrucción menos quegive_two_doubles()
Lo interesante es que notamos que el compilador ha podido optimizar
mov
en las variantes SSE2 más rápidasmovapd
ymovsd
. Además, engive_two_doubles()
realidad mueve datos dentro y fuera de la memoria, lo que hace que las cosas sean más lentas.Aparentemente, gran parte de esto puede no ser aplicable en entornos integrados (que es donde el campo de juego para C es la mayor parte del tiempo hoy en día). No soy un asistente de ensamblaje, por lo que cualquier comentario sería bienvenido.
fuente
const
todo el tiempo) y descubrí que no hay mucha penalización de rendimiento (si no ganancia) en la copia de paso por valor , contrario a lo que muchos puedan creer.La solución simple será devolver un código de error como 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 que pase esto por valor, solo envía un puntero.
Pasar la estructura por valor es peligroso, debe tener mucho cuidado con lo que está pasando, recuerde que no hay un constructor de copia en C, si uno de los parámetros de la estructura es un puntero, el valor del puntero se copiará, puede ser muy confuso y difícil de entender. mantener.
Solo para completar la respuesta (crédito total a Roddy ), el uso de la pila es otra razón por la que no se pasa la estructura por valor, créanme, depurar el desbordamiento de la pila es PITA real.
Repetir para comentar:
Al pasar la estructura por el puntero, significa que alguna entidad es propietaria de este objeto y tiene un conocimiento completo de qué y cuándo se debe liberar. Pasar struct por valor crea referencias ocultas a los datos internos de struct (punteros a otras estructuras, etc.) en este punto es difícil de mantener (posible pero ¿por qué?).
fuente
¡Una cosa que la gente aquí ha olvidado mencionar hasta ahora (o lo pasé por alto) es que las estructuras generalmente tienen un relleno!
Cada char tiene 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 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á diferentes alineaciones y diferentes tamaños en diferentes sistemas.
No solo ese relleno consumirá aún más su pila, sino que también agrega la incertidumbre de no poder predecir el relleno con anticipación, a menos que sepa cómo su sistema se rellena y luego mira cada estructura que tiene en su aplicación y calcula el tamaño para ello. Pasar un puntero requiere una cantidad de espacio predecible, no hay incertidumbre. El tamaño de un puntero es conocido para el sistema, siempre es igual, independientemente de cómo se vea la estructura y los tamaños de puntero siempre se eligen de forma que estén alineados y no necesiten relleno.
fuente
Creo que su pregunta ha resumido las cosas bastante bien.
Otra ventaja de pasar estructuras por valor es que la propiedad de la memoria es explícita. No se pregunta si la estructura es del montón y quién tiene la responsabilidad de liberarla.
fuente
Diría que pasar estructuras (no demasiado grandes) por valor, tanto como parámetros como valores de retorno, es una técnica perfectamente legítima. Hay que tener cuidado, por supuesto, de que la estructura sea del tipo POD o que la semántica de copia esté bien especificada.
Actualización: Lo siento, tenía mi gorra de pensamiento 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. Todavía diría que es válido siempre y cuando todos los compiladores que esperas usar admitan la práctica.
fuente
Aquí hay algo que nadie mencionó:
Los miembros de a
const struct
sonconst
, pero si ese miembro es un puntero (me gustachar *
), se convierte enchar *const
lugar de loconst char *
que realmente queremos. Por supuesto, podríamos suponer que elconst
trata de documentación de intenciones, y que cualquiera que viole esto está escribiendo un código incorrecto (que son), pero eso no es lo suficientemente bueno para algunos (especialmente aquellos que solo pasaron cuatro horas buscando la causa de un problema). choque).La alternativa podría ser hacer
struct const_blob { const char *c; size_t l }
y usar eso, pero eso es bastante desordenado: entra en el mismo problema de esquema de nombres que tengo contypedef
los punteros. Por lo tanto, la mayoría de las personas se limitan a tener dos parámetros (o, más probablemente para este caso, usar una biblioteca de cadenas).fuente
struct const_blob
solución es que incluso siconst_blob
tiene miembros que difieren deblob
solo en "constidad indirecta", los tiposstruct blob*
a astruct const_blob*
se considerarán distintos para fines de una estricta regla de alias. En consecuencia, si el código convierte ablob*
a aconst_blob*
, cualquier escritura posterior en la estructura subyacente utilizando un tipo invalidará silenciosamente cualquier puntero existente del otro tipo, de modo que cualquier uso invocará Comportamiento indefinido (que generalmente puede ser inofensivo, pero podría ser mortal) .La página 150 del Tutorial de ensamblaje de PC en http://www.drpaulcarter.com/pcasm/ tiene una explicación clara sobre cómo C permite que una función devuelva una estructura:
Utilizo el siguiente código C para verificar la declaración anterior:
Use "gcc -S" para generar el ensamblaje de este fragmento de código C:
La pila antes de crear la llamada:
La pila justo después de llamar a crear:
fuente
Solo quiero señalar una ventaja de pasar sus estructuras por valor es que un compilador optimizador puede optimizar mejor su código.
fuente