que - En C, ¿cómo elegiría si devolver una estructura o un puntero a una estructura?
punteros a cadenas (6)
Trabajar en mi músculo C últimamente y revisar las muchas bibliotecas con las que he estado trabajando me dio una buena idea de lo que es una buena práctica. Una cosa que NO he visto es una función que devuelve una estructura:
something_t make_something() { ... }
Por lo que he absorbido, esta es la forma "correcta" de hacer esto:
something_t *make_something() { ... }
void destroy_something(something_t *object) { ... }
La arquitectura en el fragmento de código 2 es MUCHO más popular que el fragmento 1. Así que ahora pregunto, ¿por qué devolvería una estructura directamente, como en el fragmento 1? ¿Qué diferencias debo tener en cuenta al elegir entre las dos opciones?
Además, ¿cómo se compara esta opción?
void make_something(something_t *object)
A diferencia de otros lenguajes (como Python), C no tiene el concepto de una tuple . Por ejemplo, lo siguiente es legal en Python:
def foo():
return 1,2
x,y = foo()
print x, y
La función
foo
devuelve dos valores como una tupla, que se asignan a
x
e
y
.
Como C no tiene el concepto de tupla, es inconveniente devolver múltiples valores de una función. Una forma de evitar esto es definir una estructura para contener los valores, y luego devolver la estructura, así:
typedef struct { int x, y; } stPoint;
stPoint foo( void )
{
stPoint point = { 1, 2 };
return point;
}
int main( void )
{
stPoint point = foo();
printf( "%d %d/n", point.x, point.y );
}
Este es solo un ejemplo en el que puede ver que una función devuelve una estructura.
Algunas ventajas del primer enfoque:
- Menos código para escribir.
- Más idiomático para el caso de uso de devolver múltiples valores.
- Funciona en sistemas que no tienen asignación dinámica.
- Probablemente más rápido para objetos pequeños o más pequeños.
-
No hay pérdida de memoria debido a olvidarse de
free
.
Algunas desventajas:
- Si el objeto es grande (digamos, un megabyte), puede provocar un desbordamiento de la pila o puede ser lento si los compiladores no lo optimizan bien.
- Puede sorprender a las personas que aprendieron C en la década de 1970 cuando esto no era posible y no se han mantenido al día.
- No funciona con objetos que contienen un puntero a una parte de sí mismos.
Casi siempre se trata de la estabilidad ABI.
Estabilidad binaria entre versiones de la biblioteca.
En los casos en que no lo es, a veces se trata de tener estructuras de tamaño dinámico.
Raramente se trata de
struct
o rendimiento extremadamente grandes.
Es extremadamente raro que asignar una
struct
en el montón y devolverla sea casi tan rápido como devolverla por valor.
La
struct
tendría que ser enorme.
Realmente, la velocidad no es la razón detrás de la técnica 2, retorno por puntero, en lugar de retorno por valor.
La técnica 2 existe para la estabilidad ABI.
Si tiene una
struct
y su próxima versión de la biblioteca agrega otros 20 campos, los consumidores de su versión anterior de la biblioteca
son compatibles con los binarios
si reciben punteros preconstruidos.
Los datos adicionales más allá del final de la
struct
que conocen es algo que no tienen que saber.
Si lo devuelve a la pila, la persona que llama está asignando la memoria y debe estar de acuerdo con usted en cuanto a su tamaño. Si su biblioteca se actualizó desde la última vez que se reconstruyó, va a la papelera.
La técnica 2 también le permite ocultar datos adicionales antes y después del puntero que devuelve (las versiones que anexan datos al final de la estructura es una variante de). Puede finalizar la estructura con una matriz de tamaño variable, o anteponer el puntero con algunos datos adicionales, o ambos.
Si desea
struct
asignadas a la pila en un ABI estable, casi todas las funciones que se comunican con la
struct
deben pasar información de versión.
Entonces
something_t make_something(unsigned library_version) { ... }
donde
library_version
es utilizada por la biblioteca para determinar qué versión de
library_version
se espera que regrese y
cambia la cantidad de la pila que manipula
.
Esto no es posible usando el estándar C, pero
void make_something(something_t* here) { ... }
es.
En este caso,
something_t
podría tener un campo de
version
como primer elemento (o un campo de tamaño), y necesitaría que se rellenara antes de llamar a
make_something
.
Otro código de biblioteca que toma
something_t
algo_t luego consultaría el campo de
version
para determinar con qué versión de
something_t
están trabajando.
Como regla general, nunca debe pasar objetos de
struct
por valor.
En la práctica, estará bien hacerlo siempre que sean más pequeños o iguales al tamaño máximo que su CPU puede manejar en una sola instrucción.
Pero estilísticamente, uno típicamente lo evita incluso entonces.
Si nunca pasa estructuras por valor, más tarde puede agregar miembros a la estructura y esto no afectará el rendimiento.
Creo que
void make_something(something_t *object)
es la forma más común de usar estructuras en C.
void make_something(something_t *object)
la asignación a la persona que llama.
Es eficiente pero no bonito.
Sin embargo, los programas C orientados a objetos usan
something_t *make_something()
ya que están construidos con el concepto de
tipo opaco
, lo que te obliga a usar punteros.
Si el puntero devuelto apunta a la memoria dinámica u otra cosa depende de la implementación.
OO con tipo opaco es a menudo una de las mejores y más elegantes formas de diseñar programas en C más complejos, pero lamentablemente, pocos programadores en C lo saben / les importa.
Cuando
something_t
es pequeño (léase: copiarlo es tan barato como copiar un puntero) y desea que se asigne de forma predeterminada a la pila:
something_t make_something(void);
something_t stack_thing = make_something();
something_t *heap_thing = malloc(sizeof *heap_thing);
*heap_thing = make_something();
Cuando
something_t
es grande o desea que esté asignado en el montón:
something_t *make_something(void);
something_t *heap_thing = make_something();
Independientemente del tamaño de
something_t
, y si no te importa dónde está asignado:
void make_something(something_t *);
something_t stack_thing;
make_something(&stack_thing);
something_t *heap_thing = malloc(sizeof *heap_thing);
make_something(heap_thing);
Estoy algo sorprendido
La diferencia es que el ejemplo 1 crea una estructura en la pila, el ejemplo 2 la crea en el montón. En código C o C ++ que es efectivamente C, es idiomático y conveniente crear la mayoría de los objetos en el montón. En C ++ no lo es, en su mayoría van a la pila. La razón es que si crea un objeto en la pila, el destructor se llama automáticamente, si lo crea en el montón, debe llamarse explícitamente, por lo que es mucho más fácil asegurarse de que no haya pérdidas de memoria y manejar las excepciones. todo va a la pila. En C, el destructor debe llamarse explícitamente de todos modos, y no existe el concepto de una función destructora especial (tiene destructores, por supuesto, pero son funciones normales con nombres como destroy_myobject ()).
Ahora la excepción en C ++ es para objetos de contenedor de bajo nivel, por ejemplo, vectores, árboles, mapas hash, etc. Estos retienen a los miembros del montón y tienen destructores. Ahora, la mayoría de los objetos con mucha memoria consisten en unos pocos miembros de datos inmediatos que proporcionan tamaños, identificadores, etiquetas, etc., y luego el resto de la información en estructuras STL, tal vez un vector de datos de píxeles o un mapa de pares de palabras / valores en inglés. Entonces, la mayoría de los datos están de hecho en el montón, incluso en C ++.
Y el C ++ moderno está diseñado para que este patrón
class big
{
std::vector<double> observations; // thousands of observations
int station_x; // a bit of data associated with them
int station_y;
std::string station_name;
}
big retrieveobservations(int a, int b, int c)
{
big answer;
// lots of code to fill in the structure here
return answer;
}
void high_level()
{
big myobservations = retriveobservations(1, 2, 3);
}
Se compilará en un código bastante eficiente. El miembro de observación grande no generará copias de trabajo innecesarias.