2009-03-14 11 views
7

Necesito poder tener una superclase para ejecutar devoluciones de llamada definidas por una clase que herede de ella. Soy relativamente nuevo en C++ y, por lo que puedo decir, parece que el tema de los punteros-función-miembro es un área muy turbia.C++ Punteros a funciones de miembros Herencia

He visto respuestas a preguntas y publicaciones de blog aleatorias que discuten todo tipo de cosas, pero no estoy seguro si alguna de ellas trata específicamente con mi pregunta aquí.

Aquí hay un simple fragmento de código que ilustra lo que estoy tratando de hacer. El ejemplo puede no tener mucho sentido, pero se asemeja exactamente al código que intento escribir.

class A { 
    protected: 
    void doSomething(void (A::*someCallback)(int a)) { 
     (*this.*someCallback)(1234); 
    } 
}; 

class B : public A { 
    public: 
    void runDoIt() { doSomething(&B::doIt); } 
    void runDoSomethingElse() { doSomething(&B::doSomethingElse); } 
    protected: 
    void doIt(int foo) { 
     cout << "Do It! [" << foo << "]\n"; 
    } 
    void doSomethingElse(int foo) { 
     cout << "Do Something Else! [" << foo << "]\n"; 
    } 
}; 

int main(int argc, char *argv[]) { 
    B b; 
    b.runDoIt(); 
    b.runDoSomethingElse(); 
} 

Respuesta

7

Si puede usar las bibliotecas de impulso, le sugiero que utilice boost :: function para la tarea en cuestión.

class A { 
public: 
    void doSomething(boost::function< void (int) > callback) 
    { 
     callback(5); 
    } 
}; 

Entonces, cualquier heredero (o clase externa) pueden usar boost :: bind no hacer una llamada:

class B { 
public: 
    void my_method(int a); 
}; 
void test() 
{ 
    B b; 
    A a; 
    a.doSomething(boost::bind(&B::my_method, &b, _1)); 
}; 

No he comprobado la sintaxis exacta y he tecleado desde la parte superior de mi cabeza, pero eso está al menos cerca del código correcto.

+0

Yo estaría de acuerdo con este enfoque. Usar el impulso aquí tiene mucho sentido, han dedicado un gran esfuerzo en este tipo de cosas. – sstock

+0

Estaba un poco indeciso de usar boost para cualquier cosa, pero voy a intentarlo. ¡Funciona más o menos como lo describió aquí! –

0

Está tratando de llamar al método que se define en la clase derivada como clase base de método.
Pero esta función no está definida en la clase base.

2

C++ proporciona mejores construcciones que los punteros de funciones de miembros para su propósito. (En general, no debería tener que usar punteros de función en C++ puro y escrito correctamente). Dos opciones fuera de mi cabeza:

1) Crear una función virtual pura en A llamado doIt(). Llame doIt() desde doSomething(). En B, mantenga una variable de miembro enum para indicar lo que desea hacer y configúrelo antes de llamar a doSomething(). Reemplazar doIt() en B, con su implementación cambiar/colocar el enum del miembro al código subyacente que desea ejecutar.

2) Cree una clase DoInterface con un único método virtual puro doIt(). Cree derivadas de esto para su Clase B que tenga un puntero a la instancia propietaria B e implemente doIt() llamando a la función apropiada en B en cada una de sus implementaciones DoInterface. doSomething() tomaría una instancia de DoInterface como parámetro. Esto se llama un patrón de objeto de devolución de llamada.

+0

La opción 1, funciones de miembros virtuales, es casi siempre la forma más fácil y elegante de hacerlo, con una advertencia: los miembros virtuales no pueden ser llamados desde constructores o destructores de la clase base. Pero mientras no necesites hacer eso, ese es el camino a seguir. –

+0

Si solo tuviera que preocuparme por un método, o supiera exactamente cuántos métodos me gustaría "hacer", esto funcionaría. Lo investigaré más de cerca, pero no creo que esto funcione para mis propósitos ya que ya estaba haciendo esto. –

+0

Ese es el punto de que B es una clase derivada de A. B sabe exactamente cuántos métodos tiene que "hacer" y sabe cuándo se deben hacer, solo requiere un desencadenante de A, que es el punto del reemplazó doIt() en B. –

8

El problema es que una función miembro de B no es una función miembro de A, incluso si B deriva de A. Si tiene un void (A::*)(), puede invocarlo en cualquier A *, independientemente del tipo de objeto real al que se apunta.

(El mismo principio se aplicaría a una A & también, por supuesto.)

Supongamos B deriva de A, y C deriva de A; estaban fuera posible considerar un void (B::*)() (por ejemplo) como void (A::*)(), uno sería capaz de hacer algo como esto:

A *c=new C; 
A *b=new B; 
void (A::*f)()=&B::fn;//fn is not defined in A 
(c->*f)(); 

Y la función de miembro de B se pidió a un objeto de tipo C. Los resultados serían impredecibles en el mejor de los casos.

Basado en el código de ejemplo, y suponiendo que no uso de algo así como impulso, estaría inclinado a estructurar la devolución de llamada como un objeto:

class Callback { 
public: 
    virtual ~Callback() { 
    } 

    virtual Do(int a)=0; 
}; 

A continuación, la función que llama a la devolución de llamada tiene una de estos objetos de alguna manera, más que una función de puntero sin formato:

class A { 
protected: 
    void doSomething(Callback *c) { 
     c->Do(1234); 
    } 
}; 

a continuación, podría tener una devolución de llamada por función derivada usted está interesado en llamar. Para DOIT, por ejemplo:

class B:public A { 
public: 
    void runDoIt() { 
     DoItCallback cb(this); 
     this->doSomething(&cb); 
    } 
protected: 
    void doIt(int foo) { 
     // whatever 
    } 
private: 
    class DoItCallback:public Callback { 
    public: 
     DoItCallback(B *b):b_(b) {} 

     void Do(int a) { 
      b_->doIt(a); 
     } 
    private: 
     B *b_; 
    }; 
}; 

Una manera obvia de recortar sobre la plancha de caldera sería poner el puntero de función miembro en la devolución de llamada, ya que la devolución de llamada derivada es libre para hacer frente a objetos de un tipo específico. Esto haría que la devolución de llamada un poco más genérico, en el que cuando llamó sería invocar una función miembro arbitrario en un objeto de tipo B:

class BCallback:public Callback { 
public: 
    BCallback(B *obj,void (B::*fn)(int)):obj_(obj),fn_(fn) {} 

    void Do(int a) { 
     (obj_->*fn_)(a); 
    } 
private: 
    B *obj_; 
    void (B::*fn_)(int); 
}; 

Esto haría doIT así:

void B::runDoIt() { 
    BCallback cb(this,&B::doIt); 
    this->doSomething(&cb); 
} 

potencialmente, esto podría ser "mejorado", aunque no todos los lectores pueden ver bastante de esa manera, por lo templating:

template<class T> 
class GenericCallback:public Callback { 
public: 
    GenericCallback(T *obj,void (T::*fn)(int)):obj_(obj),fn_(fn) {} 

    void Do(int a) { 
     (obj_->*fn_)(a); 
    } 
private: 
    T *obj_; 
    void (T::*fn_)(int); 
}; 

uso de este, la función runDoIt anterior podría llegar a ser:

void B::runDoIt() { 
    GenericCallback<B> cb(this,&B::doIt); 
    this->doSomething(&cb); 
} 

(La devolución de llamada genérica podría también ser templated en el propio puntero de función miembro, aunque esto es poco probable que proporcione ninguna ventaja práctica en la mayoría de los casos. Es simplemente más tipeo.)

He encontrado que las cosas de estructuración de esta manera resultan bien, ya que no requieren herencia. Por lo tanto, impone pocas restricciones sobre el código que se devolverá, lo que en mi opinión siempre es algo bueno, y he descubierto que supera la verbosidad que es difícil de eliminar por completo. Imposible basado en el ejemplo para decir si este enfoque realmente se adaptaría, aunque ...

+0

Voy a tratar de hacerlo de esta manera. Será más prolijo de lo que esperaba, pero creo que será mejor que la siguiente mejor solución que involucre el uso de la carcasa y el cambio. –

+0

Muy buena respuesta para aprender. El enfoque que das está cerca de una versión limitada de boost libs. La devolución de llamada es una versión limitada de la función boost :: y GenericCallback es una versión reducida de bind. –

+0

Otra cosa es que puede eliminar algunos de los punteros y traducirlos a referencias.doSomething (Callback & c), o GenericCallback podría almacenar un & en lugar de un puntero, por lo que es más explícito que no se ocupará de esa memoria –

Cuestiones relacionadas