¿Cómo puedo pasar una función miembro de una clase como devolución de llamada?

Resuelto ofer asked hace 15 años • 14 respuestas

Estoy usando una API que requiere que pase un puntero de función como devolución de llamada. Estoy intentando utilizar esta API de mi clase pero obtengo errores de compilación.

Esto es lo que hice desde mi constructor:

m_cRedundencyManager->Init(this->RedundencyManagerCallBack);

Esto no se compila; aparece el siguiente error:

Error 8 error C3867: 'CLoggersInfra::RedundencyManagerCallBack': falta lista de argumentos de llamada de función; use '&CLoggersInfra::RedundencyManagerCallBack' para crear un puntero al miembro

Probé la sugerencia de uso &CLoggersInfra::RedundencyManagerCallBack, pero no funcionó para mí.

¿Alguna sugerencia/explicación para esto?

Estoy usando VS2008.

¡¡Gracias!!

ofer avatar Dec 30 '08 20:12 ofer
Aceptado

Ésta es una pregunta sencilla pero la respuesta es sorprendentemente compleja. La respuesta corta es que puedes hacer lo que estás intentando hacer con std::bind1sto boost::bind. La respuesta más larga está a continuación.

El compilador tiene razón al sugerirle que utilice &CLoggersInfra::RedundencyManagerCallBack. Primero, si RedundencyManagerCallBackes una función miembro, la función en sí no pertenece a ninguna instancia particular de la clase CLoggersInfra. Pertenece a la clase misma. Si alguna vez has llamado a una función de clase estática, habrás notado que usas la misma SomeClass::SomeMemberFunctionsintaxis. Dado que la función en sí es "estática" en el sentido de que pertenece a la clase y no a una instancia particular, se utiliza la misma sintaxis. El '&' es necesario porque técnicamente hablando no se pasan funciones directamente: las funciones no son objetos reales en C++. En lugar de eso, técnicamente estás pasando la dirección de memoria de la función, es decir, un puntero al lugar donde comienzan las instrucciones de la función en la memoria. Sin embargo, la consecuencia es la misma: efectivamente estás "pasando una función" como parámetro.

Pero en este caso eso es sólo la mitad del problema. Como dije, RedundencyManagerCallBackla función no "pertenece" a ninguna instancia en particular. Pero parece que quieres pasarlo como una devolución de llamada con una instancia particular en mente. Para comprender cómo hacer esto, debe comprender qué son realmente las funciones miembro: funciones regulares no definidas en ninguna clase con un parámetro oculto adicional.

Por ejemplo:

class A {
public:
    A() : data(0) {}
    void foo(int addToData) { this->data += addToData; }

    int data;
};

...

A an_a_object;
an_a_object.foo(5);
A::foo(&an_a_object, 5); // This is the same as the line above!
std::cout << an_a_object.data; // Prints 10!

¿ Cuántos parámetros se A::foonecesitan? Normalmente diríamos 1. Pero en el fondo, foo realmente requiere 2. Mirando la definición de A::foo, necesita una instancia específica de A para que el puntero 'this' tenga significado (el compilador necesita saber qué ' esto es). La forma en que normalmente especificas lo que quieres que sea "esto" es a través de la sintaxis MyObject.MyMemberFunction(). Pero esto es solo azúcar sintáctico para pasar la dirección de MyObjectcomo primer parámetro a MyMemberFunction. De manera similar, cuando declaramos funciones miembro dentro de definiciones de clases, no ponemos 'esto' en la lista de parámetros, pero esto es solo un regalo de los diseñadores del lenguaje para ahorrarnos escribir. En su lugar, debe especificar que una función miembro es estática para optar por no recibirla y obtener automáticamente el parámetro adicional 'este'. Si el compilador de C++ tradujera el ejemplo anterior a código C (el compilador de C++ original en realidad funcionaba de esa manera), probablemente escribiría algo como esto:

struct A {
    int data;
};

void a_init(A* to_init)
{
    to_init->data = 0;
}

void a_foo(A* this, int addToData)
{ 
    this->data += addToData;
}

...

A an_a_object;
a_init(0); // Before constructor call was implicit
a_foo(&an_a_object, 5); // Used to be an_a_object.foo(5);

Volviendo a su ejemplo, ahora hay un problema obvio. 'Init' quiere un puntero a una función que toma un parámetro. Pero &CLoggersInfra::RedundencyManagerCallBackes un puntero a una función que toma dos parámetros, su parámetro normal y el parámetro secreto "este". Es por eso que todavía recibe un error del compilador (como nota al margen: si alguna vez ha usado Python, este tipo de confusión es la razón por la cual se requiere un parámetro 'self' para todas las funciones miembro).

La forma detallada de manejar esto es crear un objeto especial que contenga un puntero a la instancia que desea y tenga una función miembro llamada algo así como 'ejecutar' o 'ejecutar' (o sobrecargue el operador '()') que toma los parámetros. para la función miembro, y simplemente llama a la función miembro con esos parámetros en la instancia almacenada. Pero esto requeriría que cambies 'Init' para tomar tu objeto especial en lugar de un puntero de función sin formato, y parece que Init es el código de otra persona. Y crear una clase especial para cada vez que surja este problema provocará un exceso de código.

Ahora, finalmente, la buena solución boost::bindy boost::functionla documentación para cada una se pueden encontrar aquí:

impulso::enlazar documentos , impulso::documentos de función

boost::bindle permitirá tomar una función y un parámetro para esa función, y crear una nueva función donde ese parámetro esté "bloqueado" en su lugar. Entonces, si tengo una función que suma dos números enteros, puedo usarla boost::bindpara crear una nueva función donde uno de los parámetros esté bloqueado para decir 5. Esta nueva función solo tomará un parámetro de número entero y siempre le agregará 5 específicamente. Usando esta técnica, puede 'bloquear' el parámetro oculto 'este' para que sea una instancia de clase particular y generar una nueva función que solo tome un parámetro, tal como lo desea (tenga en cuenta que el parámetro oculto es siempre el primer parámetro, y los parámetros normales vienen en orden después). Mire los boost::binddocumentos para ver ejemplos, incluso analizan específicamente su uso para funciones miembro. Técnicamente existe una función estándar llamada [std::bind1st][3]que también puedes usar, pero boost::bindes más general.

Por supuesto, sólo hay una trampa más. boost::bindserá una buena función boost::para usted, pero técnicamente todavía no es un puntero de función sin formato como probablemente quiera Init. Afortunadamente, boost proporciona una forma de convertir boost::function en punteros sin formato, como se documenta en StackOverflow aquí . La forma en que implementa esto está más allá del alcance de esta respuesta, aunque también es interesante.

No se preocupe si esto parece ridículamente difícil: su pregunta cruza varios de los rincones más oscuros de C++ y boost::bindes increíblemente útil una vez que la aprende.

Actualización de C++11: en lugar de esto, boost::bindahora puede usar una función lambda que captura "esto". Básicamente, se trata de hacer que el compilador genere lo mismo para usted.

Joseph Garvin avatar Dec 31 '2008 05:12 Joseph Garvin

Eso no funciona porque un puntero de función miembro no se puede manejar como un puntero de función normal, porque espera un argumento de objeto "este".

En su lugar, puede pasar una función miembro estática de la siguiente manera, que son como funciones normales que no son miembros en este sentido:

m_cRedundencyManager->Init(&CLoggersInfra::Callback, this);

La función se puede definir de la siguiente manera

static void Callback(int other_arg, void * this_pointer) {
    CLoggersInfra * self = static_cast<CLoggersInfra*>(this_pointer);
    self->RedundencyManagerCallBack(other_arg);
}
Johannes Schaub - litb avatar Dec 30 '2008 13:12 Johannes Schaub - litb

Esta respuesta es una respuesta a un comentario anterior y no funciona con VisualStudio 2008, pero debería preferirse con compiladores más recientes.


Mientras tanto, ya no tienes que usar un puntero nulo y tampoco hay necesidad de impulso ya que std::bindy std::functionestán disponibles. Una ventaja (en comparación con los punteros nulos) es la seguridad de tipos, ya que el tipo de retorno y los argumentos se indican explícitamente mediante std::function:

// std::function<return_type(list of argument_type(s))>
void Init(std::function<void(void)> f);

Luego puedes crear el puntero de función std::bindy pasarlo a Init:

auto cLoggersInfraInstance = CLoggersInfra();
auto callback = std::bind(&CLoggersInfra::RedundencyManagerCallBack, cLoggersInfraInstance);
Init(callback);

Ejemplo completo para usar std::bindcon funciones miembro, miembros estáticos y no miembros:

#include <functional>
#include <iostream>
#include <string>

class RedundencyManager // incl. Typo ;-)
{
public:
    // std::function<return_type(list of argument_type(s))>
    std::string Init(std::function<std::string(void)> f) 
    {
        return f();
    }
};

class CLoggersInfra
{
private:
    std::string member = "Hello from non static member callback!";

public:
    static std::string RedundencyManagerCallBack()
    {
        return "Hello from static member callback!";
    }

    std::string NonStaticRedundencyManagerCallBack()
    {
        return member;
    }
};

std::string NonMemberCallBack()
{
    return "Hello from non member function!";
}

int main()
{
    auto instance = RedundencyManager();

    auto callback1 = std::bind(&NonMemberCallBack);
    std::cout << instance.Init(callback1) << "\n";

    // Similar to non member function.
    auto callback2 = std::bind(&CLoggersInfra::RedundencyManagerCallBack);
    std::cout << instance.Init(callback2) << "\n";

    // Class instance is passed to std::bind as second argument.
    // (heed that I call the constructor of CLoggersInfra)
    auto callback3 = std::bind(&CLoggersInfra::NonStaticRedundencyManagerCallBack,
                               CLoggersInfra()); 
    std::cout << instance.Init(callback3) << "\n";
}

Posible salida:

Hello from non member function!
Hello from static member callback!
Hello from non static member callback!

Además, std::placeholderspuede pasar argumentos dinámicamente a la devolución de llamada (por ejemplo, esto permite el uso de return f("MyString");in Initsi f tiene un parámetro de cadena).

Richard avatar Aug 05 '2017 18:08 Richard

Todavía es difícil conectar funciones de devolución de llamada de estilo C con instancias de clase C++. Quiero reformular la pregunta original:

  • Alguna biblioteca que está utilizando requiere que se vuelva a llamar una función de estilo C desde esa biblioteca. Cambiar la API de la biblioteca está fuera de discusión ya que no es su API.

  • Quiere que la devolución de llamada se maneje en su propio código C++ en métodos miembro

Como no mencionaste (exactamente) qué devolución de llamada quieres manejar, daré un ejemplo usando devoluciones de llamada GLFW para la entrada de claves . (Como nota al margen: sé que GLFW ofrece algún otro mecanismo para adjuntar datos de usuario a su API, pero ese no es el tema aquí).

No conozco ninguna solución a este problema que no incluya el uso de algún tipo de objeto estático. Veamos nuestras opciones:

Enfoque simple: use objetos globales de estilo C

Como siempre pensamos en clases e instancias, a veces olvidamos que en C++ todavía tenemos todo el arsenal de C en nuestras manos. Por eso a veces no se nos ocurre esta solución tan sencilla.

Supongamos que tenemos una clase Presentación que debería manejar la entrada del teclado. Esto podría verse así:

struct KeyInput {
    int pressedKey;
} KeyInputGlobal;

void globalKeyHandler(GLFWwindow* window, int key, int scancode, int action, int mods) {
    KeyInputGlobal.pressedKey = key;
}

int Presentation::getCurrentKey()
{
    return KeyInputGlobal.pressedKey;
}

void Presentation::initGLFW()
{
    glfwInit();
    glfwSetKeyCallback(window, globalKeyHandler);
}

Tenemos un objeto global KeyInputGlobal que debería recibir la tecla presionada. La función globalKeyHandler tiene exactamente la firma API estilo C que necesita la biblioteca GLFW para poder llamar a nuestro código. Se activa en nuestro método miembro initGLFW . Si en algún lugar de nuestro código estamos interesados ​​en la tecla presionada actualmente, podemos simplemente llamar al otro método miembro Presentation::getCurrentKey

¿Qué hay de malo en este enfoque?

Quizás todo esté bien. Depende completamente de su caso de uso. Tal vez esté totalmente bien con leer la última tecla presionada en algún lugar del código de su aplicación. No le importa que se hayan perdido eventos de pulsación de teclas. El enfoque simple es todo lo que necesita.

Para generalizar: si puede procesar completamente la devolución de llamada en código estilo C, calcular algún resultado y almacenarlo en un objeto global para leerlo más tarde desde otras partes de su código, entonces puede que tenga sentido utilizar este enfoque simple. Lo positivo: es muy sencillo de entender. ¿La baja? Se siente un poco como hacer trampa, porque realmente no procesaste la devolución de llamada en tu código C++, solo usaste los resultados. Si piensa en la devolución de llamada como un evento y desea que cada evento se procese adecuadamente en sus métodos miembro, este enfoque no será suficiente.

Otro enfoque simple: usar objetos estáticos de C++

Supongo que muchos de nosotros ya lo hemos hecho. Ciertamente lo he hecho. Pensando: Espera, tenemos un concepto C++ de globales, que es usar static . Pero podemos mantener la discusión breve aquí: puede ser más estilo C++ que usar el estilo C del ejemplo anterior, pero los problemas son los mismos: todavía tenemos globales, que son difíciles de combinar con métodos de miembros regulares y no estáticos. . Para completar, se vería así en nuestra declaración de clase:

class Presentation
{
public:
    struct KeyInput {
        int pressedKey;
    };
    static KeyInput KeyInputGlobal;

    static void globalKeyHandler(GLFWwindow* window, int key, int scancode, int action, int mods) {
        KeyInputGlobal.pressedKey = key;
    }
    int getCurrentKey()
    {
        return KeyInputGlobal.pressedKey;
    }
...
}

Activar nuestra devolución de llamada se vería igual, pero también tenemos que definir la estructura estática que recibe la tecla presionada en nuestra implementación:

void Presentation::initGLFW()
{
    glfwInit();
    glfwSetKeyCallback(window, globalKeyHandler);
}
//static
Presentation::KeyInput Presentation::KeyInputGlobal;

Es posible que se sienta inclinado a simplemente eliminar la palabra clave estática de nuestro método de devolución de llamada globalKeyHandler : el compilador le dirá inmediatamente que ya no puede pasar esto a GLFW en glfwSetKeyCallback() . Ahora, si tan solo pudiéramos conectar métodos estáticos con métodos regulares de alguna manera...

Enfoque basado en eventos C ++ 11 con estática y lambdas

La mejor solución que pude encontrar es la siguiente. Funciona y es algo elegante, pero todavía no lo considero perfecto. Veámoslo y discutamos:

void Presentation::initGLFW()
{
    glfwInit();
    static auto callback_static = [this](
           GLFWwindow* window, int key, int scancode, int action, int mods) {
        // because we have a this pointer we are now able to call a non-static member method:
        key_callbackMember(window, key, scancode, action, mods);
    };
    glfwSetKeyCallback(window,
    [](GLFWwindow* window, int key, int scancode, int action, int mods)
      {
        // only static methods can be called here as we cannot change glfw function parameter list to include instance pointer
        callback_static(window, key, scancode, action, mods);
      }
    );
}

void Presentation::key_callbackMember(GLFWwindow* window, int key, int scancode, int action, int mods)
{
    // we can now call anywhere in our code to process the key input:
    app->handleInput(key, action);
}

La definición de callback_static es donde conectamos un objeto estático con datos de instancia, en este caso esta es una instancia de nuestra clase Presentación . Puede leer la definición de la siguiente manera: Si se llama a callback_static en cualquier momento después de esta definición, todos los parámetros se pasarán al método miembro key_callbackMember llamado en la instancia de presentación que se acaba de usar. Esta definición todavía no tiene nada que ver con la biblioteca GLFW; es solo la preparación para el siguiente paso.

Ahora usamos una segunda lambda para registrar nuestra devolución de llamada con la biblioteca en glfwSetKeyCallback() . Nuevamente, si callback_static no se hubiera definido como estático , no podríamos pasarlo a GLFW aquí.

Esto es lo que sucede en tiempo de ejecución después de todas las inicializaciones, cuando GLFW llama a nuestro código:

  1. GLFW reconoce un evento clave y llama a nuestro objeto estático callback_static
  2. callback_static tiene acceso a una instancia de la clase Presentación y llama a su método de instancia key_callbackMember
  3. Ahora que estamos en el "mundo de los objetos", podemos procesar el evento clave en otro lugar. En este caso llamamos al método handleInput en alguna aplicación de objeto arbitrario , que se ha configurado en algún otro lugar de nuestro código.

Lo bueno: hemos logrado lo que queríamos sin necesidad de definir objetos globales fuera de nuestro método de inicialización initGLFW . No hay necesidad de globales estilo C.

Lo malo: no se deje engañar sólo porque todo está cuidadosamente empaquetado en un solo método. Todavía tenemos objetos estáticos. Y con ellos todos los problemas que tienen los objetos globales. Por ejemplo, múltiples llamadas a nuestro método de inicialización (con diferentes instancias de Presentation ) probablemente no tendrían el efecto deseado.

Resumen

Es posible conectar devoluciones de llamada estilo C de bibliotecas existentes a instancias de clases en su propio código. Puede intentar minimizar el código de limpieza definiendo los objetos necesarios en los métodos miembro de su código. Pero aún necesitas un objeto estático para cada devolución de llamada. Si desea conectar varias instancias de su código C++ con una devolución de llamada estilo C, prepárese para introducir una gestión más complicada de sus objetos estáticos que en el ejemplo anterior.

Espero que esto ayude a alguien. Feliz codificación.

Clemens Fehr avatar Feb 20 '2022 13:02 Clemens Fehr