¿Cuál es la ventaja de devolver un puntero a una estructura en lugar de devolver toda la estructura en el return
enunciado 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 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 NULL
puntero.
Devolver una estructura completa que NULL
sería más difícil, supongo, o menos eficiente. ¿Es esta una razón válida?
fuente
gets()
función completamente mal diseñada . Algunos programadores todavía tienen aversión a copiar estructuras, los viejos hábitos mueren con dificultad.FILE*
Es efectivamente un mango opaco. El código de usuario no debería importarle cuál es su estructura interna.&
y acceder a un miembro con.
".Respuestas:
Existen varias razones prácticas por las que funciones como
fopen
punteros de retorno en lugar de instancias destruct
tipos:struct
tipo del usuario;En el caso de tipos como
FILE *
, es porque no desea exponer detalles de la representación del tipo al usuario: unFILE *
objeto sirve como un controlador opaco, y simplemente pasa ese controlador a varias rutinas de E / S (y aunque aFILE
menudo es implementado como unstruct
tipo, no tiene que ser).Por lo tanto, puede exponer un tipo incompleto
struct
en un encabezado en alguna parte: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 defopen
,freopen
etc., pero no puedo manipular directamente el objeto al que apunta.También es probable que la
fopen
función esté asignando unFILE
objeto dinámicamente, usandomalloc
o similar. En ese caso, tiene sentido devolver un puntero.Finalmente, es posible que esté almacenando algún tipo de estado en un
struct
objeto, y necesita hacer que ese estado esté disponible en varios lugares diferentes. Si devuelve instancias delstruct
tipo, 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.fuente
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.
fuente
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,
fopen
devuelve solo un dato (el abiertoFILE*
) y, en caso de error, da el código de error a través de laerrno
variable pseudo-global. Pero quizás sería mejor devolver unostruct
de los dos miembros: elFILE*
identificador y el código de error (que se establecería si el identificador del archivo esNULL
). Por razones históricas, no es el caso (y los errores se informan a través de loerrno
global, 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
struct
de 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
struct
puede ser más legible, más amigable con los hilos y más eficiente.fuente
rdx:rax
. Porstruct foo { int a,b; };
lo tanto, se devuelve empaquetado enrax
(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.std::optional<int>
devuelve el booleano en la mitad superior derax
, por lo que necesita una constante de máscara de 64 bits para probarlotest
. O podrías usarbt
. Pero es una mierda para la persona que llama y la persona que llama en comparación con el usodl
, 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)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.div_t div()
Estás en el camino correcto
Las dos razones que mencionó son válidas:
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:
fuente
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.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 llamafopen
, generalmente no le importará ninguno de los contenidos de la estructura devuelta; lo único que le importará es que otras funciones comofread
hagan 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 afread
debería ser capaz de actualizar esa información. Haberfread
recibido un puntero alFILE
hace que sea fácil Si, enfread
cambio, recibiera unFILE
, no tendría forma de actualizar elFILE
objeto retenido por la persona que llama.fuente
Ocultación de informació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
struct
privado, 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,
FILE
entonces, 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 deFILE
solo será visible para aquellos que implementan las operaciones que requieren su definición, comofopen
, 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
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) relacionadasFILE
.No es una razón tan válida, al menos desde el punto de vista del rendimiento, para hacer que
fopen
devuelva un puntero, ya que la única razón por la que devolveríaNULL
es 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 permitirNULL
que 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
fclose
no 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ónFILE
y devolverlo por valorfopen
o 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
malloc
y 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:
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:
Ese espacio reservado es un poco derrochador, pero puede salvarle la vida si descubrimos en el futuro que necesitamos agregar más datos
Foo
sin 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:
... para inicializar
Foo
desde la pila sin la posibilidad de una copia superflua. O el cliente incluso tiene la libertad de asignarFoo
en el montón si lo desea por alguna razón.fuente