Llamar a métodos de clase C ++ a través de un puntero de función

113

¿Cómo obtengo un puntero de función para una función miembro de clase y luego llamo a esa función miembro con un objeto específico? Me gustaría escribir:

class Dog : Animal
{
    Dog ();
    void bark ();
}


Dog* pDog = new Dog ();
BarkFunction pBark = &Dog::bark;
(*pBark) (pDog);

Además, si es posible, también me gustaría invocar el constructor a través de un puntero:

NewAnimalFunction pNew = &Dog::Dog;
Animal* pAnimal = (*pNew)();    

¿Es esto posible y, de ser así, cuál es la forma preferida de hacerlo?

Tony el pony
fuente
1
Todavía no entiendo realmente 'por qué' si desea llamar a una función miembro de objetos, simplemente pase un puntero al objeto. Si la gente se queja porque te permite encapsular mejor la clase, ¿por qué no crear una clase de interfaz de la que todas las clases hereden?
Chad
Puede ser útil para implementar algo como el patrón de comando, aunque muchas personas usarían boost :: function para ocultar la mecánica del puntero del miembro sin formato.
CB Bailey
9
¿Por qué asignas a ese perro de forma dinámica? Luego, también debe eliminar manualmente el objeto. Esto se parece mucho a que vienes de Java, C # o algún otro lenguaje comparable y aún luchas con C ++. Dog dog;Es más probable que lo que desee sea un objeto automático simple ( ).
sbi
1
@Chad: Estoy de acuerdo en su mayoría, pero hay ocasiones en las que pasar una referencia sería más costoso. Considere un bucle que está iterando sobre algún tipo de datos (análisis, cálculo, etc.) que poder llamar a una función basada en algunos cálculos if / else impone un costo en el que simplemente llamar a la función apuntada también podría evitar tal if / then / else comprueba si estas comprobaciones se pueden realizar antes de entrar en el ciclo.
Eric
1
Consulte también el puntero de función a la función miembro .
jww

Respuestas:

127

Lea esto para obtener más detalles:

// 1 define a function pointer and initialize to NULL

int (TMyClass::*pt2ConstMember)(float, char, char) const = NULL;

// C++

class TMyClass
{
public:
   int DoIt(float a, char b, char c){ cout << "TMyClass::DoIt"<< endl; return a+b+c;};
   int DoMore(float a, char b, char c) const
         { cout << "TMyClass::DoMore" << endl; return a-b+c; };

   /* more of TMyClass */
};
pt2ConstMember = &TMyClass::DoIt; // note: <pt2Member> may also legally point to &DoMore

// Calling Function using Function Pointer

(*this.*pt2ConstMember)(12, 'a', 'b');
Satbir
fuente
24
Sorprende que hayan decidido que esto: *this.*pt2Memberfuncionaría. *tiene mayor precedencia sobre .*... Personalmente, todavía habría escrito this->*pt2Member, es un operador menos.
Alexis Wilke
7
¿Por qué tiene que inicializar pt2ConstMembera NULL?
Ciro Santilli 郝海东 冠状 病 六四 事件 法轮功
@AlexisWilke ¿por qué es sorprendente? Para objetos directos (no punteros) lo es (object.*method_pointer), por lo que queremos *que tenga mayor prioridad.
Ciro Santilli 郝海东 冠状 病 六四 事件 法轮功
@ TomášZato, si no me equivoco (y podría estarlo), thissolo se usa para demostrar que todo lo que aplique .*debe ser un puntero a una instancia de la (sub) clase. Sin embargo, esta es una nueva sintaxis para mí, solo supongo que basándome en otras respuestas y recursos vinculados aquí. Sugiero una edición para que quede más claro.
c1moore
1
¡Oh cielos, felicidades por 100!
Jonathan Mee
58

¿Cómo obtengo un puntero de función para una función miembro de clase y luego llamo a esa función miembro con un objeto específico?

Es más fácil comenzar con un typedef. Para una función miembro, agrega el nombre de clase en la declaración de tipo:

typedef void(Dog::*BarkFunction)(void);

Luego, para invocar el método, usa el ->*operador:

(pDog->*pBark)();

Además, si es posible, también me gustaría invocar el constructor a través de un puntero. ¿Es esto posible y, de ser así, cuál es la forma preferida de hacerlo?

No creo que puedas trabajar con constructores como este, los ctors y dtors son especiales. La forma normal de lograr ese tipo de cosas sería usando un método de fábrica, que es básicamente una función estática que llama al constructor por usted. Consulte el código a continuación para ver un ejemplo.

He modificado su código para hacer básicamente lo que describe. Hay algunas advertencias a continuación.

#include <iostream>

class Animal
{
public:

    typedef Animal*(*NewAnimalFunction)(void);

    virtual void makeNoise()
    {
        std::cout << "M00f!" << std::endl;
    }
};

class Dog : public Animal
{
public:

    typedef void(Dog::*BarkFunction)(void);

    typedef Dog*(*NewDogFunction)(void);

    Dog () {}

    static Dog* newDog()
    {
        return new Dog;
    }

    virtual void makeNoise ()
    {
        std::cout << "Woof!" << std::endl;
    }
};

int main(int argc, char* argv[])
{
    // Call member function via method pointer
    Dog* pDog = new Dog ();
    Dog::BarkFunction pBark = &Dog::makeNoise;

    (pDog->*pBark)();

    // Construct instance via factory method
    Dog::NewDogFunction pNew = &Dog::newDog;

    Animal* pAnimal = (*pNew)();

    pAnimal->makeNoise();

    return 0;
}

Ahora bien, aunque normalmente puede usar un Dog*en lugar de un Animal*gracias a la magia del polimorfismo, el tipo de puntero de función no sigue las reglas de búsqueda de la jerarquía de clases. Por lo tanto, un puntero de método Animal no es compatible con un puntero de método Dog, en otras palabras, no puede asignar un Dog* (*)()a una variable de tipo Animal* (*)().

El newDogmétodo estático es un ejemplo simple de una fábrica, que simplemente crea y devuelve nuevas instancias. Al ser una función estática, tiene una función regular typedef(sin calificador de clase).

Habiendo respondido lo anterior, me pregunto si no existe una mejor manera de lograr lo que necesita. Hay algunos escenarios específicos en los que haría este tipo de cosas, pero es posible que encuentre otros patrones que funcionan mejor para su problema. Si describe en términos más generales lo que está tratando de lograr, ¡la mente colmena puede resultar aún más útil!

En relación con lo anterior, sin duda encontrará muy útiles la biblioteca de enlaces Boost y otros módulos relacionados.

Gavinb
fuente
10
He usado C ++ durante más de 10 años y sigo aprendiendo algo nuevo de forma regular. Nunca había oído hablar de él ->*antes, pero ahora espero que nunca lo necesite :)
Thomas
31

No creo que nadie haya explicado aquí que un problema es que necesita " punteros a miembros " en lugar de punteros a funciones normales.

Los apuntadores de miembros a funciones no son simplemente apuntadores de funciones. En términos de implementación, el compilador no puede usar una dirección de función simple porque, en general, no conoce la dirección a la que llamar hasta que sepa qué objeto desreferenciar (piense en funciones virtuales). También necesita conocer el objeto para proporcionar el thisparámetro implícito, por supuesto.

Habiendo dicho que los necesita, ahora diré que realmente necesita evitarlos. En serio, los consejos para miembros son un fastidio. Es mucho más sensato mirar patrones de diseño orientados a objetos que logren el mismo objetivo, o usar uno boost::functiono lo que sea como se mencionó anteriormente, suponiendo que pueda tomar esa decisión, claro.

Si está proporcionando ese puntero de función a código existente, por lo que realmente necesita un puntero de función simple, debe escribir una función como miembro estático de la clase. Una función miembro estática no comprende this, por lo que deberá pasar el objeto como un parámetro explícito. Hubo una vez un modismo no tan inusual en este sentido para trabajar con código C antiguo que necesita punteros de función

class myclass
{
  public:
    virtual void myrealmethod () = 0;

    static void myfunction (myclass *p);
}

void myclass::myfunction (myclass *p)
{
  p->myrealmethod ();
}

Dado myfunctionque en realidad es solo una función normal (dejando de lado los problemas del alcance), se puede encontrar un puntero de función en la forma C normal.

EDITAR : este tipo de método se denomina "método de clase" o "función miembro estática". La principal diferencia con una función que no es miembro es que, si hace referencia a ella desde fuera de la clase, debe especificar el alcance utilizando el ::operador de resolución de alcance. Por ejemplo, para obtener el puntero de función, use &myclass::myfunctiony para llamarlo use myclass::myfunction (arg);.

Este tipo de cosas es bastante común cuando se utilizan las antiguas API de Win32, que fueron diseñadas originalmente para C en lugar de C ++. Por supuesto, en ese caso, el parámetro normalmente es LPARAM o similar en lugar de un puntero, y se necesita algo de conversión.

Steve314
fuente
'myfunction' no es una función normal si por normal te refieres a una función de estilo C. 'myfunction' se llama más exactamente un método de myclass. Los métodos de una clase no son como funciones normales en el sentido de que tienen algo que una función de estilo C no tiene, que es el puntero 'this'.
Eric
3
Aconsejar el uso de boost es draconiano. Hay buenas razones prácticas para usar punteros de método. No me importa la mención de boost como alternativa, pero odio cuando alguien dice que otra persona debería usarlo sin conocer todos los hechos. ¡El impulso tiene un costo! Y si se trata de una plataforma integrada, es posible que no sea una opción posible. Más allá de esto, me gusta mucho tu redacción.
Eric
@Eric - En tu segundo punto, no tenía la intención de decir "usarás Boost" y, de hecho, nunca usé Boost. La intención (que yo sepa después de 3 años) era que la gente buscara alternativas y enumerara algunas posibilidades. "O lo que sea" indica que una lista no pretende ser exhaustiva. Los punteros para miembros tienen un costo en legibilidad. Su representación de origen concisa también puede disfrazar los costos de tiempo de ejecución; en particular, un puntero de miembro a un método debe hacer frente a métodos virtuales y no virtuales, y debe saber cuál.
Steve 314
@Eric: no solo eso, sino que estos problemas son una razón para la no portabilidad con punteros de miembros: Visual C ++, al menos en el pasado, necesitaba algunas pistas adicionales sobre cómo representar los tipos de punteros de miembros. Usaría el enfoque de función estática para un sistema integrado: la representación de un puntero es la misma que la de cualquier otro puntero de función, los costos son obvios y no hay problemas de portabilidad. Y la llamada envuelta por la función miembro estática sabe (en tiempo de compilación) si la llamada es virtual o no; no se necesitan comprobaciones en tiempo de ejecución más allá de las búsquedas habituales de vtable para métodos virtuales.
Steve 314
@Eric: en su primer punto, soy consciente de que una función miembro estática no es exactamente lo mismo que una función de estilo C (por lo tanto, "aparte de los problemas de alcance"), pero probablemente debería haber incluido el nombre.
Steve 314
18
typedef void (Dog::*memfun)();
memfun doSomething = &Dog::bark;
....
(pDog->*doSomething)(); // if pDog is a pointer
// (pDog.*doSomething)(); // if pDog is a reference
AraK
fuente
2
Debería ser: (pDog -> * hacer algo) (); // si pDog es un puntero // (pDog. * hacerAlgo) (); // si pDog es una referencia as () operador tiene mayor prioridad entonces -> * y. *.
Tomek
12

Ejemplo ejecutable mínimo

main.cpp

#include <cassert>

class C {
    public:
        int i;
        C(int i) : i(i) {}
        int m(int j) { return this->i + j; }
};

int main() {
    // Get a method pointer.
    int (C::*p)(int) = &C::m;

    // Create a test object.
    C c(1);
    C *cp = &c;

    // Operator .*
    assert((c.*p)(2) == 3);

    // Operator ->*
    assert((cp->*p)(2) == 3);
}

Compilar y ejecutar:

g++ -ggdb3 -O0 -std=c++11 -Wall -Wextra -pedantic -o main.out main.cpp
./main.out

Probado en Ubuntu 18.04.

No puede cambiar el orden de los paréntesis ni omitirlos. Lo siguiente no funciona:

c.*p(2)
c.*(p)(2)

GCC 9.2 fallaría con:

main.cpp: In function int main()’:
main.cpp:19:18: error: must use ‘.*’ or ‘->*’ to call pointer-to-member function in p (...)’, e.g. ‘(... ->* p) (...)’
   19 |     assert(c.*p(2) == 3);
      |

Estándar C ++ 11

.*y ->*son operadores únicos introducidos en C ++ para este propósito, y no están presentes en C.

Borrador estándar de C ++ 11 N3337 :

  • 2.13 "Operadores y puntuadores" tiene una lista de todos los operadores, que contiene .*y ->*.
  • 5.5 "Operadores de puntero a miembro" explica lo que hacen
Ciro Santilli 郝海东 冠状 病 六四 事件 法轮功
fuente
11

Vine aquí para aprender cómo crear un puntero de función (no un puntero de método) a partir de un método, pero ninguna de las respuestas aquí proporciona una solución. Esto es lo que se me ocurrió:

template <class T> struct MethodHelper;
template <class C, class Ret, class... Args> struct MethodHelper<Ret (C::*)(Args...)> {
    using T = Ret (C::*)(Args...);
    template <T m> static Ret call(C* object, Args... args) {
        return (object->*m)(args...);
    }
};

#define METHOD_FP(m) MethodHelper<decltype(m)>::call<m>

Entonces, para su ejemplo, ahora haría:

Dog dog;
using BarkFunction = void (*)(Dog*);
BarkFunction bark = METHOD_FP(&Dog::bark);
(*bark)(&dog); // or simply bark(&dog)

Editar:
usando C ++ 17, hay una solución aún mejor:

template <auto m> struct MethodHelper;
template <class C, class Ret, class... Args, Ret (C::*m)(Args...)> struct MethodHelper<m> {
    static Ret call(C* object, Args... args) {
        return (object->*m)(args...);
    }
};

que se puede utilizar directamente sin la macro:

Dog dog;
using BarkFunction = void (*)(Dog*);
BarkFunction bark = MethodHelper<&Dog::bark>::call;
(*bark)(&dog); // or simply bark(&dog)

Para métodos con modificadores como, constes posible que necesite algunas especializaciones más como:

template <class C, class Ret, class... Args, Ret (C::*m)(Args...) const> struct MethodHelper<m> {
    static Ret call(const C* object, Args... args) {
        return (object->*m)(args...);
    }
};
pestaña
fuente
6

La razón por la que no puede usar punteros de función para llamar a funciones miembro es que los punteros de función ordinarios suelen ser solo la dirección de memoria de la función.

Para llamar a una función miembro, necesita saber dos cosas:

  • Que función miembro llamar
  • Qué instancia debe usarse (cuya función miembro)

Los punteros de función ordinaria no pueden almacenar ambos. Los punteros de función miembro de C ++ se utilizan para almacenar a), por lo que es necesario especificar la instancia explícitamente al llamar a un puntero de función miembro.

hrnt
fuente
1
He votado a favor de esto, pero agregaría un punto de aclaración en caso de que el OP no sepa a qué se refiere con "qué instancia". Me expandiría para explicar el puntero inherente 'esto'.
Eric
6

Un puntero de función a un miembro de clase es un problema que es realmente adecuado para usar boost :: function. Pequeño ejemplo:

#include <boost/function.hpp>
#include <iostream>

class Dog 
{
public:
   Dog (int i) : tmp(i) {}
   void bark ()
   {
      std::cout << "woof: " << tmp << std::endl;
   }
private:
   int tmp;
};



int main()
{
   Dog* pDog1 = new Dog (1);
   Dog* pDog2 = new Dog (2);

   //BarkFunction pBark = &Dog::bark;
   boost::function<void (Dog*)> f1 = &Dog::bark;

   f1(pDog1);
   f1(pDog2);
}
Benjamín
fuente
2

Para crear un nuevo objeto, puede usar la ubicación nueva, como se mencionó anteriormente, o hacer que su clase implemente un método clone () que cree una copia del objeto. Luego, puede llamar a este método de clonación utilizando un puntero de función miembro como se explicó anteriormente para crear nuevas instancias del objeto. La ventaja de la clonación es que a veces puede estar trabajando con un puntero a una clase base donde no conoce el tipo de objeto. En este caso, un método clone () puede ser más fácil de usar. Además, clone () le permitirá copiar el estado del objeto si eso es lo que desea.

Corwin Joy
fuente
los clones pueden ser costosos y el OP puede desear evitarlos si el rendimiento es un problema o preocupa.
Eric
0

Hice esto con std :: function y std :: bind ..

Escribí esta clase EventManager que almacena un vector de controladores en un mapa unordered_map que asigna tipos de eventos (que son solo const unsigned int, tengo una gran enumeración de espacios de nombres de ellos) a un vector de controladores para ese tipo de evento.

En mi clase EventManagerTests, configuré un controlador de eventos, como este:

auto delegate = std::bind(&EventManagerTests::OnKeyDown, this, std::placeholders::_1);
event_manager.AddEventListener(kEventKeyDown, delegate);

Aquí está la función AddEventListener:

std::vector<EventHandler>::iterator EventManager::AddEventListener(EventType _event_type, EventHandler _handler)
{
    if (listeners_.count(_event_type) == 0) 
    {
        listeners_.emplace(_event_type, new std::vector<EventHandler>());
    }
    std::vector<EventHandler>::iterator it = listeners_[_event_type]->end();
    listeners_[_event_type]->push_back(_handler);       
    return it;
}

Aquí está la definición de tipo EventHandler:

typedef std::function<void(Event *)> EventHandler;

Luego, de vuelta en EventManagerTests :: RaiseEvent, hago esto:

Engine::KeyDownEvent event(39);
event_manager.RaiseEvent(1, (Engine::Event*) & event);

Aquí está el código para EventManager :: RaiseEvent:

void EventManager::RaiseEvent(EventType _event_type, Event * _event)
{
    if (listeners_.count(_event_type) > 0)
    {
        std::vector<EventHandler> * vec = listeners_[_event_type];
        std::for_each(
            begin(*vec), 
            end(*vec), 
            [_event](EventHandler handler) mutable 
            {
                (handler)(_event);
            }
        );
    }
}

Esto funciona. Recibo la llamada en EventManagerTests :: OnKeyDown. Tengo que eliminar los vectores en tiempo de limpieza, pero una vez que lo hago no hay fugas. Generar un evento toma alrededor de 5 microsegundos en mi computadora, que es alrededor de 2008. No exactamente súper rápido, pero. Bastante justo siempre que sepa eso y no lo use en código ultra caliente.

Me gustaría acelerarlo rodando mi propia std :: function y std :: bind, y tal vez usando una matriz de matrices en lugar de un unordered_map de vectores, pero no he descubierto cómo almacenar una función miembro puntero y llamarlo desde un código que no sabe nada sobre la clase que se llama. La respuesta de Eyelash parece muy interesante.

Shavais
fuente