¿Cómo puedo pasar una función miembro donde se espera una función libre?

122

La pregunta es la siguiente: considere este código:

#include <iostream>


class aClass
{
public:
    void aTest(int a, int b)
    {
        printf("%d + %d = %d", a, b, a + b);
    }
};

void function1(void (*function)(int, int))
{
    function(1, 1);
}

void test(int a,int b)
{
    printf("%d - %d = %d", a , b , a - b);
}

int main (int argc, const char* argv[])
{
    aClass a();

    function1(&test);
    function1(&aClass::aTest); // <-- How should I point to a's aClass::test function?

    return 0;
}

¿Cómo puedo usar los a's aClass::testcomo argumento para function1? Estoy atrapado haciendo esto.

Me gustaría acceder a un miembro de la clase.

Jorge Leitao
fuente
1
Eche un vistazo a esta respuesta stackoverflow.com/questions/2402579/… y también esta C ++ FAQ parashift.com/c++-faq/pointers-to-members.html
amdn
16
Esto no es absolutamente un duplicado (al menos no de la pregunta particular que está vinculada). Esa pregunta es acerca de cómo declarar un miembro que es un puntero a una función; se trata de cómo pasar un puntero a una función miembro no estática como parámetro.
CarLuva

Respuestas:

144

No hay nada de malo en usar punteros de función. Sin embargo, los punteros a funciones miembro no estáticas no son como los punteros de función normales: las funciones miembro deben llamarse en un objeto que se pasa como un argumento implícito a la función. La firma de su función miembro anterior es, por lo tanto

void (aClass::*)(int, int)

en lugar del tipo que intentas usar

void (*)(int, int)

Un enfoque podría consistir en hacer que el miembro funcione, staticen cuyo caso no requiere invocar ningún objeto y puede usarlo con el tipo void (*)(int, int).

Si necesita acceder a cualquier miembro no estático de su clase y necesita seguir con punteros de función, por ejemplo, debido a que la función es parte de una interfaz C, su mejor opción es pasar siempre void*a su función tomando punteros de función y llamando su miembro a través de una función de reenvío que obtiene un objeto del void*y luego llama a la función miembro.

En una interfaz C ++ adecuada, es posible que desee ver cómo su función toma un argumento con plantilla para que los objetos de función usen tipos de clase arbitrarios. Si el uso de una interfaz con plantilla no es deseable, debe usar algo como std::function<void(int, int)>: puede crear un objeto de función invocable adecuadamente para estos, por ejemplo, usando std::bind().

Los enfoques de tipo seguro que utilizan un argumento de plantilla para el tipo de clase o un tipo adecuado std::function<...>son preferibles que el uso de una void*interfaz, ya que eliminan la posibilidad de errores debido a una conversión al tipo incorrecto.

Para aclarar cómo usar un puntero de función para llamar a una función miembro, aquí hay un ejemplo:

// the function using the function pointers:
void somefunction(void (*fptr)(void*, int, int), void* context) {
    fptr(context, 17, 42);
}

void non_member(void*, int i0, int i1) {
    std::cout << "I don't need any context! i0=" << i0 << " i1=" << i1 << "\n";
}

struct foo {
    void member(int i0, int i1) {
        std::cout << "member function: this=" << this << " i0=" << i0 << " i1=" << i1 << "\n";
    }
};

void forwarder(void* context, int i0, int i1) {
    static_cast<foo*>(context)->member(i0, i1);
}

int main() {
    somefunction(&non_member, 0);
    foo object;
    somefunction(&forwarder, &object);
}
Dietmar Kühl
fuente
Ok, me gusta esta respuesta! ¿Puede especificar a qué se refiere con "llamar a su miembro a través de una función de reenvío que obtiene un objeto del vacío * y luego llama a la función miembro", o compartir un enlace útil? Gracias
Jorge Leitao
Creo que lo tengo. (He editado tu publicación) Gracias por la explicación y el ejemplo, realmente útil. Solo para confirmar: para cada función miembro que quiero señalar, tengo que hacer un reenviador. ¿Correcto?
Jorge Leitao
Bueno, sí, más o menos. Dependiendo de qué tan efectivo esté usando las plantillas, puede salirse con la suya creando plantillas de reenvío que pueden funcionar con diferentes clases y funciones miembro. Cómo hacerlo sería una función separada, creo ;-)
Dietmar Kühl
1
No me gusta esta respuesta porque usa void*, lo que significa que puede obtener errores muy desagradables, porque ya no está marcada como marcada.
Superlokkus
@Superlokkus: ¿puedes iluminarnos con una alternativa?
Dietmar Kühl
88

La respuesta de @Pete Becker está bien, pero también puede hacerlo sin pasar la classinstancia como un parámetro explícito function1en C ++ 11:

#include <functional>
using namespace std::placeholders;

void function1(std::function<void(int, int)> fun)
{
    fun(1, 1);
}

int main (int argc, const char * argv[])
{
   ...

   aClass a;
   auto fp = std::bind(&aClass::test, a, _1, _2);
   function1(fp);

   return 0;
}
Matt Phillips
fuente
1
Es void function1(std::function<void(int, int)>)correcto?
Deqing
2
Debe dar al argumento de la función un nombre de variable y luego el nombre de la variable es lo que realmente pasa. Entonces: void function1(std::function<void(int, int)> functionToCall)y luego functionToCall(1,1);. Traté de editar la respuesta, pero alguien la rechazó por no tener ningún sentido por alguna razón. Veremos si se vota en algún momento.
Dorky Engineer
1
@DorkyEngineer Eso es bastante extraño, creo que debes estar en lo cierto, pero no sé cómo ese error podría haber pasado desapercibido durante tanto tiempo. De todos modos, he editado la respuesta ahora.
Matt Phillips
2
Encontré esta publicación diciendo que hay una severa penalización de rendimiento por std :: function
Kevin
2
@kevin, es posible que desee revisar su comentario, ya que las respuestas de esa publicación mostraron un defecto en el punto de referencia.
Jorge Leitao
54

Un puntero a la función miembro es diferente de un puntero a la función. Para usar una función miembro a través de un puntero, necesita un puntero (obviamente) y un objeto para aplicarlo. Entonces la versión apropiada de function1sería

void function1(void (aClass::*function)(int, int), aClass& a) {
    (a.*function)(1, 1);
}

y para llamarlo:

aClass a; // note: no parentheses; with parentheses it's a function declaration
function1(&aClass::test, a);
Pete Becker
fuente
1
Muchas gracias. Acabo de descubrir que los corchetes cuentan aquí: function1 (& (aClass :: test), a) funciona con MSVC2013 pero no con gcc. gcc necesita el & directamente delante del nombre de la clase (lo cual me parece confuso, porque el operador & toma la dirección de la función, no de la clase)
kritzel_sw
Pensé functionen void (aClass::*function)(int, int)era un tipo, debido a que es un tipo de typedef void (aClass::*function)(int, int).
Olumide
@Olumide: typedef int X;define un tipo; int X;crea un objeto
Pete Becker
11

Desde 2011, si puede cambiar function1, hágalo así:

#include <functional>
#include <cstdio>

using namespace std;

class aClass
{
public:
    void aTest(int a, int b)
    {
        printf("%d + %d = %d", a, b, a + b);
    }
};

template <typename Callable>
void function1(Callable f)
{
    f(1, 1);
}

void test(int a,int b)
{
    printf("%d - %d = %d", a , b , a - b);
}

int main()
{
    aClass obj;

    // Free function
    function1(&test);

    // Bound member function
    using namespace std::placeholders;
    function1(std::bind(&aClass::aTest, obj, _1, _2));

    // Lambda
    function1([&](int a, int b) {
        obj.aTest(a, b);
    });
}

( demostración en vivo )

Observe también que arreglé su definición de objeto roto ( aClass a();declara una función).

Carreras de ligereza en órbita
fuente
2

Hice una pregunta similar ( openframeworks de C ++ pasando nulo desde otras clases ) pero la respuesta que encontré fue más clara, así que aquí la explicación para los registros futuros:

es más fácil usar std :: function como en:

 void draw(int grid, std::function<void()> element)

y luego llame como:

 grid.draw(12, std::bind(&BarrettaClass::draw, a, std::placeholders::_1));

o incluso más fácil:

  grid.draw(12, [&]{a.draw()});

donde crea una lambda que llama al objeto que lo captura por referencia

Nicola Bertelloni
fuente
1
Dado que esto está etiquetado como CPP, creo que esta es la solución más limpia. Sin embargo, podríamos usar una referencia constante a std::functionin en drawlugar de copiarlo en cada llamada.
Sohaib
2
El marcador de posición no debe usarse en este ejemplo ya que la función no toma ningún parámetro.
kroiz
1

Hice que el miembro funcionara como estático y todo funciona:

#include <iostream>

class aClass
{
public:
    static void aTest(int a, int b)
    {
        printf("%d + %d = %d\n", a, b, a + b);
    }
};

void function1(int a,int b,void function(int, int))
{
    function(a, b);
}

void test(int a,int b)
{
    printf("%d - %d = %d\n", a , b , a - b);
}

int main (int argc, const char* argv[])
{
    aClass a;

    function1(10,12,test);
    function1(10,12,a.aTest); // <-- How should I point to a's aClass::test function?

    getchar();return 0;
}
mathengineer
fuente
No solo resuelve la pregunta principal "¿Cómo puedo usar la prueba aClass :: a de a como un argumento para funcionar1?" pero también se recomienda evitar modificar las variables privadas de la clase usando código externo a la clase
mathengineer
1

No estoy seguro de por qué se ha pasado por alto esta solución increíblemente simple:

#include <stdio.h>

class aClass
{
public:
    void aTest(int a, int b)
    {
        printf("%d + %d = %d\n", a, b, a + b);
    }
};

template<class C>
void function1(void (C::*function)(int, int), C& c)
{
    (c.*function)(1, 1);
}
void function1(void (*function)(int, int)) {
  function(1, 1);
}

void test(int a,int b)
{
    printf("%d - %d = %d\n", a , b , a - b);
}

int main (int argc, const char* argv[])
{
    aClass a;

    function1(&test);
    function1<aClass>(&aClass::aTest, a);
    return 0;
}

Salida:

1 - 1 = 0
1 + 1 = 2
hLk
fuente
0

Si realmente no necesita usar la instancia a (es decir, puede hacerla estática como la respuesta de @mathengineer ), simplemente puede pasar una lambda sin captura. (que decae al puntero de función)


#include <iostream>

class aClass
{
public:
   void aTest(int a, int b)
   {
      printf("%d + %d = %d", a, b, a + b);
   }
};

void function1(void (*function)(int, int))
{
    function(1, 1);
}

int main()
{
   //note: you don't need the `+`
   function1(+[](int a,int b){return aClass{}.aTest(a,b);}); 
}

Wandbox


nota: si aClasses costoso de construir o tiene efectos secundarios, esto puede no ser una buena manera.

manzana manzana
fuente
-1

Puedes dejar de golpearte la cabeza ahora. Aquí está el contenedor de la función miembro para admitir funciones existentes que toman en funciones simples C como argumentos. thread_localdirectiva es la clave aquí.

http://cpp.sh/9jhk3

// Example program
#include <iostream>
#include <string>

using namespace std;

typedef int FooCooker_ (int);

// Existing function
extern "C" void cook_10_foo (FooCooker_ FooCooker) {
    cout << "Cooking 10 Foo ..." << endl;
    cout << "FooCooker:" << endl;
    FooCooker (10);
}

struct Bar_ {
    Bar_ (int Foo = 0) : Foo (Foo) {};
    int cook (int Foo) {
        cout << "This Bar got " << this->Foo << endl;
        if (this->Foo >= Foo) {
            this->Foo -= Foo;
            cout << Foo << " cooked" << endl;
            return Foo;
        } else {
            cout << "Can't cook " <<  Foo << endl;
            return 0;
        }
    }
    int Foo = 0;
};

// Each Bar_ object and a member function need to define
// their own wrapper with a global thread_local object ptr
// to be called as a plain C function.
thread_local static Bar_* BarPtr = NULL;
static int cook_in_Bar (int Foo) {
    return BarPtr->cook (Foo);
}

thread_local static Bar_* Bar2Ptr = NULL;
static int cook_in_Bar2 (int Foo) {
    return Bar2Ptr->cook (Foo);
}

int main () {
  BarPtr = new Bar_ (20);
  cook_10_foo (cook_in_Bar);

  Bar2Ptr = new Bar_ (40);
  cook_10_foo (cook_in_Bar2);

  delete BarPtr;
  delete Bar2Ptr;
  return 0;
}

Por favor comente sobre cualquier problema con este enfoque.

Otras respuestas no pueden llamar a lasC funciones simples existentes : http://cpp.sh/8exun

Necktwi
fuente
3
Entonces, en lugar de usar std :: bind o una lambda para ajustar la instancia, confía en una variable global. No puedo ver ninguna ventaja de este enfoque en comparación con las otras respuestas.
súper
@super, Otras respuestas no pueden llamar a las funciones existentes tomando Cfunciones simples como argumentos.
Necktwi
2
La pregunta es cómo llamar a una función miembro. Pasar una función gratuita ya está funcionando para OP en la pregunta. Tampoco estás pasando nada. Aquí solo hay funciones codificadas y un puntero Foo_ global. ¿Cómo funcionaría esta escala si desea llamar a una función miembro diferente? Tendría que reescribir las funciones subyacentes, o usar diferentes para cada objetivo.
súper