Estándar C++
Punteros a funciones miembro
¿Es diferente el tipo de «puntero a función miembro» que el de «puntero a función»?
Sí.
Considera la siguiente función:
int f(char a, float b);
El tipo de esta función es diferente dependiendo de si es una función ordinaria o una función nostatic
miembro de alguna clase:
Nota: si es una función static
miembro de class
Fred
, su tipo es el mismo que si fuera una función ordinaria:»int (*)(char,float)
«.
¿Cómo paso un puntero a función-miembro a un manejador de señales, callback de eventos X, llamada al sistema que inicia un hilo/tarea, etc?
No lo hagas.
Debido a que una función miembro no tiene sentido sin un objeto sobre el que invocarla, no puedes hacerlo directamente (si The X WindowSystem se reescribiera en C++, probablemente pasaría referencias a objetos, no sólo punteros a funciones; naturalmente los objetos encarnarían la función requerida y probablemente mucho más).
Como parche para el software existente, utilice una función de nivel superior (no miembro) como una envoltura que toma un objeto obtenido a través de alguna otra técnica. Dependiendo de la rutina que está llamando, esta «otra técnica» podría ser trivial o podría requerir un poco de trabajo de su parte. La llamada al sistema que inicia un hilo, por ejemplo, puede requerir que pases un puntero de función junto con un void*
, por lo que puedes pasar el puntero del objeto en el void*
. Muchos sistemas operativos en tiempo real hacen algo similar para la función que inicia una nueva tarea. En el peor de los casos, podría almacenar el puntero del objeto en una variable global; esto podría ser necesario para los manejadores de señales de Unix (pero las globales son, en general, indeseables). En cualquier caso, la función de nivel superior llamaría a la función miembro deseada en el objeto.
Aquí hay un ejemplo del peor caso (usando un global). Suponga que quiere llamar a Fred::memberFn()
en la interrupción:
Nota: las funciones miembro static
no requieren un objeto real para ser invocadas, por lo que los punteros astatic
-funciones miembro suelen ser compatibles con los punteros a funciones normales. Sin embargo, aunque probablemente funcione en la mayoría de los compiladores, en realidad tendría que ser una función extern "C"
no miembro para ser correcta, ya que la «vinculación con C» no sólo cubre cosas como la manipulación de nombres, sino también las convenciones de llamada, que podrían ser diferentes entre C y C++.
¿Por qué sigo recibiendo errores de compilación (desajuste de tipo) cuando intento utilizar una función miembro como una rutina de servicio de interrupción?
Este es un caso especial de las dos preguntas anteriores, por lo tanto, lea primero las dos respuestas anteriores.
Las funciones miembro nostatic
tienen un parámetro oculto que corresponde al puntero this
. El puntero this
apunta a los datos de instancia del objeto. El hardware/firmware de interrupción del sistema no es capaz de proporcionar el argumento del puntero this
. Debe utilizar funciones «normales» (no miembros de la clase) o funciones miembro static
como rutinas de servicio de interrupción.
Una posible solución es utilizar un miembro static
como rutina de servicio de interrupción y hacer que esa función busque en algún lugar el par instancia/miembro que debe ser llamado en la interrupción. Así, el efecto es que una función miembro es invocada en una interrupción, pero por razones técnicas es necesario llamar a una función intermedia en primer lugar.
¿Por qué estoy teniendo problemas para tomar la dirección de una función de C++?
Respuesta corta: si usted está tratando de almacenar en (o pasar como) un puntero a la función, entonces ese es el problema – esto es un corolario de la anterior FAQ.
Respuesta larga: En C++, las funciones miembro tienen un parámetro implícito que apunta al objeto (el puntero this
dentro de la función miembro). Las funciones normales de C pueden considerarse como una convención de llamada diferente a la de las funciones miembro, por lo que los tipos de sus punteros (puntero-a-función-miembro vs puntero-a-función) son diferentes e incompatibles. C++ introduce un nuevo tipo de puntero, llamado puntero-a-miembro, que sólo puede ser invocado proporcionando un objeto.
NOTA: no intente «fundir» un puntero-a-miembro-función en un puntero-a-función; el resultado es indefinido y probablemente desastroso. Por ejemplo, no se requiere que un puntero-a-función-miembro contenga la dirección de máquina de la función apropiada. Como se dijo en el último ejemplo, si tiene un puntero a una función regular de C, utilice una función de nivel superior (no miembro), o una función miembro static
(clase).
¿Cómo puedo evitar errores de sintaxis al crear punteros a miembros?
Utilice un typedef
.
Sí, claro, lo sé: usted es diferente. Eres inteligente. Puedes hacer estas cosas sin un typedef
. Suspiro. He recibido muchos correos electrónicos de personas que, como tú, se negaron a seguir el simple consejo de este FAQ. Perdieron horas y horas de su tiempo, cuando 10 segundos de typedef
s hubieran simplificado sus vidas. Además, asúmelo, no estás escribiendo código que sólo tú puedas leer; esperas escribir tu código para que otros también puedan leerlo, cuando estén cansados, cuando tengan sus propios plazos y sus propios retos. Así que, ¿por qué complicarse intencionadamente la vida a uno mismo y a los demás? Sé inteligente: utiliza un typedef
.
Aquí tienes una clase de ejemplo:
class Fred {public: int f(char x, float y); int g(char x, float y); int h(char x, float y); int i(char x, float y); // ...};
El typedef es trivial:
typedef int (Fred::*FredMemFn)(char x, float y); // Please do this!
¡Eso es! FredMemFn
es el nombre del tipo, y un puntero de ese tipo apunta a cualquier miembro de Fred
que tome(char,float)
, como f
, g
, h
y i
de Fred
.
Entonces es trivial declarar un puntero de función miembro:
int main(){ FredMemFn p = &Fred::f; // ...}
Y también es trivial declarar funciones que reciben punteros de función miembro:
void userCode(FredMemFn p){ /*...*/ }
Y también es trivial declarar funciones que devuelven punteros de función miembro:
FredMemFn userCode(){ /*...*/ }
Así que por favor, usa un typedef
. O eso o no me envíes un correo electrónico sobre los problemas que tienes con tus punteros a funciones miembro!
¿Cómo puedo evitar errores de sintaxis al llamar a una función miembro utilizando un puntero a función miembro?
Si tienes acceso a un compilador y a una biblioteca estándar que implementa las partes apropiadas del próximo estándar C++17, utiliza std::invoke
. De lo contrario, utilice una macro #define
.
Por favor.
Por favor.
Recibo demasiados correos electrónicos de personas confundidas que se negaron a tomar este consejo. Es muy sencillo. Lo sé, no necesitasstd::invoke
o una macro, y el experto con el que hablaste puede hacerlo sin ninguno de ellos, pero por favor no dejes que tu ego se interponga en lo que es importante: el dinero. Otros programadores tendrán que leer / mantener tu código. Sí, lo sé: eres más inteligente que los demás; bien. Y eres increíble; bien. Pero no añadas complejidad innecesaria a tu código.
Usar std::invoke
es trivial. Nota: FredMemFn
es un typedef
para un puntero a tipo de miembro:
Si no puedes usar std::invoke
, reduce el coste de mantenimiento, paradójicamente, usando una macro #define
en este caso particular.
(Normalmente no me gustan las macros #define
, pero deberías usarlas con los punteros tomembers porque mejoran la legibilidad y la escritura de ese tipo de código.)
La macro es trivial:
#define CALL_MEMBER_FN(object,ptrToMember) ((object).*(ptrToMember))
Usar la macro también es trivial. Nota: FredMemFn
es un typedef
para un puntero-a-tipo-miembro:
La razón por la que std::invoke
o esta macro es una buena idea es porque las invocaciones a funciones miembro son a menudo mucho más complejas que el ejemplo simple que se acaba de dar. La diferencia en legibilidad y escritura es significativa.comp.lang.c++
ha tenido que soportar cientos y cientos de envíos de programadores confusos que no conseguían acertar con la sintaxis. Casi todos estos errores habrían desaparecido si hubieran utilizado std::invoke
o la macro anterior.
Nota: Las macros #define
son malas en 4 sentidos diferentes: malvado#1, malvado#2, malvado#3 y malvado#4. Pero siguen siendo útiles a veces. Pero usted todavía debe sentir una vaga sensación de vergüenza después de usarlos.
¿Cómo puedo crear y utilizar una matriz de puntero-a-función-miembro?
Use tanto el typedef
y std::invoke
o la macro #define
descrito anteriormente, y usted es 90% hecho.
Paso 1: crea una macro typedef
:
Paso 2: crea una macro #define
si no tienes std::invoke
:
#define CALL_MEMBER_FN(object,ptrToMember) ((object).*(ptrToMember))
Ahora tu matriz de punteros-a-funciones-miembro es sencilla:
FredMemFn a = { &Fred::f, &Fred::g, &Fred::h, &Fred::i };
Y el uso de uno de los punteros a funciones miembro también es sencillo:
void userCode(Fred& fred, int memFnNum){ // Assume memFnNum is between 0 and 3 inclusive: std::invoke(a, fred, 'x', 3.14);}
o si no tienes std::invoke
,
void userCode(Fred& fred, int memFnNum){ // Assume memFnNum is between 0 and 3 inclusive: CALL_MEMBER_FN(fred, a) ('x', 3.14);}
Nota: las macros #define
son malvadas en 4 sentidos diferentes: mal#1, mal#2, mal#3 y mal#4. Pero siguen siendo útiles a veces. Siéntase avergonzado, siéntase culpable, pero cuando una construcción malvada como una macro mejora su software, úsela.
¿Cómo declaro un puntero a función-miembro que apunta a una función miembro const?
Respuesta corta: añada un const
a la derecha del )
cuando use un typedef
para declarar el puntero a función-miembro.
Por ejemplo, supongamos que quieres un puntero a función-miembro que apunte a Fred::f
, Fred::g
o Fred::h
:
class Fred {public: int f(int i) const; int g(int i) const; int h(int j) const; // ...};
Entonces, cuando utilices un typedef
para declarar el tipo de puntero a función-miembro, debería ser así:
¡Eso es!
Entonces puede declarar/pasar/devolver punteros a funciones miembro como es normal:
¿Cuál es la diferencia entre los operadores .* y ->*?
No necesitará entender esto si utiliza std::invoke
o una macro para las llamadas a punteros a funciones miembro. Ohyea, por favor usa std::invoke
o una macro en este caso. ¿Y mencioné que deberías usar std::invoke
o una macro en este caso?
Por ejemplo:
Pero por favor considera usar un std::invoke
o una macro en su lugar:
void sample(Fred x, Fred& y, Fred* z, FredMemFn func){ std::invoke(func, x, 42, 3.14); std::invoke(func, y, 42, 3.14); std::invoke(func, *z, 42, 3.14);}
o
Como se discutió antes, las invocaciones en el mundo real son a menudo mucho más complicadas que las simples aquí, así que usar un std::invoke
o una macro típicamente mejorará la escritura y legibilidad de tu código.
¿Puedo convertir un puntero a función miembro en un void*?
¡No!
Detalles técnicos: los punteros a funciones miembro y los punteros a datos no se representan necesariamente de la misma manera. Un puntero a una función miembro puede ser una estructura de datos en lugar de un puntero simple. Piénsalo: si está apuntando a una función virtual, puede que no esté apuntando a un montón de código que se pueda resolver estáticamente, por lo que puede que ni siquiera sea una dirección normal – puede que sea una estructura de datos diferente de algún tipo.
Por favor, no me envíes un correo electrónico si lo anterior parece funcionar en tu versión particular de tu compilador particular en tu sistema operativo particular. No me importa. Es ilegal, punto.
¿Puedo convertir un puntero a función en un void*?
¡No!
Detalles técnicos: void*
Los punteros son punteros a datos, y los punteros a funciones apuntan a funciones. El lenguaje no requiere que las funciones y los datos estén en el mismo espacio de direcciones, así que, a modo de ejemplo y no de limitación, en las arquitecturas que los tienen en espacios de direcciones diferentes, los dos tipos de punteros diferentes no serán comparables.
Por favor, no me envíes un correo electrónico si lo anterior parece funcionar en tu versión particular de tu compilador particular en tu sistema operativo particular. No me importa. Es ilegal, y punto.
Necesito algo como los punteros de función, pero con más flexibilidad y/o seguridad de hilos; ¿hay otra manera?
Usa un functionoide.
¿Qué diablos es un functionoide, y por qué debería usar uno?
Los functionoides son funciones con esteroides. Los functionoides son estrictamente más poderosos que las funciones, y ese poder extra resuelve algunos (no todos) de los desafíos que típicamente se enfrentan cuando se usan punteros-función.
Trabajemos con un ejemplo que muestre un uso tradicional de los punteros-función, luego traduciremos ese ejemplo a los functionoides. La idea tradicional de los punteros a funciones es tener un montón de funciones compatibles:
int funct1( /*...params...*/ ) { /*...code...*/ }int funct2( /*...params...*/ ) { /*...code...*/ }int funct3( /*...params...*/ ) { /*...code...*/ }
Entonces se accede a ellas mediante punteros a funciones:
typedef int(*FunctPtr)( /*...params...*/ );void myCode(FunctPtr f){ // ... f( /*...args-go-here...*/ ); // ...}
A veces la gente crea un array de estos punteros a funciones:
FunctPtr array;array = funct1;array = funct1;array = funct3;array = funct2;// ...
En cuyo caso llaman a la función accediendo al array:
array( /*...args-go-here...*/ );
Con los functionoides, primero se crea una clase base con un método puramente virtual:
Después, en lugar de tres funciones, se crean tres clases derivadas:
Después, en lugar de pasar un puntero de función, se pasa un Funct*
. Crearé un typedef
llamado FunctPtr
simplemente para que el resto del código sea similar al enfoque antiguo:
typedef Funct* FunctPtr;void myCode(FunctPtr f){ // ... f->doit( /*...args-go-here...*/ ); // ...}
Puedes crear un array de ellos casi de la misma manera:
Esto nos da la primera pista sobre dónde los functionoides son estrictamente más poderosos que los function-pointers: el hecho de que el enfoque de los functionoides tiene argumentos que puedes pasar a los ctors (mostrados arriba como …ctor-args…) mientras que la versión de los function-pointers no. Piensa en un objeto functionoide como una llamada a una función congelada (énfasis en la palabra llamada). A diferencia de un puntero a una función, un functionoide es (conceptualmente) un puntero a una función parcialmente llamada. Imagina por un momento una tecnología que te permite pasar algunos-pero-no-todos los argumentos a una función, y luego te permite congelar esa llamada (parcialmente completada). Imagina que esa tecnología te devuelve una especie de puntero mágico a esa llamada a la función parcialmente completada y congelada. Entonces, más tarde, pasas los argumentos restantes utilizando ese puntero, y el sistema toma mágicamente tus argumentos originales (que fueron congelados), los combina con cualquier variable local que la función calculó antes de ser congelada, combina todo eso con los argumentos recién pasados, y continúa la ejecución de la función donde la dejó cuando fue congelada. Esto puede sonar a ciencia ficción, pero es lo que te permiten hacer los functionoids. Además, te permiten «completar» repetidamente esa función liofilizada con diferentes «parámetros restantes», tantas veces como quieras. Además, te permiten (no requieren) cambiar el estado de la función congelada cuando se llama, lo que significa que los functionoides pueden recordar la información de una llamada a la siguiente.
Volvamos a poner los pies en el suelo y vamos a trabajar un par de ejemplos para explicar lo que todo ese galimatías realmente significa.
Supongamos que las funciones originales (en el viejo estilo de función-puntero) tomaron parámetros ligeramente diferentes.
int funct1(int x, float y){ /*...code...*/ }int funct2(int x, const std::string& y, int z){ /*...code...*/ }int funct3(int x, const std::vector<double>& y){ /*...code...*/ }
Cuando los parámetros son diferentes, el enfoque anticuado de función-puntero es difícil de usar, ya que el llamador no sabe qué parámetros pasar (el llamador sólo tiene un puntero a la función, no el nombre de la función o, cuando los parámetros son diferentes, el número y los tipos de sus parámetros) (no me escriba un correo electrónico acerca de esto; sí se puede hacer, pero usted tiene que estar de pie en la cabeza y hacer las cosas desordenadas; pero no me escriba acerca de esto – usefunctionoids en su lugar).
Con los functionoides, la situación es, al menos a veces, mucho mejor. Dado que un functionoide puede ser considerado como una llamada a una función congelada, sólo hay que tomar los argumentos no comunes, como los que he llamado y
y/o z
, y convertirlos en cargas para los ctores correspondientes. También puedes pasar los args comunes (en este caso el int
llamado x
) al ctor, pero no tienes que hacerlo – tienes la opción de pasarlos al método virtual puro doit()
en su lugar. Asumiré que quieres pasar x
a doit()
y y
y/o z
a los ctores:
class Funct {public: virtual int doit(int x) = 0;};
Entonces, en lugar de tres funciones, creas tres clases derivadas:
Ahora ves que los parámetros del ctor se congelan en el functionoide cuando creas el array de functionoides:
Así que cuando el usuario invoca el doit()
en uno de estos functionoides, suministra los args «restantes», y la llamada combina conceptualmente los args originales pasados al ctor con los pasados al método doit()
:
array->doit(12);
Como ya he insinuado, uno de los beneficios de los functionoides es que puedes tener varias instancias de, digamos, Funct1
en tu array, y esas instancias pueden tener diferentes parámetros congelados en ellas. Por ejemplo, array
yarray
son ambos de tipo Funct1
, pero el comportamiento de array->doit(12)
será diferente del comportamiento dearray->doit(12)
ya que el comportamiento dependerá tanto del 12 que se pasó a doit()
como de los argumentos pasados a los ctores.
Otra ventaja de los functionoides es evidente si cambiamos el ejemplo de un array de functionoides a un localfunctionoid. Para preparar el escenario, volvamos a la antigua aproximación de puntero-función, e imaginemos que estás intentando pasar una función de comparación a una rutina sort()
o binarySearch()
. La rutina sort()
o binarySearch()
se llama childRoutine()
y el tipo de puntero-función de comparación se llama FunctPtr
:
void childRoutine(FunctPtr f){ // ... f( /*...args...*/ ); // ...}
Entonces, los diferentes llamadores pasarían diferentes punteros-función dependiendo de lo que consideraran mejor:
void myCaller(){ // ... childRoutine(funct1); // ...}void yourCaller(){ // ... childRoutine(funct3); // ...}
Podemos traducir fácilmente este ejemplo en uno que utilice functionoids:
Dado este ejemplo como telón de fondo, podemos ver dos beneficios de los functionoids sobre los function-pointers. El beneficio del «ctor args» descrito anteriormente, más el hecho de que los functionoides pueden mantener el estado entre las llamadas de una manera segura para los hilos.Con los punteros de función simples, la gente normalmente mantiene el estado entre las llamadas a través de datos estáticos. Sin embargo, los datos estáticos no son intrínsecamente seguros para los hilos: los datos estáticos se comparten entre todos los hilos. El enfoque de functionoid proporciona algo que es intrínsecamente seguro para los hilos, ya que el código termina con datos locales para los hilos. La implementación es trivial: cambiar el antiguo dato estático por un miembro de datos de instancia dentro del objeto this
del functionoide y, de repente, los datos no sólo son locales para los hilos, sino que incluso son seguros con las llamadas recursivas: cada llamada a yourCaller()
tendrá su propio objeto Funct3
distinto con sus propios datos de instancia distintos.
Nota que hemos ganado algo sin perder nada. Si quieres datos globales del hilo, los functionoides también pueden darlos: sólo tienes que cambiarlos de un miembro de datos de instancia dentro del objeto this
del functionoide a un miembro de datos estáticos dentro de la clase del functionoide, o incluso a un dato estático de ámbito local. No estarías mejor que con los punteros de función, pero tampoco estarías peor.
El enfoque del functionoide te da una tercera opción que no está disponible con el enfoque antiguo: el functionoide permite a los llamantes decidir si quieren datos locales o globales del hilo. Serían responsables de usar bloqueos en los casos en los que quisieran datos globales del hilo, pero al menos tendrían la opción. Es fácil:
Los functionoids no resuelven todos los problemas que se encuentran al hacer software flexible, pero son estrictamente más poderosos que los punteros de función y vale la pena al menos evaluarlos. De hecho, puedes probar fácilmente que los functionoides no pierden ningún poder sobre los punteros de función, ya que puedes imaginar que el enfoque anticuado de los punteros de función es equivalente a tener un objeto functionoide global. Como siempre se puede hacer un objeto functionoide global, no se ha perdido nada. QED.
¿Se pueden hacer functionoides más rápidos que las llamadas a funciones normales?
Sí.
Si tienes un functionoide pequeño, y en el mundo real eso es bastante común, el coste de la llamada a la función puede ser altocomparado con el coste del trabajo realizado por el functionoide. En la FAQ anterior, los functionoides se implementaban utilizando funciones virtuales y normalmente le costarán una llamada a la función. Un enfoque alternativo utiliza plantillas.
El siguiente ejemplo es similar en espíritu al de la FAQ anterior. He cambiado el nombre de doit()
aoperator()()
para mejorar la legibilidad del código de la llamada y para permitir que alguien pase un puntero de función regular:
La diferencia entre este enfoque y el de la FAQ anterior es que el fuctionoide se «vincula» a la llamada en tiempo de compilación en lugar de en tiempo de ejecución. Piensa en ello como si pasaras un parámetro: si sabes en tiempo de compilación el tipo de functionoide que quieres pasar en última instancia, entonces puedes usar la técnica anterior, y puedes, al menos en los casos típicos, obtener un beneficio de velocidad al tener el compiladorinline-expandir el código del functionoide dentro del llamador. Aquí hay un ejemplo:
template <typename FunctObj>void myCode(FunctObj f){ // ... f( /*...args-go-here...*/ ); // ...}
Cuando el compilador compila lo anterior, podría inline-expandir la llamada que podría mejorar el rendimiento.
Aquí tienes una forma de llamar a lo anterior:
void blah(){ // ... Funct2 x("functionoids are powerful", 42); myCode(x); // ...}
Además: como se insinuó en el primer párrafo anterior, también puedes pasar los nombres de las funciones normales (aunque podrías incurrir en el coste de la llamada a la función cuando la persona que llama la usa):
void myNormalFunction(int x);void blah(){ // ... myCode(myNormalFunction); // ...}
¿Cuál es la diferencia entre un functionoide y un functor?
Un functionoide es un objeto que tiene un método principal. Es básicamente la extensión OO de una función tipo C comoprintf(). Uno utilizaría un functionoide siempre que la función tenga más de un punto de entrada (es decir, más de un «método») y/o necesite mantener el estado entre las llamadas de una manera segura para los hilos (el enfoque de estilo C para mantener el estado entre las llamadas es añadir una variable local «estática» a la función, pero eso es terriblemente inseguro en un entorno multihilo).
Un functor es un caso especial de un functionoide: es un functionoide cuyo método es el «operador de llamada a función», operator()(). Dado que sobrecarga el operador de llamada a función, el código puede llamar a su método principal utilizando la misma sintaxis que para una llamada a función. Por ejemplo, si «foo» es un functor, para llamar al método «operator()()» en el objeto «foo» se diría «foo()». La ventaja de esto es en las plantillas, ya que entonces la plantilla puede tener un parámetro de plantilla que se utilizará como una función, y este parámetro puede ser el nombre de una función o un objeto functor. Hay una ventaja de rendimiento al ser un objeto functor ya que el método «operator()()» puede ser inlineado (mientras que si se pasa la dirección de una función debe, necesariamente, ser no inlineada).
Esto es muy útil para cosas como la función «comparación» en contenedores ordenados. En C, la función de comparación se pasa siempre por puntero (por ejemplo, ver la firma de «qsort()»), pero en C++ el parámetro puede venir como un puntero a la función O como el nombre de un objeto-functor, y el resultado es que los contenedores ordenados en C++ pueden ser, en algunos casos, mucho más rápidos (y nunca más lentos) que el equivalente en C.
Dado que Java no tiene nada similar a las plantillas, debe utilizar la vinculación dinámica para todas estas cosas, y la vinculación dinámica ofnecessity significa una llamada a la función. Normalmente no es un gran problema, pero en C++ queremos permitir un código de muy alto rendimiento. Es decir, C++ tiene una filosofía de «paga por ello sólo si lo usas», lo que significa que el lenguaje nunca debe imponer arbitrariamente ninguna sobrecarga por encima de lo que la máquina física es capaz de realizar (por supuesto, un programador puede, opcionalmente, usar técnicas como la vinculación dinámica que, en general, impondrá alguna sobrecarga a cambio de flexibilidad o alguna otra «habilidad», pero depende del diseñador y del programador decidir si quieren los beneficios (y los costes) de tales construcciones).
Leave a Reply