¿Por qué muchas funciones que devuelven estructuras en C, en realidad devuelven punteros a estructuras?

¿Cuál es la ventaja de devolver un puntero a una estructura en lugar de devolver la estructura completa en el return declaración de la función?

Estoy hablando de funciones como fopen y otras funciones de bajo nivel, pero probablemente hay funciones de nivel superior que también devuelven punteros a estructuras.

Creo que se trata más de una elección de diseño que de una cuestión de programación y tengo curiosidad por saber más sobre las ventajas y desventajas de los dos métodos.

Uno de los Las razones por las que pensé que sería una ventaja devolver un puntero a una estructura es poder saber más fácilmente si la función falla al devolver el puntero NULL.

Devolver una estructura completa que sea NULL supongo que sería más difícil o menos eficiente. ¿Es esta una razón válida?

Comentarios

  • @ JohnR.Strohm Lo probé y realmente funciona. Una función puede devolver una estructura … Entonces, ¿cuál es la razón por la que no se hace?
  • La preestandarización C no permitía que las estructuras se copiaran o pasaran por valor. La biblioteca estándar de C tiene muchas reservas de esa época que no se escribirían de esa manera hoy, p. Ej. se necesitó hasta C11 para eliminar la función gets() completamente mal diseñada. Algunos programadores todavía tienen aversión a copiar estructuras, los viejos hábitos son difíciles de eliminar.
  • FILE* es efectivamente un identificador opaco. El código de usuario no debería importarle cuál es su estructura interna.
  • El retorno por referencia es solo un valor predeterminado razonable cuando se tiene recolección de basura.
  • @ JohnR.Strohm El » muy senior » en su perfil parece remontarse antes de 1989 😉 – cuando ANSI C permitía lo que K & RC didn ‘ t: copiar estructuras en asignaciones, pasar parámetros y devolver valores. El libro original de K & R ‘ s de hecho se indica explícitamente (yo ‘ m parafraseo): » puedes hacer exactamente dos cosas con una estructura, tomar su dirección con & y acceder a un miembro con .. »

Responder

Ahí Hay varias razones prácticas por las que funciones como fopen devuelven punteros a en lugar de instancias de struct tipos:

  1. Quiere ocultar la representación del tipo struct del usuario;
  2. Está asignando un objeto dinámicamente;
  3. Está refiriéndose a una sola instancia de un objeto a través de múltiples referencias;

En el caso de tipos como FILE *, es porque no desea exponer los detalles de la representación del tipo al usuario – un FILE * obje ct sirve como un identificador opaco, y simplemente pasa ese identificador a varias rutinas de E / S (y aunque FILE a menudo se implementa como un struct tipo, no tiene que ser).

Por lo tanto, puede exponer un tipo incompleto struct en un encabezado en alguna parte:

typedef struct __some_internal_stream_implementation FILE; 

Si bien no puede declarar una instancia de un tipo incompleto, puede declarar un puntero a ella. Entonces puedo crear un FILE * y asignarlo a través de fopen, freopen, etc. , pero no puedo manipular directamente el objeto al que apunta.

También es probable que la función fopen esté asignando un FILE objeto dinámicamente, usando malloc o similar. En ese caso, tiene sentido devolver un puntero.

Finalmente, es posible que esté almacenando algún tipo de estado en un objeto struct, y necesita hacer que ese estado esté disponible en varios lugares diferentes. Si devolvió instancias del tipo struct, esas instancias serían objetos separados en la memoria entre sí y, finalmente, perderían la sincronización. Al devolver un puntero a un solo objeto, todos se refieren al mismo objeto.

Comentarios

  • Una ventaja particular de usar el puntero como un El tipo opaco es que la estructura en sí puede cambiar entre las versiones de la biblioteca y usted ‘ no necesita volver a compilar las personas que llaman.
  • @Barmar: De hecho, la estabilidad de ABI es el gran punto de venta de C, y no sería tan estable sin punteros opacos.

Respuesta

Hay dos formas de» devolver una estructura «. Puede devolver una copia de los datos o puede devolver una referencia (puntero) a ella.Generalmente se prefiere devolver (y pasar en general) un puntero, por un par de razones.

Primero, copiar una estructura requiere mucho más tiempo de CPU que copiar un puntero. Si esto es algo su código lo hace con frecuencia, puede causar una diferencia de rendimiento notable.

En segundo lugar, no importa cuántas veces copie un puntero, seguirá apuntando a la misma estructura en la memoria. Todas las modificaciones se reflejarán en la misma estructura. Pero si copia la estructura en sí y luego realiza una modificación, el cambio solo aparece en esa copia . Cualquier código que contenga una copia diferente no verá el cambio. A veces, muy raramente, esto es lo que desea, pero la mayoría de las veces no lo es, y puede causar errores si lo hace mal.

Comentarios

  • El inconveniente de regresar por puntero: ahora ‘ tienes que rastrear la propiedad de ese objeto y posibles libéralo. Además, la indirección del puntero puede ser más costosa que una copia rápida. Aquí hay muchas variables, por lo que el uso de punteros no es universalmente mejor.
  • Además, los punteros en estos días son de 64 bits en la mayoría de las plataformas de escritorio y servidor. ‘ he visto más de unas pocas estructuras en mi carrera que encajarían en 64 bits. Por lo tanto, puede ‘ t siempre decir que copiar un puntero cuesta menos que copiar una estructura.
  • Esta es principalmente una buena respuesta , pero no estoy de acuerdo con la parte a veces, muy raramente, esto es lo que quieres, pero la mayoría de las veces ‘ no – todo lo contrario. Devolver un puntero permite varios tipos de efectos secundarios no deseados y varios tipos de formas desagradables de equivocarse en la propiedad de un puntero. En los casos en los que el tiempo de la CPU no es tan importante, prefiero la variante de copia, si esa es una opción, es mucho menos propensa a errores.
  • Cabe señalar que esto realmente solo se aplica a las API externas. Para las funciones internas, cada compilador incluso marginalmente competente de las últimas décadas reescribirá una función que devuelve una estructura grande para tomar un puntero como argumento adicional y construir el objeto directamente allí. Los argumentos de inmutable vs mutable se han hecho con bastante frecuencia, pero creo que todos podemos estar de acuerdo en que la afirmación de que las estructuras de datos inmutables casi nunca son lo que desea no es cierta.
  • También podría mencionar las paredes cortafuegos de compilación como profesional de los punteros. En programas grandes con encabezados ampliamente compartidos, los tipos incompletos con funciones evitan la necesidad de volver a compilar cada vez que cambia un detalle de implementación. El mejor comportamiento de compilación es en realidad un efecto secundario de la encapsulación que se logra cuando se separan la interfaz y la implementación. Devolver (y pasar, asignar) por valor necesita la información de implementación.

Respuesta

Además de otras respuestas , a veces vale la pena devolver un pequeño struct por valor. Por ejemplo, uno podría devolver un par de datos y algún código de error (o éxito) relacionado con él.

Para tomar un ejemplo, fopen devuelve solo un dato (el FILE* abierto) y en caso de error, da el código de error a través del errno variable pseudo-global. Pero quizás sería mejor devolver un struct de dos miembros: el FILE* identificador y el código de error (que se establecería si el identificador del archivo es NULL). Por razones históricas, no es el caso (y los errores se informan a través del errno global, que hoy es una macro).

Observe que el Go language tiene una buena notación para devolver dos (o algunos) valores.

Note también que en Linux / x86-64 el ABI y convenciones de llamada (consulte la página x86-psABI ) especifica que una struct de dos miembros escalares (por ejemplo, un puntero y un entero, o dos punteros, o dos enteros) se devuelve a través de dos registros (y esto es muy eficiente y no pasa por la memoria).

Entonces, en el nuevo código C, devolver un pequeño C struct puede ser más legible, más compatible con subprocesos y más eficiente.

Comentarios

  • En realidad, las estructuras pequeñas están empaquetadas en rdx:rax. Entonces, struct foo { int a,b; }; se devuelve empaquetado en rax (por ejemplo, con shift / o), y debe descomprimirse con shift / mov. Aquí ‘ hay un ejemplo de Godbolt . Pero x86 puede usar los 32 bits bajos de un registro de 64 bits para operaciones de 32 bits sin preocuparse por los bits altos, por lo que ‘ siempre es una lástima, pero definitivamente es peor que usar 2 registra la mayor parte del tiempo para estructuras de 2 miembros.
  • Relacionado: bugs.llvm.org/show_bug.cgi? id = 34840 std::optional<int> devuelve el valor booleano en la mitad superior de rax, por lo que necesita una máscara de 64 bits constante para probarlo con test. O puede usar bt. Pero es una mierda para la persona que llama y el destinatario de la llamada en comparación con el uso de dl, que los compiladores deberían hacer para » private » funciones. También relacionado: libstdc ++ ‘ s std::optional<T> isn ‘ t trivialmente copiable incluso cuando T es , por lo que siempre regresa a través de un puntero oculto: stackoverflow.com/questions/46544019/… . (libc ++ ‘ s se puede copiar trivialmente)
  • @PeterCordes: tus cosas relacionadas son C ++, no C
  • Vaya, cierto. Bueno, lo mismo se aplicaría exactamente a struct { int a; _Bool b; }; en C, si el llamador quisiera probar el booleano, porque las estructuras de C ++ que se pueden copiar trivialmente usan la misma ABI que C.
  • Ejemplo clásico div_t div()

Respuesta

Estás en el camino correcto

Las dos razones que mencionaste son válidas:

Una de las razones por las que Pensé que sería una ventaja devolver un puntero a una estructura para poder saber más fácilmente si la función falló al devolver el puntero NULL.

Devolver una estructura COMPLETA que es NULL sería más difícil, supongo o menos eficiente. ¿Es esta una razón válida?

Si tiene una textura (por ejemplo) en algún lugar de la memoria y desea hacer referencia a esa textura en varios lugares de su programa; No sería prudente hacer una copia cada vez que quisiera hacer referencia a ella. En cambio, si simplemente pasa un puntero para hacer referencia a la textura, su programa se ejecutará mucho más rápido.

Sin embargo, la principal razón es la asignación de memoria dinámica. A menudo, cuando se compila un programa, no está seguro exactamente de cuánta memoria necesita para ciertas estructuras de datos. Cuando esto sucede, la cantidad de memoria que necesita usar se determinará en tiempo de ejecución. Puede solicitar memoria usando malloc y luego liberarla cuando haya terminado de usar free.

Un buen ejemplo de esto es leer desde un archivo que es especificado por el usuario. En este caso, no tiene idea del tamaño del archivo cuando compila el programa. Solo puede calcular la cantidad de memoria que necesita cuando el programa se está ejecutando.

Tanto malloc como punteros de retorno gratuitos a ubicaciones en la memoria. que hacen uso de la asignación de memoria dinámica devolverán punteros al lugar donde han creado sus estructuras en la memoria.

Además, en los comentarios veo que hay una pregunta sobre si puede devolver una estructura desde una función. De hecho, puedes hacer esto. Lo siguiente debería funcionar:

struct s1 { int integer; }; struct s1 f(struct s1 input){ struct s1 returnValue = xinput return returnValue; } int main(void){ struct s1 a = { 42 }; struct s1 b= f(a); return 0; } 

Comentarios

  • ¿Cómo es posible no saber cuánta memoria ¿Necesitará una determinada variable si ya tiene definido el tipo de estructura?
  • @JenniferAnderson C tiene un concepto de tipos incompletos: un nombre de tipo puede ser declarado pero aún no definido, por lo que ‘ s tamaño no está disponible. No puedo declarar variables de ese tipo, pero puedo declarar punteros a ese tipo, p. Ej. struct incomplete* foo(void). De esa manera puedo declarar funciones en un encabezado, pero solo definir las estructuras dentro de un archivo C, lo que permite la encapsulación.
  • @amon Así es como se declaran los encabezados de función (prototipos / firmas) antes de declarar cómo el trabajo se hace realmente en C? Y es posible hacer lo mismo con las estructuras y uniones en C
  • @JenniferAnderson usted declara la función prototipos (funciones sin cuerpos) en archivos de encabezado y luego puede llamar a esas funciones en otro código, sin conocer el cuerpo de las funciones, porque el compilador solo necesita saber cómo organizar los argumentos y cómo aceptar el valor de retorno. En el momento en que vincula el programa, en realidad debe conocer la función definición (es decir, con un cuerpo), pero solo necesita procesarla una vez. Si utiliza un tipo no simple, también debe saber la estructura del tipo ‘, pero los punteros suelen tener el mismo tamaño y no ‘ no importa para un prototipo ‘ s uso.

Respuesta

Algo como un FILE* no es realmente un puntero a una estructura en lo que respecta al código del cliente, sino que es una forma de identificador opaco asociado con algún otra entidad como un archivo. Cuando un programa llama a fopen, generalmente no le importará ninguno de los contenidos de la estructura devuelta; todo lo que le importará es que otras funciones como fread harán lo que tengan que hacer con él.

Si una biblioteca estándar mantiene dentro de una FILE* información sobre p. ej. la posición de lectura actual dentro de ese archivo, una llamada a fread necesitaría poder actualizar esa información. Hacer que fread reciba un puntero al FILE lo hace fácil. Si fread en su lugar recibió un FILE, no tendría forma de actualizar el objeto FILE retenido por la persona que llama.

Responder

Ocultar información

¿Cuál es la ventaja de devolver un puntero a una estructura en lugar de devolver la estructura completa en la declaración de retorno de la función?

La más común es ocultar información . C no tiene, digamos, la capacidad de hacer que los campos de un struct sean privados, y mucho menos proporcionar métodos para acceder a ellos.

Entonces, si quiere forzar evitar que los desarrolladores puedan ver y manipular el contenido de un puntero, como FILE, entonces la única forma es evitar que estén expuestos a su definición tratando el puntero como opaco cuyo tamaño y definición de punteros son desconocidos para el mundo exterior. La definición de FILE solo será visible para aquellos que implementen las operaciones que requieren su definición, como fopen, mientras que solo la declaración de estructura será visible para el encabezado público.

Compatibilidad binaria

Ocultar la definición de la estructura también puede ayudar a proporcionar un respiro para preservar la compatibilidad binaria en las API de dylib. Permite a los implementadores de la biblioteca cambiar los campos en la estructura opaca ure sin romper la compatibilidad binaria con quienes usan la biblioteca, ya que la naturaleza de su código solo necesita saber qué pueden hacer con la estructura, no qué tan grande es o qué campos tiene.

Como Por ejemplo, actualmente puedo ejecutar algunos programas antiguos creados durante la era de Windows 95 (no siempre perfectamente, pero sorprendentemente muchos todavía funcionan). Lo más probable es que parte del código de esos binarios antiguos utilizara punteros opacos a estructuras cuyo tamaño y contenido han cambiado desde la era de Windows 95. Sin embargo, los programas continúan funcionando en nuevas versiones de Windows, ya que no estaban expuestos al contenido de esas estructuras. Cuando se trabaja en una biblioteca donde la compatibilidad binaria es importante, lo que el cliente no está expuesto generalmente puede cambiar sin romper compatibilidad con versiones anteriores.

Eficiencia

Devolver una estructura completa que sea NULL supongo que sería más difícil o menos eficiente. ¿Es esta una razón válida?

Por lo general, es menos eficiente asumir que el tipo puede caber prácticamente y ser asignado en la pila a menos que normalmente haya mucho menos asignador de memoria generalizado que se utiliza detrás de escena que malloc, como una memoria de agrupación de asignadores de tamaño fijo en lugar de tamaño variable ya asignada. Es una compensación de seguridad en este caso, la mayoría probablemente, para permitir que los desarrolladores de la biblioteca mantengan invariantes (garantías conceptuales) relacionadas con FILE.

No es una razón tan válida al menos desde el punto de vista del desempeño para hacer que fopen devuelva un puntero ya que la única razón por la que «d devuelve NULL es porque no se abre un archivo. Eso sería optimizar un escenario excepcional a cambio de ralentizar todas las rutas de ejecución de casos comunes. Puede haber una razón de productividad válida en algunos casos para hacer que los diseños sean más sencillos y hacer que devuelvan punteros que permitan que NULL se devuelva en alguna condición posterior.

Para las operaciones con archivos, la sobrecarga es relativamente bastante trivial en comparación con las operaciones con archivos en sí, y la necesidad manual de fclose no se puede evitar de todos modos. Por lo tanto, no podemos ahorrarle al cliente la molestia de liberar (cerrar) el recurso exponiendo la definición de FILE y devolviéndola por valor en fopen o espere un gran aumento de rendimiento dado el costo relativo de las operaciones de archivo para evitar una asignación de montón.

Hotspots y correcciones

Sin embargo, para otros casos, he perfilado una gran cantidad de código C derrochador en bases de código heredadas con hotspots en malloc y fallas de caché obligatorias innecesarias como resultado de usar esta práctica con demasiada frecuencia con punteros opacos y asignar demasiadas cosas innecesariamente en el montón, a veces en grandes bucles.

Una práctica alternativa que utilizo en su lugar es exponer las definiciones de estructura, incluso si el cliente no tiene la intención de alterarlas, mediante el uso de un estándar de convención de nomenclatura para comunicar que nadie más debe tocar los campos:

struct Foo { /* priv_* indicates that you shouldn"t tamper with these fields! */ int priv_internal_field; int priv_other_one; }; struct Foo foo_create(void); void foo_destroy(struct Foo* foo); void foo_something(struct Foo* foo); 

Si hay problemas de compatibilidad binaria en el futuro, entonces lo he encontrado lo suficientemente bueno como para reservar un espacio extra superfluo para propósitos futuros, así:

struct Foo { /* priv_* indicates that you shouldn"t tamper with these fields! */ int priv_internal_field; int priv_other_one; /* reserved for possible future uses (emergency backup plan). currently just set to null. */ void* priv_reserved; }; 

Ese espacio reservado es un poco derrochador, pero puede ser un salvavidas si en el futuro descubrimos que necesitamos agregar más datos a Foo sin romper los binarios que usan nuestra biblioteca.

En mi opinión, ocultar información y la compatibilidad binaria es típicamente la única razón decente para permitir solo la asignación de montón de estructuras además de estructuras de longitud variable (que siempre lo requerirían, o al menos sería un poco incómodo de usar de lo contrario si el cliente tuviera que asignar memoria en la pila en un fash VLA ion para asignar el VLS). Incluso las estructuras grandes suelen ser más baratas de devolver por valor si eso significa que el software trabaja mucho más con la memoria activa en la pila. E incluso si no fuera más barato devolverlos por valor en la creación, simplemente se podría hacer esto:

int foo_create(struct Foo* foo); ... /* In the client code: */ struct Foo foo; if (foo_create(&foo)) { foo_something(&foo); foo_destroy(&foo); } 

… para inicializar Foo de la pila sin la posibilidad de una copia superflua. O el cliente incluso tiene la libertad de asignar Foo en el montón si lo desea por alguna razón.

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *