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

49

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

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

Creo que esta es más una opción de diseño que una simple pregunta de programación y tengo curiosidad por saber más sobre las ventajas y desventajas de los dos métodos.

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

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

yoyo_fun
fuente
10
@ 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?
yoyo_fun
28
La pre-estandarización C no permitía copiar estructuras ni pasarlas por valor. La biblioteca estándar de C tiene muchos recursos de esa época que no se escribirían de esa manera hoy, por ejemplo, tomó hasta C11 para eliminar la gets()función completamente mal diseñada . Algunos programadores todavía tienen aversión a copiar estructuras, los viejos hábitos mueren con dificultad.
amon
26
FILE*Es efectivamente un mango opaco. El código de usuario no debería importarle cuál es su estructura interna.
CodesInChaos
3
La devolución por referencia es solo un valor predeterminado razonable cuando tiene recolección de basura.
Idan Arye
77
@ JohnR.Strohm El "muy superior" en su perfil parece remontarse antes de 1989 ;-) - cuando ANSI C permitió lo que K&R C no permitió: Copiar estructuras en asignaciones, pasar parámetros y devolver valores. El libro original de K&R de hecho declaró explícitamente (estoy parafraseando): "puedes hacer exactamente dos cosas con una estructura, tomar su dirección & y acceder a un miembro con .".
Peter - Restablece a Monica el

Respuestas:

61

Existen varias razones prácticas por las que funciones como fopenpunteros de retorno en lugar de instancias de structtipos:

  1. Desea ocultar la representación del structtipo del usuario;
  2. Estás asignando un objeto dinámicamente;
  3. Se refiere 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 detalles de la representación del tipo al usuario: un FILE *objeto sirve como un controlador opaco, y simplemente pasa ese controlador a varias rutinas de E / S (y aunque a FILEmenudo es implementado como un structtipo, 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 declararle un puntero. Entonces puedo crear un FILE *y asignarlo a través de fopen, freopenetc., pero no puedo manipular directamente el objeto al que apunta.

También es probable que la fopenfunción esté asignando un FILEobjeto dinámicamente, usando malloco similar. En ese caso, tiene sentido devolver un puntero.

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

John Bode
fuente
31
Una ventaja particular de usar el puntero como un tipo opaco es que la estructura misma puede cambiar entre las versiones de la biblioteca y no es necesario volver a compilar los llamadores.
Barmar
66
@Barmar: De hecho, ABI Stability es el gran punto de venta de C, y no sería tan estable sin indicadores opacos.
Matthieu M.
37

Hay dos formas de "devolver una estructura". Puede devolver una copia de los datos o puede devolverle una referencia (puntero). En general, 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 que su código hace con frecuencia, puede causar una notable diferencia de rendimiento.

En segundo lugar, no importa cuántas veces copie un puntero, sigue 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 se equivoca.

Mason Wheeler
fuente
54
El inconveniente de regresar por puntero: ahora tienes que rastrear la propiedad de ese objeto y posiblemente liberarlo. Además, la indirección del puntero puede ser más costosa que una copia rápida. Aquí hay muchas variables, por lo que usar punteros no es universalmente mejor.
amon
17
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, no siempre se puede decir que copiar un puntero cuesta menos que copiar una estructura.
Solomon Slow
37
Esta es principalmente una buena respuesta, pero no estoy de acuerdo con la parte que a veces, muy raramente, es lo que quieres, pero la mayoría de las veces no lo es , todo lo contrario. Devolver un puntero permite varios tipos de efectos secundarios no deseados, y varios tipos de formas desagradables para equivocarse en la propiedad de un puntero. En los casos en que el tiempo de CPU no es tan importante, prefiero la variante de copia, si esa es una opción, es mucho menos propensa a errores.
Doc Brown
66
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 devuelva una estructura grande para tomar un puntero como argumento adicional y construir el objeto directamente allí. Los argumentos de inmutable frente a 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 quieres no es cierta.
Voo
66
También podría mencionar los cortafuegos de compilación como un profesional para 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 la interfaz y la implementación están separadas. Devolver (y pasar, asignar) por valor necesita la información de implementación.
Peter - Restablece a Monica el
12

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

Para tomar un ejemplo, fopendevuelve solo un dato (el abierto FILE*) y, en caso de error, da el código de error a través de la errnovariable pseudo-global. Pero quizás sería mejor devolver uno structde los 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 de lo errnoglobal, que hoy es una macro).

Tenga en cuenta que el lenguaje Go tiene una buena notación para devolver dos (o algunos) valores.

Observe también que en Linux / x86-64 las convenciones ABI y de llamada (consulte la página x86-psABI ) especifica que uno structde los 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 C pequeño structpuede ser más legible, más amigable con los hilos y más eficiente.

Basile Starynkevitch
fuente
En realidad pequeñas estructuras están empaquetados en rdx:rax. Por struct foo { int a,b; };lo tanto, se devuelve empaquetado en rax(p. Ej., Con shift / or), y se debe desempaquetar con shift / mov. Aquí hay un ejemplo en 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 demasiado malo, pero definitivamente peor que usar 2 registros la mayor parte del tiempo para estructuras de 2 miembros.
Peter Cordes
Relacionado: bugs.llvm.org/show_bug.cgi?id=34840 std::optional<int> devuelve el booleano en la mitad superior de rax, por lo que necesita una constante de máscara de 64 bits para probarlo test. O podrías usar bt. Pero es una mierda para la persona que llama y la persona que llama en comparación con el uso dl, lo que los compiladores deberían hacer para las funciones "privadas". También relacionado: libstdc ++ std::optional<T>no se puede copiar trivialmente incluso cuando T lo es, por lo que siempre regresa a través de un puntero oculto: stackoverflow.com/questions/46544019/… . (libc ++ es trivialmente copiable)
Peter Cordes
@PeterCordes: sus cosas relacionadas son C ++, no C
Basile Starynkevitch
Vaya, cierto. Pues lo mismo se aplicaría exactamente a struct { int a; _Bool b; };en C, si la persona que llama quería probar el booleano, porque trivialmente-copiable C ++ estructuras utilizan el mismo ABI como C.
Peter Cordes
1
Ejemplo clásicodiv_t div()
chux - Restablecer Monica
6

Estás en el camino correcto

Las dos razones que mencionó son válidas:

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

Devolver una estructura FULL que sea 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 razón más importante es la asignación dinámica de memoria. Muchas veces, cuando se compila un programa, no está seguro exactamente 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 termine de usar 'free'.

Un buen ejemplo de esto es leer un archivo especificado por el usuario. En este caso, no tiene idea de qué tan grande puede ser el archivo cuando compila el programa. Solo puede calcular cuánta memoria necesita cuando el programa realmente se está ejecutando.

Tanto malloc como los punteros de retorno libre a ubicaciones en la memoria. Por lo tanto, las funciones que utilizan la asignación de memoria dinámica devolverán los punteros a 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;
}
Ryan
fuente
¿Cómo es posible no saber cuánta memoria necesitará una determinada variable si ya tiene definido el tipo de estructura?
yoyo_fun
99
@JenniferAnderson C tiene un concepto de tipos incompletos: un nombre de tipo puede declararse pero aún no definirse, por lo que su tamaño no está disponible. No puedo declarar variables de ese tipo, pero puedo declarar punteros a ese tipo, por ejemplo struct incomplete* foo(void). De esa manera puedo declarar funciones en un encabezado, pero solo definir las estructuras dentro de un archivo C, permitiendo así la encapsulación.
amon
@amon Entonces, ¿así es como se declaran los encabezados de función (prototipos / firmas) antes de declarar cómo funcionan realmente en C? Y es posible hacer lo mismo con las estructuras y los sindicatos en C
yoyo_fun
@JenniferAnderson declara prototipos de funciones (funciones sin cuerpos) en los archivos de encabezado y luego puede llamar 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 definición de la función (es decir, con un cuerpo), pero solo necesita procesarla una vez. Si usa un tipo no simple, también necesita conocer la estructura de ese tipo, pero los punteros suelen ser del mismo tamaño y no importa para el uso de un prototipo.
simpleuser
6

Algo así 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 alguna otra entidad como un archivo. Cuando un programa llama fopen, generalmente no le importará ninguno de los contenidos de la estructura devuelta; lo único que le importará es que otras funciones como freadhagan lo que necesiten hacer con él.

Si una biblioteca estándar mantiene dentro de una FILE*información sobre, por ejemplo, la posición de lectura actual dentro de ese archivo, una llamada a freaddebería ser capaz de actualizar esa información. Haber freadrecibido un puntero al FILEhace que sea fácil Si, en freadcambio, recibiera un FILE, no tendría forma de actualizar el FILEobjeto retenido por la persona que llama.

Super gato
fuente
3

Ocultación de información

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

El más común es la ocultación de información . C no tiene, por ejemplo, la capacidad de hacer que los campos de un structprivado, y mucho menos proporcionar métodos para acceder a ellos.

Entonces, si desea evitar a la fuerza que los desarrolladores puedan ver y alterar el contenido de un puntero, FILEentonces, la única forma es evitar que se expongan a su definición tratando el puntero como opaco cuyo tamaño y puntero definición son desconocidas para el mundo exterior. La definición de FILEsolo será visible para aquellos que implementan 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 espacio para respirar para preservar la compatibilidad binaria en las API de dylib. Permite a los implementadores de la biblioteca cambiar los campos en la estructura opaca sin romper la compatibilidad binaria con aquellos que 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 ejemplo, actualmente puedo ejecutar algunos programas antiguos creados durante la era de Windows 95 hoy (no siempre perfectamente, pero sorprendentemente muchos todavía funcionan). Lo más probable es que parte del código para esos binarios antiguos usara 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 la compatibilidad con versiones anteriores.

Eficiencia

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

Por lo general, es menos eficiente suponiendo que el tipo prácticamente puede caber y asignarse en la pila a menos que normalmente se use un asignador de memoria mucho menos generalizado detrás de escena que malloc, como una memoria de asignación de asignador de tamaño fijo en lugar de variable, ya asignada. Es un compromiso de seguridad en este caso, muy probablemente, permitir que los desarrolladores de la biblioteca mantengan invariantes (garantías conceptuales) relacionadas FILE.

No es una razón tan válida, al menos desde el punto de vista del rendimiento, para hacer que fopendevuelva un puntero, ya que la única razón por la que devolvería NULLes por no abrir 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 para permitir NULLque se devuelvan en alguna condición posterior.

Para las operaciones de archivo, la sobrecarga es relativamente trivial en comparación con las operaciones de archivo en sí, y el manual fcloseno debe evitarse de todos modos. Por lo tanto, no es como si pudiéramos ahorrarle al cliente la molestia de liberar (cerrar) el recurso al exponer la definición FILEy devolverlo por valor fopeno esperar un gran aumento del rendimiento dado el costo relativo de las operaciones de archivo para evitar una asignación de montón. .

Puntos calientes y soluciones

Sin embargo, para otros casos, he perfilado una gran cantidad de código C derrochador en bases de códigos heredadas con puntos de acceso mallocy errores innecesarios de caché obligatorios 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 uso en su lugar es exponer las definiciones de estructura, incluso si el cliente no está destinado a manipularlas, utilizando 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 he encontrado que es lo suficientemente bueno como para reservar un poco de espacio extra para fines futuros, de esta manera:

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 salvarle la vida si descubrimos en el futuro que necesitamos agregar más datos Foosin romper los binarios que usan nuestra biblioteca.

En mi opinión, la ocultación de la información y la compatibilidad binaria suelen ser la única razón decente para permitir solo la asignación de estructuras de montón además de las estructuras de longitud variable (que siempre lo requerirían, o al menos sería un poco incómodo de usar de otra manera si el cliente tuviera que asignar memoria en la pila en forma de VLA para asignar el VLS). Incluso las estructuras grandes a menudo son más baratas de devolver por valor si eso significa que el software funciona mucho más con la memoria activa en la pila. E incluso si no fueran más baratos devolver por valor en la creación, uno simplemente 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 Foodesde la pila sin la posibilidad de una copia superflua. O el cliente incluso tiene la libertad de asignar Fooen el montón si lo desea por alguna razón.


fuente