¿Por qué los punteros a las funciones y los punteros de datos son incompatibles en C / C ++?

He leído que la conversión de un puntero de función a un puntero de datos y viceversa funciona en la mayoría de las plataformas, pero no se garantiza que funcione. ¿Por qué es este el caso? ¿No deberían ambas ser simplemente direcciones en la memoria principal y, por lo tanto, ser compatibles?

Una architecture no tiene que almacenar código y datos en la misma memoria. Con una architecture de Harvard, el código y los datos se almacenan en una memoria completamente diferente. La mayoría de las architectures son architectures de Von Neumann con código y datos en la misma memoria, pero C no se limita a ciertos tipos de architectures si es posible.

Algunas computadoras tienen (tenían) espacios de direcciones separados para el código y los datos. En dicho hardware, simplemente no funciona.

El lenguaje está diseñado no solo para las aplicaciones de escritorio actuales, sino para permitir su implementación en un gran conjunto de hardware.


Parece que el comité de lenguaje C nunca tuvo la intención de ser void* para ser un puntero a la función, solo querían un puntero genérico para los objetos.

El C99 Rationale dice:

6.3.2.3 Punteros
C ahora se ha implementado en una amplia gama de architectures. Si bien algunas de estas architectures tienen punteros uniformes que tienen el tamaño de algún tipo entero, el código máximo portátil no puede asumir ninguna correspondencia necesaria entre los diferentes tipos de punteros y los tipos enteros. En algunas implementaciones, los punteros pueden ser incluso más amplios que cualquier tipo de entero.

El uso de void* (“puntero a void “) como un tipo de puntero de objeto genérico es una invención del Comité C89. La adopción de este tipo fue estimulada por el deseo de especificar argumentos de función prototipo que silenciosamente convierten punteros arbitrarios (como en fread ) o se quejan si el tipo de argumento no coincide exactamente (como en strcmp ). No se dice nada sobre los punteros a las funciones, que pueden ser inconmensurables con los punteros y / o enteros del objeto.

Nota: no se dice nada sobre los punteros a las funciones en el último párrafo. Pueden ser diferentes de otros indicadores, y el comité es consciente de eso.

Para aquellos que recuerdan MS-DOS, Windows 3.1 y versiones anteriores, la respuesta es bastante fácil. Todos estos utilizados para admitir varios modelos de memoria diferentes, con diversas combinaciones de características para el código y punteros de datos.

Entonces, por ejemplo, para el modelo compacto (código pequeño, datos grandes):

 sizeof(void *) > sizeof(void(*)()) 

y por el contrario en el modelo Medio (código grande, datos pequeños):

 sizeof(void *) < sizeof(void(*)()) 

En este caso, no tenía almacenamiento por separado para el código y la fecha, pero aún no podía convertir los dos punteros (excepto el uso de modificadores __near y __far no estándar).

Además, no hay garantía de que, incluso si los punteros son del mismo tamaño, apuntan a lo mismo: en el modelo de memoria pequeña de DOS, tanto el código como los datos utilizados cerca de los punteros, pero apuntan a segmentos diferentes. Por lo tanto, convertir un puntero a un puntero de datos no le daría un puntero que tuviera alguna relación con la función y, por lo tanto, no se usaría esa conversión.

Se supone que los punteros al vacío son capaces de acomodar un puntero a cualquier tipo de datos, pero no necesariamente un puntero a una función. Algunos sistemas tienen diferentes requisitos para los punteros a las funciones que los punteros a los datos (por ejemplo, hay DSP con direccionamiento diferente para datos vs. código, modelo mediano en MS-DOS utiliza punteros de 32 bits para el código pero solo punteros de 16 bits para los datos) .

Además de lo que ya se ha dicho aquí, es interesante observar POSIX dlsym() :

El estándar ISO C no requiere que los punteros a las funciones se puedan enviar hacia adelante y hacia atrás a los punteros a los datos. De hecho, el estándar ISO C no requiere que un objeto de tipo void * pueda contener un puntero a una función. Sin embargo, las implementaciones que admiten la extensión XSI requieren que un objeto de tipo void * pueda contener un puntero a una función. El resultado de convertir un puntero a una función en un puntero a otro tipo de datos (excepto void *) aún no está definido. Tenga en cuenta que los comstackdores que cumplen con el estándar ISO C son necesarios para generar una advertencia si se intenta una conversión desde un puntero void * a un puntero a función como en:

  fptr = (int (*)(int))dlsym(handle, "my_function"); 

Debido al problema que se menciona aquí, una versión futura puede agregar una función nueva para devolver punteros a funciones, o la interfaz actual puede dejar de utilizarse en favor de dos nuevas funciones: una que devuelve punteros de datos y la otra que devuelve punteros a funciones.

C ++ 11 tiene una solución a la antigua falta de coincidencia entre C / C ++ y POSIX con respecto a dlsym() . Se puede usar reinterpret_cast para convertir un puntero de función a / desde un puntero de datos siempre que la implementación sea compatible con esta característica.

De la norma, 5.2.10 párr. 8, “la conversión de un puntero de función a un tipo de puntero de objeto o viceversa es condicionalmente compatible”. 1.3.5 define “condicionalmente respaldado” como una “construcción de progtwig que no se requiere que una implementación soporte”.

Dependiendo de la architecture de destino, el código y los datos pueden almacenarse en áreas de memoria fundamentalmente incompatibles y físicamente distintas.

indefinido no significa necesariamente no permitido, puede significar que el implementador del comstackdor tiene más libertad para hacerlo como lo desee.

Por ejemplo, es posible que no sea posible en algunas architectures: undefined les permite tener una biblioteca conformada con ‘C’ incluso si no puede hacerlo.

Pueden ser diferentes tipos con diferentes requisitos de espacio. Asignar a uno puede cortar de forma irreversible el valor del puntero para que la asignación de resultados en algo diferente.

Creo que pueden ser de tipos diferentes porque el estándar no quiere limitar las posibles implementaciones que ahorran espacio cuando no es necesario o cuando el tamaño puede hacer que la CPU tenga que hacer más cosas para usarlo, etc.

Otra solución:

Suponiendo que POSIX garantiza que los punteros de función y datos tienen el mismo tamaño y representación (no puedo encontrar el texto para esto, pero el ejemplo OP citado sugiere que al menos tenían la intención de cumplir este requisito), lo siguiente debería funcionar:

 double (*cosine)(double); void *tmp; handle = dlopen("libm.so", RTLD_LAZY); tmp = dlsym(handle, "cos"); memcpy(&cosine, &tmp, sizeof cosine); 

Esto evita violar las reglas de alias pasando por la representación char [] , que permite alias de todos los tipos.

Sin embargo, otro enfoque:

 union { double (*fptr)(double); void *dptr; } u; u.dptr = dlsym(handle, "cos"); cosine = u.fptr; 

Pero recomendaría el enfoque memcpy si quieres absolutamente el 100% de C.

La única solución verdaderamente portátil es no utilizar dlsym para funciones, y en su lugar usar dlsym para obtener un puntero a datos que contengan punteros a funciones. Por ejemplo, en tu biblioteca:

 struct module foo_module = { .create = create_func, .destroy = destroy_func, .write = write_func, /* ... */ }; 

y luego en tu aplicación:

 struct module *foo = dlsym(handle, "foo_module"); foo->create(/*...*/); /* ... */ 

Dicho sea de paso, esta es una buena práctica de diseño, y facilita el soporte tanto de la carga dinámica mediante dlopen como de la vinculación estática de todos los módulos en sistemas que no admiten enlaces dynamics, o donde el integrador de usuario / sistema no desea utilizar enlaces dynamics.

En la mayoría de las architectures, los punteros a todos los tipos de datos normales tienen la misma representación, por lo que la conversión entre los tipos de puntero de datos no es operativa.

Sin embargo, es concebible que los punteros de función requieran una representación diferente, tal vez sean más grandes que otros punteros. Si void * podría contener punteros de función, esto significaría que la representación de void * tendría que ser del tamaño más grande. Y todos los lanzamientos de punteros a / desde void * tendrían que realizar esta copia adicional.

Como alguien mencionó, si necesita esto puede lograrlo usando una unión. Pero la mayoría de los usos de void * son solo para datos, por lo que sería oneroso boost todo el uso de memoria solo en caso de que se necesite almacenar un puntero a la función.

Un ejemplo moderno en el que los punteros a funciones pueden diferir en tamaño de los punteros de datos: punteros de función de miembro de clase C ++

Citado directamente de https://blogs.msdn.microsoft.com/oldnewthing/20040209-00/?p=40713/

 class Base1 { int b1; void Base1Method(); }; class Base2 { int b2; void Base2Method(); }; class Derived : public Base1, Base2 { int d; void DerivedMethod(); }; 

Ahora hay dos posibles this indicadores.

Un puntero a una función miembro de Base1 se puede usar como un puntero a una función miembro de Derived , ya que ambos usan el mismo puntero. Pero un puntero a una función miembro de Base2 no se puede usar tal como es como un puntero a una función miembro de Derived , ya que this puntero debe ajustarse.

Hay muchas formas de resolver esto. Así es como el comstackdor de Visual Studio decide manejarlo:

Un puntero a una función miembro de una clase heredada de múltiples es realmente una estructura.

 [Address of function] [Adjustor] 

El tamaño de una función de puntero a miembro de una clase que usa herencia múltiple es el tamaño de un puntero más el tamaño de un size_t .

tl; dr: cuando se usa herencia múltiple, un puntero a una función miembro puede (en realidad, según el comstackdor, la versión, la architecture, etc.) almacenarse como

 struct { void * func; size_t offset; } 

que obviamente es más grande que un void * .

Sé que esto no se ha comentado desde 2012, pero pensé que sería útil agregar que conozco una architecture que tiene punteros muy incompatibles para los datos y las funciones, ya que una llamada a esa architecture verifica el privilegio y transporta información adicional. Ninguna cantidad de fundición ayudará. Es el molino .