2011-03-05 13 views
13

Quería implementar un evento de C# en C++ solo para ver si podía hacerlo. Me quedé atascado, sé que el fondo está mal, pero me doy cuenta de que mi mayor problema es ...¿Es posible implementar eventos en C++?

¿Cómo sobrecargo el operador () para que sea lo que está en T, en este caso int func(float)? No puedo? ¿Puedo? ¿Puedo implementar una buena alternativa?

#include <deque> 
using namespace std; 

typedef int(*MyFunc)(float); 

template<class T> 
class MyEvent 
{ 
    deque<T> ls; 
public: 
    MyEvent& operator +=(T t) 
    { 
     ls.push_back(t); 
     return *this; 
    } 
}; 
static int test(float f){return (int)f; } 
int main(){ 
    MyEvent<MyFunc> e; 
    e += test; 
} 
+0

Por favor, no sobrecargar 'operador + =' como C# hace, me parece que muy confuso . – fredoverflow

+2

Vea http://www.codeproject.com/KB/cpp/ImpossiblyFastCppDelegate.aspx –

+1

Uno de su problema es que C++ no admite directamente a los delegados, que son una parte central de los eventos de estilo C#. http://www.codeproject.com/KB/cpp/ FastDelegate.aspx Implementa Delegados de una manera no estándar, pero compatible con la mayoría de los compiladores. – CodesInChaos

Respuesta

22

Si puede usar Boost, considerar el uso de Boost.Signals2, que proporciona señales de ranuras/eventos/funcionalidad observadores. Es sencillo y fácil de usar, y es bastante flexible. Boost.Signals2 también le permite registrar objetos invocables arbitrarios (como funtores o funciones miembro enlazadas), por lo que es más flexible y tiene una gran cantidad de funcionalidades para ayudarlo a administrar las vidas útiles de los objetos.


Si está tratando de implementarlo usted mismo, está en el camino correcto. Sin embargo, tiene un problema: ¿qué quiere exactamente hacer con los valores devueltos por cada una de las funciones registradas? Solo puede devolver un valor desde operator(), por lo que debe decidir si desea devolver nada, o uno de los resultados, o de alguna manera agregar los resultados.

Suponiendo que queremos ignorar los resultados, es bastante sencillo implementar esto, pero es un poco más fácil si toma cada uno de los tipos de parámetro como un parámetro de tipo de plantilla separado (alternativamente, podría usar algo como Boost.TypeTraits, que le permite diseccionar fácilmente un tipo de función):

template <typename TArg0> 
class MyEvent 
{ 
    typedef void(*FuncPtr)(TArg0); 
    typedef std::deque<FuncPtr> FuncPtrSeq; 

    FuncPtrSeq ls; 
public: 
    MyEvent& operator +=(FuncPtr f) 
    { 
     ls.push_back(f); 
     return *this; 
    } 

    void operator()(TArg0 x) 
    { 
     for (typename FuncPtrSeq::iterator it(ls.begin()); it != ls.end(); ++it) 
      (*it)(x); 
    } 
}; 

Esto requiere la función registrada a tener un tipo void retorno. Para ser capaz de aceptar las funciones con cualquier tipo de retorno, se puede cambiar para ser FuncPtr

typedef std::function<void(TArg0)> FuncPtr; 

(o utilizar boost::function o std::tr1::function si no tiene la versión de C++ 0x disponible). Si desea hacer algo con los valores devueltos, puede tomar el tipo de devolución como otro parámetro de plantilla al MyEvent. Eso debería ser relativamente sencillo de hacer.

Con la implementación anterior, lo siguiente debe funcionar:

void test(float) { } 

int main() 
{ 
    MyEvent<float> e; 
    e += test; 
    e(42); 
} 

Otro enfoque, que le permite soportar diferentes arities de eventos, sería utilizar un único parámetro de tipo para el tipo de función y tiene varias sobrecargas operator() sobrecargadas, cada una teniendo un número diferente de argumentos. Estas sobrecargas tienen que ser plantillas, de lo contrario obtendrás errores de compilación para cualquier sobrecarga que no coincida con la realidad del evento. Aquí está un ejemplo viable:

template <typename TFunc> 
class MyEvent 
{ 
    typedef typename std::add_pointer<TFunc>::type FuncPtr; 
    typedef std::deque<FuncPtr> FuncPtrSeq; 

    FuncPtrSeq ls; 
public: 
    MyEvent& operator +=(FuncPtr f) 
    { 
     ls.push_back(f); 
     return *this; 
    } 

    template <typename TArg0> 
    void operator()(TArg0 a1) 
    { 
     for (typename FuncPtrSeq::iterator it(ls.begin()); it != ls.end(); ++it) 
      (*it)(a1); 
    } 

    template <typename TArg0, typename TArg1> 
    void operator()(const TArg0& a1, const TArg1& a2) 
    { 
     for (typename FuncPtrSeq::iterator it(ls.begin()); it != ls.end(); ++it) 
      (*it)(a1, a2); 
    } 
}; 

(He usado std::add_pointer de C++ 0x aquí, pero este tipo modificador también ser encontrado en Boost y C++ TR1, sino que sólo lo hace un poco más limpio para utilizar la función . plantilla ya que se puede utilizar un tipo de función directamente, que no tiene que utilizar un tipo de puntero de función) Este es un ejemplo de uso:

void test1(float) { } 
void test2(float, float) { } 

int main() 
{ 
    MyEvent<void(float)> e1; 

    e1 += test1; 
    e1(42); 

    MyEvent<void(float, float)> e2; 
    e2 += test2; 
    e2(42, 42); 
} 
+0

Creo que se trata de aprender * cómo * implementar ese tipo de sistema de devolución de llamada. –

+0

-edit- no te vi editar. Lo estoy leyendo ahora. –

+0

Excelente respuesta. Intenté copiar/pegar la función pero usando 2 plantillas y no me gustó el hecho de que dos clases con el mismo nombre usan una cantidad diferente de plantillas. Estaba pensando en ello, pero no hay forma de que acepte MyEvent , MyEvent al predefinir la cantidad de plantillas? Al igual que ahora, tengo MyEvent0-9 (no necesitaré más de 9 parámetros ... tal vez debería hacer una lista nula * que pueda pasar también). ¿Está utilizando MyEvent2 la solución y es imposible de usar MyEvent Y MyEvent ? –

1

Eso es posible, pero no con su diseño actual. El problema radica en el hecho de que la firma de la función de devolución de llamada está bloqueada en su argumento de plantilla.No creo que debas intentar apoyar esto de todos modos, todas las devoluciones de llamada en la misma lista deberían tener la misma firma, ¿no crees?

3

Usted absolutamente puede. James McNellis ya se ha vinculado a una solución completa, pero por su ejemplo de juguete que puede hacer lo siguiente:

#include <deque> 
using namespace std; 

typedef int(*MyFunc)(float); 

template<typename F> 
class MyEvent; 

template<class R, class Arg> 
class MyEvent<R(*)(Arg)> 
{ 
    typedef R (*FuncType)(Arg); 
    deque<FuncType> ls; 
    public: 
    MyEvent<FuncType>& operator+=(FuncType t) 
    { 
      ls.push_back(t); 
      return *this; 
    } 

    void operator()(Arg arg) 
    { 
      typename deque<FuncType>::iterator i = ls.begin(); 
      typename deque<FuncType>::iterator e = ls.end(); 
      for(; i != e; ++i) { 
        (*i)(arg); 
      } 
    } 
}; 
static int test(float f){return (int)f; } 
int main(){ 
    MyEvent<MyFunc> e; 
    e += test; 
    e(2.0); 
} 

Aquí he hecho uso de la especialización parcial de separar los componentes del tipo puntero de función para descubrir la tipo de argumento boost.signals hace esto y más, aprovechando funciones como borrado de tipo y rasgos para determinar esta información para objetos invocables con puntero sin función.

Para N argumentos, hay dos enfoques. La forma "fácil", que se agregó para C++ 0x, es aprovechar las plantillas variadic y algunas otras características. Sin embargo, hemos estado haciendo esto desde antes de agregar esas características, y no sé qué compiladores ., apoyar las plantillas variadic todavía Así que podemos hacerlo de la manera más difícil, que es, de nuevo se especializan:

template<typename R, typename Arg0, typename Arg1> 
class MyEvent<R(*)(Arg0, Arg1)> 
{ 
    typedef R (*FuncType)(Arg0, Arg1); 
    deque<FuncType> ls; 
    ... 
    void operatror()(Arg0 a, Arg1) 
    { ... } 
    MyEvent<FuncType>& operator+=(FuncType f) 
    { ls.push_back(f); } 
    ... 
}; 

esto se pone aburrido, por supuesto, por lo que tienen las bibliotecas como boost.signals que ya se golpeaban a cabo (y esas utilizar macros, etc., para aliviar un poco el tedio).

para permitir una sintaxis MyEvent<int, int> estilo puedes utilizar una técnica similar a la siguiente

struct NullEvent; 

template<typename A = NullEvent, typename B = NullEvent, typename C = NullEvent> 
class HisEvent; 


template<> 
struct HisEvent<NullEvent,NullEvent,NullEvent> 
{ void operator()() {} }; 

template<typename A> 
struct HisEvent<A,NullEvent,NullEvent> 
{ void operator()(A a) {} }; 

template<typename A, typename B> 
struct HisEvent<A, B, NullEvent> 
{ 
    void operator()(A a, B b) {} 
}; 

template<typename A, typename B, typename C> 
struct HisEvent 
{ 
    void operator()(A a, B b, C c) 
    {} 
}; 

static int test(float f){return (int)f; } 
int main(){ 
    MyEvent<MyFunc> e; 
    e += test; 
    e(2.0); 

    HisEvent<int> h; 
    HisEvent<int, int> h2; 
} 

El tipo NullEvent se usa como marcador de posición y nuevamente utilizamos la especialización parcial para determinar la aridad.

+0

+1 para publicar de manera efectiva la misma cosa en la que estaba trabajando para editar mi respuesta. –

+0

El problema con todo lo que he visto hasta ahora es no poder especificar la cantidad y el tipo de argumento. Al igual que ahora, aunque arg es solo un argumento, ¿y si quiero dos? De hecho, tengo una idea, pero no creo que funcione. –

+0

Excelente respuesta.Intenté copiar/pegar la función pero usando 2 plantillas y no me gustó el hecho de que dos clases con el mismo nombre usan una cantidad diferente de plantillas. Estaba pensando en ello, pero no hay forma de que acepte MyEvent , MyEvent al predefinir la cantidad de plantillas? Al igual que ahora, tengo MyEvent0-9 (no necesitaré más de 9 parámetros ... tal vez debería hacer una lista nula * que pueda pasar también). ¿Está utilizando MyEvent2 la solución y es imposible de usar MyEvent Y MyEvent ? –

1

EDIT: Implementación segura de subprocesos, basada en this respuesta. correcciones y mejoras de rendimiento

Ésta es mi versión, la mejora de un tal James McNellis' mediante la adición de muchos: operator-=, plantilla variadic para apoyar cualquier ariedad de los objetos que se puede llamar almacenados, conveniencia Bind(func, object) y Unbind(func, object) métodos para unir fácilmente los objetos y funciones miembro ejemplo, operadores de asignación y comparación con nullptr. Me alejé de usar std::add_pointer para simplemente usar std::function que en mis intentos es más flexible (acepta tanto lambdas como std :: function). También me moví para usar std::vector para una iteración más rápida y eliminé el retorno de *this en los operadores, ya que no parece ser muy seguro/útil de todos modos. Todavía falta en la semántica de C#: los eventos C# no se pueden borrar desde fuera de la clase donde se declaran (sería fácil agregar esto por amistad del estado a un tipo de plantilla).

Sigue el código, comentarios son bienvenidos:

#pragma once 

#include <typeinfo> 
#include <functional> 
#include <stdexcept> 
#include <memory> 
#include <atomic> 
#include <cstring> 

template <typename TFunc> 
class Event; 

template <class RetType, class... Args> 
class Event<RetType(Args ...)> final 
{ 
private: 
    typedef typename std::function<RetType(Args ...)> Closure; 

    struct ComparableClosure 
    { 
     Closure Callable; 
     void *Object; 
     uint8_t *Functor; 
     int FunctorSize; 

     ComparableClosure(const ComparableClosure &) = delete; 

     ComparableClosure() : Object(nullptr), Functor(nullptr), FunctorSize(0) { } 

     ComparableClosure(Closure &&closure) : Callable(std::move(closure)), Object(nullptr), Functor(nullptr), FunctorSize(0) { } 

     ~ComparableClosure() 
     { 
      if (Functor != nullptr) 
       delete[] Functor; 
     } 

     ComparableClosure & operator=(const ComparableClosure &closure) 
     { 
      Callable = closure.Callable; 
      Object = closure.Object; 
      FunctorSize = closure.FunctorSize; 
      if (closure.FunctorSize == 0) 
      { 
       Functor = nullptr; 
      } 
      else 
      { 
       Functor = new uint8_t[closure.FunctorSize]; 
       std::memcpy(Functor, closure.Functor, closure.FunctorSize); 
      } 

      return *this; 
     } 

     bool operator==(const ComparableClosure &closure) 
     { 
      if (Object == nullptr && closure.Object == nullptr) 
      { 
       return Callable.target_type() == closure.Callable.target_type(); 
      } 
      else 
      { 
       return Object == closure.Object && FunctorSize == closure.FunctorSize 
        && std::memcmp(Functor, closure.Functor, FunctorSize) == 0; 
      } 
     } 
    }; 

    struct ClosureList 
    { 
     ComparableClosure *Closures; 
     int Count; 

     ClosureList(ComparableClosure *closures, int count) 
     { 
      Closures = closures; 
      Count = count; 
     } 

     ~ClosureList() 
     { 
      delete[] Closures; 
     } 
    }; 

    typedef std::shared_ptr<ClosureList> ClosureListPtr; 

private: 
    ClosureListPtr m_events; 

private: 
    bool addClosure(const ComparableClosure &closure) 
    { 
     auto events = std::atomic_load(&m_events); 
     int count; 
     ComparableClosure *closures; 
     if (events == nullptr) 
     { 
      count = 0; 
      closures = nullptr; 
     } 
     else 
     { 
      count = events->Count; 
      closures = events->Closures; 
     } 

     auto newCount = count + 1; 
     auto newClosures = new ComparableClosure[newCount]; 
     if (count != 0) 
     { 
      for (int i = 0; i < count; i++) 
       newClosures[i] = closures[i]; 
     } 

     newClosures[count] = closure; 
     auto newEvents = ClosureListPtr(new ClosureList(newClosures, newCount)); 
     if (std::atomic_compare_exchange_weak(&m_events, &events, newEvents)) 
      return true; 

     return false; 
    } 

    bool removeClosure(const ComparableClosure &closure) 
    { 
     auto events = std::atomic_load(&m_events); 
     if (events == nullptr) 
      return true; 

     int index = -1; 
     auto count = events->Count; 
     auto closures = events->Closures; 
     for (int i = 0; i < count; i++) 
     { 
      if (closures[i] == closure) 
      { 
       index = i; 
       break; 
      } 
     } 

     if (index == -1) 
      return true; 

     auto newCount = count - 1; 
     ClosureListPtr newEvents; 
     if (newCount == 0) 
     { 
      newEvents = nullptr; 
     } 
     else 
     { 
      auto newClosures = new ComparableClosure[newCount]; 
      for (int i = 0; i < index; i++) 
       newClosures[i] = closures[i]; 

      for (int i = index + 1; i < count; i++) 
       newClosures[i - 1] = closures[i]; 

      newEvents = ClosureListPtr(new ClosureList(newClosures, newCount)); 
     } 

     if (std::atomic_compare_exchange_weak(&m_events, &events, newEvents)) 
      return true; 

     return false; 
    } 

public: 
    Event() 
    { 
     std::atomic_store(&m_events, ClosureListPtr()); 
    } 

    Event(const Event &event) 
    { 
     std::atomic_store(&m_events, std::atomic_load(&event.m_events)); 
    } 

    ~Event() 
    { 
     (*this) = nullptr; 
    } 

    void operator =(const Event &event) 
    { 
     std::atomic_store(&m_events, std::atomic_load(&event.m_events)); 
    } 

    void operator=(nullptr_t nullpointer) 
    { 
     while (true) 
     { 
      auto events = std::atomic_load(&m_events); 
      if (!std::atomic_compare_exchange_weak(&m_events, &events, ClosureListPtr())) 
       continue; 

      break; 
     } 
    } 

    bool operator==(nullptr_t nullpointer) 
    { 
     auto events = std::atomic_load(&m_events); 
     return events == nullptr; 
    } 

    bool operator!=(nullptr_t nullpointer) 
    { 
     auto events = std::atomic_load(&m_events); 
     return events != nullptr; 
    } 

    void operator +=(Closure f) 
    { 
     ComparableClosure closure(std::move(f)); 
     while (true) 
     { 
      if (addClosure(closure)) 
       break; 
     } 
    } 

    void operator -=(Closure f) 
    { 
     ComparableClosure closure(std::move(f)); 
     while (true) 
     { 
      if (removeClosure(closure)) 
       break; 
     } 
    } 

    template <typename TObject> 
    void Bind(RetType(TObject::*function)(Args...), TObject *object) 
    { 
     ComparableClosure closure; 
     closure.Callable = [object, function](Args&&...args) 
     { 
      return (object->*function)(std::forward<Args>(args)...); 
     }; 
     closure.FunctorSize = sizeof(function); 
     closure.Functor = new uint8_t[closure.FunctorSize]; 
     std::memcpy(closure.Functor, (void*)&function, sizeof(function)); 
     closure.Object = object; 

     while (true) 
     { 
      if (addClosure(closure)) 
       break; 
     } 
    } 

    template <typename TObject> 
    void Unbind(RetType(TObject::*function)(Args...), TObject *object) 
    { 
     ComparableClosure closure; 
     closure.FunctorSize = sizeof(function); 
     closure.Functor = new uint8_t[closure.FunctorSize]; 
     std::memcpy(closure.Functor, (void*)&function, sizeof(function)); 
     closure.Object = object; 

     while (true) 
     { 
      if (removeClosure(closure)) 
       break; 
     } 
    } 

    void operator()() 
    { 
     auto events = std::atomic_load(&m_events); 
     if (events == nullptr) 
      return; 

     auto count = events->Count; 
     auto closures = events->Closures; 
     for (int i = 0; i < count; i++) 
      closures[i].Callable(); 
    } 

    template <typename TArg0, typename ...Args2> 
    void operator()(TArg0 a1, Args2... tail) 
    { 
     auto events = std::atomic_load(&m_events); 
     if (events == nullptr) 
      return; 

     auto count = events->Count; 
     auto closures = events->Closures; 
     for (int i = 0; i < count; i++) 
      closures[i].Callable(a1, tail...); 
    } 
}; 

he comprobado con esto:

#include <iostream> 
using namespace std; 

class Test 
{ 
public: 
    void foo() { cout << "Test::foo()" << endl; } 
    void foo1(int arg1, double arg2) { cout << "Test::foo1(" << arg1 << ", " << arg2 << ") " << endl; } 
}; 

class Test2 
{ 
public: 

    Event<void()> Event1; 
    Event<void(int, double)> Event2; 
    void foo() { cout << "Test2::foo()" << endl; } 
    Test2() 
    { 
     Event1.Bind(&Test2::foo, this); 
    } 
    void foo2() 
    { 
     Event1(); 
     Event2(1, 2.2); 
    } 
    ~Test2() 
    { 
     Event1.Unbind(&Test2::foo, this); 
    } 
}; 

int main(int argc, char* argv[]) 
{ 
    (void)argc; 
    (void)argv; 

    Test2 t2; 
    Test t1; 

    t2.Event1.Bind(&Test::foo, &t1); 
    t2.Event2 += [](int arg1, double arg2) { cout << "Lambda(" << arg1 << ", " << arg2 << ") " << endl; }; 
    t2.Event2.Bind(&Test::foo1, &t1); 
    t2.Event2.Unbind(&Test::foo1, &t1); 
    function<void(int, double)> stdfunction = [](int arg1, double arg2) { cout << "stdfunction(" << arg1 << ", " << arg2 << ") " << endl; }; 
    t2.Event2 += stdfunction; 
    t2.Event2 -= stdfunction; 
    t2.foo2(); 
    t2.Event2 = nullptr; 
} 
Cuestiones relacionadas