pointer - Puntero de función emitida a una firma diferente
pointers in c pdf (7)
Debería estar bien. Como la persona que llama es responsable de limpiar la pila después de una llamada, no debe dejar nada adicional en la pila. El llamado (nada () en este caso) está bien ya que no intentará usar ningún parámetro en la pila.
EDITAR: esto asume las convenciones de llamada cdecl, que generalmente es el valor predeterminado para C.
Utilizo una estructura de indicadores de función para implementar una interfaz para diferentes backends. Las firmas son muy diferentes, pero los valores de retorno son casi todos vacíos, nulos * o int.
struct my_interface {
void (*func_a)(int i);
void *(*func_b)(const char *bla);
...
int (*func_z)(char foo);
};
Pero no es necesario que un backend admita funciones para cada función de interfaz. Entonces tengo dos posibilidades, la primera opción es verificar antes de cada llamada si el puntero es NULL desigual. No me gusta mucho, debido a la legibilidad y porque me temo que el rendimiento impacta (pero no lo he medido). La otra opción es tener una función ficticia, para los casos excepcionales una función de interfaz no existe.
Por lo tanto, necesitaría una función ficticia para cada firma, me pregunto si es posible tener solo una para los diferentes valores de devolución. Y echarlo a la firma dada.
#include <stdio.h>
int nothing(void) {return 0;}
typedef int (*cb_t)(int);
int main(void)
{
cb_t func;
int i;
func = (cb_t) nothing;
i = func(1);
printf("%d/n", i);
return 0;
}
Probé este código con gcc y funciona. ¿Pero está cuerdo? ¿O puede dañar la pila o puede causar otros problemas?
EDITAR: Gracias a todas las respuestas, aprendí mucho sobre las convenciones de llamadas, después de leer un poco más. Y ahora tengo una comprensión mucho mejor de lo que sucede bajo el capó.
El estándar C no admite explícitamente el lanzamiento de un puntero de función a NULL. Estás a merced del escritor del compilador. Funciona bien en muchos compiladores.
Una de las grandes molestias de C es que no existe un equivalente de NULL o void * para los punteros de función.
Si realmente desea que su código sea a prueba de balas, puede declarar sus propios valores nulos, pero necesita uno para cada tipo de función. Por ejemplo,
void void_int_NULL(int n) { (void)n; abort(); }
y luego puedes probar
if (my_thing->func_a != void_int_NULL) my_thing->func_a(99);
Ugly, ¿no?
Esto no funcionará a menos que utilice cosas específicas de la plataforma / específicas de la implementación para forzar la convención de llamadas correcta. Para algunas convenciones de llamadas, la función llamada es responsable de limpiar la pila, por lo que deben saber qué se ha aplicado.
Buscaría el NULL y luego llamaría. No puedo imaginar que tenga un impacto en el rendimiento.
Las computadoras pueden verificar NULL tan rápido como cualquier cosa que hagan.
Según la especificación C, el lanzamiento de un puntero a función da como resultado un comportamiento indefinido. De hecho, durante un tiempo, las versiones previas de GCC 4.3 devolverán NULL siempre que genere un puntero a la función, perfectamente válido para la especificación, pero retiraron ese cambio antes de la publicación porque rompió muchos programas.
Suponiendo que GCC continúe haciendo lo que hace ahora, funcionará bien con la convención de llamadas x86 predeterminada (y la mayoría de las convenciones de llamadas en la mayoría de las arquitecturas), pero yo no dependería de ello. Probar el puntero de función contra NULL en cada sitio de llamada no es mucho más caro que una llamada a función. Si realmente quieres, puedes escribir una macro:
#define CALL_MAYBE(func, args...) do {if (func) (func)(## args);} while (0)
O podría tener una función ficticia diferente para cada firma, pero puedo entender que le gustaría evitar eso.
Editar
Charles Bailey me llamó a esto, así que fui y busqué los detalles (en lugar de confiar en mi memoria holey). La especificación C dice
766 Un puntero a una función de un tipo se puede convertir en un puntero a una función de otro tipo y viceversa;
767 el resultado se comparará igual al puntero original.
768 Si se utiliza un puntero convertido para llamar a una función cuyo tipo no es compatible con el tipo apuntado, el comportamiento no está definido.
y las versiones previas de GCC 4.2 (esto se resolvió mucho antes de 4.3) siguieron estas reglas: el lanzamiento de un puntero a función no dio como resultado NULL, como escribí, pero tratando de llamar a una función a través de un tipo incompatible, es decir
func = (cb_t)nothing;
func(1);
de su ejemplo, resultaría en un abort
. Volvieron al comportamiento 4.1 (permitir pero advertir), en parte porque este cambio rompió OpenSSL, pero OpenSSL se ha corregido mientras tanto, y este es un comportamiento indefinido que el compilador puede cambiar en cualquier momento.
OpenSSL solo estaba emitiendo punteros de funciones a otros tipos de funciones que toman y devuelven el mismo número de valores de los mismos tamaños exactos, y esto (suponiendo que no se trate de punto flotante) pasa a ser seguro en todas las plataformas y convenciones de llamadas I saber de. Sin embargo, cualquier otra cosa es potencialmente inseguro.
Siempre que pueda garantizar que está realizando una llamada utilizando un método que haga que el que llama equilibre la pila en lugar de la llamada (__cdecl). Si no tiene una convención de llamadas especificada, la convención global podría establecerse en otra cosa. (__stdcall o __fastcall) Ambos podrían provocar daños en la pila.
Sospecho que obtendrás un comportamiento indefinido.
Puede asignar (con el molde apropiado) un puntero para funcionar a otro puntero para funcionar con una firma diferente, pero cuando lo llame pueden suceder cosas extrañas.
Su función nothing()
no toma argumentos, para el compilador esto puede significar que puede optimizar el uso de la pila, ya que no habrá argumentos allí. Pero aquí lo llamas con un argumento, esa es una situación inesperada y puede colapsar.
No puedo encontrar el punto correcto en el estándar, pero recuerdo que dice que puede lanzar indicadores de función, pero cuando llama a la función resultante, tiene que ver con el prototipo correcto, de lo contrario, el comportamiento no está definido.
Como nota al margen, no debe comparar un puntero de función con un puntero de datos (como NULL) ya que los punteros pueden pertenecer a espacios de direcciones separados. Hay un apéndice en el estándar C99 que permite este caso específico, pero no creo que esté ampliamente implementado. Dicho esto, en la arquitectura donde solo hay un espacio de direcciones fundiendo un puntero de función a un puntero de datos o comparándolo con NULL, generalmente funcionará.
Usted corre el riesgo de causar daños en la pila. Habiendo dicho eso, si declaras las funciones con un enlace extern "C"
(y / o __cdecl
dependiendo de tu compilador), puedes __cdecl
con la tuya. Sería similar a la forma en que una función como printf()
puede tomar una cantidad variable de argumentos a discreción de la persona que llama.
Si esto funciona o no en su situación actual también puede depender de las opciones exactas del compilador que esté utilizando. Si está utilizando MSVC, las opciones de compilación de depuración frente a versión pueden marcar una gran diferencia.