En C, ¿cómo elegiría si devolver una estructura o un puntero a una estructura?


Trabajando en mi músculo C últimamente y mirando a través de las muchas bibliotecas que he estado trabajando con su sin duda 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() { ... }

De lo que he absorbido esta es la forma" correcta " de hacer esto:

something_t *make_something() { ... }
void destroy_something(something_t *object) { ... }

La arquitectura del fragmento de código 2 es mucho más popular que el fragmento de código 1. Así que ahora pregunto, ¿por qué devolvería una estructura directamente, como en el fragmento 1? Qué diferencias deberían Tengo en cuenta cuando estoy eligiendo entre las dos opciones?

Además, ¿cómo se compara esta opción?

void make_something(something_t *object)
Author: Sanchke Dellowar, 2016-10-21

6 answers

Cuando something_t es pequeño (léase: copiarlo es tan barato como copiar un puntero) y desea que se asigne por pila de forma predeterminada:

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 usted quiere que sea montón-asignado:

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 se asigna:

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);
 57
Author: Jon Purdy,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2016-10-21 04:16:24

Esto es casi siempre sobre 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. Rara vez se trata de structs extremadamente grandes o rendimiento.


Es extremadamente raro que asignar un struct en el montón y devolverlo sea casi tan rápido como devolverlo por valor. El struct tendría que ser enorme.

Realmente, la velocidad no es la razón detrás de la técnica 2, return-by-pointer, en lugar de return-by-value.

La técnica 2 existe para la estabilidad ABI. Si tiene un struct y su próxima versión de la biblioteca le agrega otros 20 campos, los consumidores de su versión anterior de la biblioteca son compatibles binarios si se les entregan punteros preconstruidos. Los datos adicionales más allá del final del struct que conocen es algo que no tienen que saber.

Si lo devuelve en la pila, la persona que llama está asignando la memoria para ello, y deben estar de acuerdo con usted en lo grande que es. Si tu biblioteca se actualizó desde la última vez que se reconstruyó, vas a desechar la pila.

La técnica 2 también le permite ocultar datos adicionales tanto antes como después del puntero que devuelve (de las cuales versiones que agregan datos al final de la estructura es una variante). Puede terminar la estructura con una matriz de tamaño variable, o anteponer el puntero con algunos datos adicionales, o ambos.

Si desea structs asignados a la pila en un ABI estable, casi todas las funciones que se comunican con struct necesitan información de versión pasada.

So

something_t make_something(unsigned library_version) { ... }

Donde library_version es utilizado por la biblioteca para determinar qué versión de something_t se espera que devuelva 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 version como primer elemento (o un campo de tamaño), y requeriría que se rellene antes de llamar make_something.

Otro código de biblioteca que tome un something_t consultaría el campo version para determinar con qué versión de something_t están trabajando.

 36
Author: Yakk - Adam Nevraumont,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2016-11-17 16:19:49

Como regla general, nunca debe pasar struct objetos por valor. En la práctica, estará bien hacerlo siempre y cuando 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 generalmente lo evita incluso entonces. Si nunca pasa estructuras por valor, puede agregar miembros a la estructura más adelante y no afectará el rendimiento.

Creo que void make_something(something_t *object) es la forma más común de usar estructuras en C. Deja la asignación a la llama. Es eficiente pero no es 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 obliga a usar punteros. Si el puntero devuelto apunta a la memoria dinámica u otra cosa depende de la implementación. OO con el tipo opaco es a menudo una de las maneras más elegantes y mejores de diseñar programas más complejos de C, pero tristemente, pocos programadores de C saben/se preocupan por ello.

 13
Author: Lundin,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2016-10-21 06:54:32

Algunos pros 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 pequeños.
  • No hay pérdida de memoria debido al olvido de free.

Algunos contras:

  • Si el objeto es grande (digamos, un megabyte) , puede causar desbordamiento de 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.
 9
Author: M.M,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2016-10-21 04:44:43

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, o código 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 en 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 ser llamado explicitly.So es mucho más fácil asegurarse de que no hay fugas de memoria y para manejar excepciones es todo va en la pila. En C, el destructor debe ser llamado explícitamente de todos modos, y no hay concepto de una función destructora especial (tienes destructores, por supuesto, pero son solo funciones normales con nombres como destroy_myobject()).

Ahora la excepción en C++ es para objetos contenedores de bajo nivel, por ejemplo, vectores, árboles, mapas hash, etc. Estos retienen miembros del montón, y tienen destructores. Ahora la mayoría de los objetos pesados de memoria consisten en algunos miembros de datos inmediatos que dan tamaños, ids, 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. Así que la mayoría de los datos es de hecho en el montón, incluso en C++.

Y 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);
}

Compilará un código bastante eficiente. El miembro de observación grande no generará copias innecesarias de makework.

 4
Author: Malcolm McLean,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2016-10-21 04:08:49

A diferencia de otros lenguajes (como Python), C no tiene el concepto de una tupla . 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 y y.

Dado que C no tiene el concepto de una tupla, es inconveniente devolver varios valores de una función. Una forma de evitar esto es definir una estructura para contener los valores, y luego devolver la estructura, como esto:

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 donde puede ver que una función devuelve una estructura.

 3
Author: user3386109,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2016-10-21 03:11:13