que - ¿Por qué los punteros a las funciones y los punteros de datos son incompatibles en C/C++?
punteros void lenguaje c (14)
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?
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 compiladores 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.
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 arquitecturas. Si bien algunas de estas arquitecturas 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 avoid
") 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 enfread
) o se quejan si el tipo de argumento no coincide exactamente (como enstrcmp
). 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.
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 programa que no se requiere que una implementación soporte".
Dependiendo de la arquitectura de destino, el código y los datos pueden almacenarse en áreas de memoria fundamentalmente incompatibles y físicamente distintas.
En la mayoría de las arquitecturas, 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 aumentar todo el uso de memoria solo en caso de que se necesite almacenar un puntero a la función.
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 dinámicos, o donde el integrador de usuario / sistema no desea utilizar enlaces dinámicos.
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.
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.
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.
Sé que esto no se ha comentado desde 2012, pero pensé que sería útil agregar que sí conozco una arquitectura que tiene punteros muy incompatibles para los datos y las funciones, ya que una llamada a esa arquitectura verifica el privilegio y transporta información adicional. Ninguna cantidad de fundición ayudará. Es el molino .
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) .
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 deDerived
, ya que ambos usan el mismo puntero. Pero un puntero a una función miembro deBase2
no se puede usar tal como es como un puntero a una función miembro deDerived
, ya quethis
puntero debe ajustarse.Hay muchas formas de resolver esto. Así es como el compilador 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 compilador, la versión, la arquitectura, etc.) almacenarse como
struct {
void * func;
size_t offset;
}
que obviamente es más grande que un void *
.
Una arquitectura no tiene que almacenar código y datos en la misma memoria. Con una arquitectura de Harvard, el código y los datos se almacenan en una memoria completamente diferente. La mayoría de las arquitecturas son arquitecturas de Von Neumann con código y datos en la misma memoria, pero C no se limita a ciertos tipos de arquitecturas si es posible.
indefinido no significa necesariamente no permitido, puede significar que el implementador del compilador tiene más libertad para hacerlo como lo desee.
Por ejemplo, es posible que no sea posible en algunas arquitecturas: undefined les permite tener una biblioteca conformada con ''C'' incluso si no puede hacerlo.