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 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 de FILE
solo 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 fopen
devuelva un puntero, ya que la única razón por la que devolvería NULL
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 permitir NULL
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ón FILE
y devolverlo por valor fopen
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:
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 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:
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
desde 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.