Можно ли реализовать события в C ++? - PullRequest
14 голосов
/ 05 марта 2011

Я хотел реализовать событие C # в C ++, чтобы посмотреть, смогу ли я это сделать. Я застрял, я знаю, что дно не так, но я понимаю, что моя самая большая проблема ...

Как перегрузить оператор (), чтобы он находился в T, в данном случае int func(float)? Я не могу? Могу я? Могу ли я реализовать хорошую альтернативу?

#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;
}

Ответы [ 4 ]

23 голосов
/ 05 марта 2011

Если вы можете использовать Boost, рассмотрите возможность использования Boost.Signals2 , который обеспечивает функциональность сигналов-слотов / событий / наблюдателей.Это простой и удобный в использовании и довольно гибкий.Boost.Signals2 также позволяет регистрировать произвольно вызываемые объекты (например, функторы или функции связанного члена), что делает его более гибким и обладает множеством функций, помогающих правильно управлять временем жизни объектов.


ЕслиВы пытаетесь реализовать это самостоятельно, вы на правильном пути.Однако у вас есть проблема: что именно вы хотите делать со значениями, возвращаемыми каждой из зарегистрированных функций?Вы можете вернуть только одно значение из operator(), поэтому вам нужно решить, хотите ли вы ничего не возвращать, или один из результатов, или каким-либо образом агрегировать результаты.

Предполагая, что мы хотим игнорировать результаты, этореализовать это довольно просто, но немного проще, если вы берете каждый из типов параметров в качестве отдельного параметра типа шаблона (в качестве альтернативы вы можете использовать что-то вроде Boost.TypeTraits, что позволяет легко разбирать тип функции):

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);
    }
};

Для этого требуется, чтобы зарегистрированная функция имела тип возврата void.Чтобы иметь возможность принимать функции с любым типом возврата, вы можете изменить FuncPtr на

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

(или использовать boost::function или std::tr1::function, если у вас нет версии C ++ 0xимеется в наличии).Если вы хотите что-то сделать с возвращаемыми значениями, вы можете взять возвращаемый тип в качестве другого параметра шаблона для MyEvent.Это должно быть относительно просто сделать.

С вышеупомянутой реализацией должно работать следующее:

void test(float) { }

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

Другой подход, который позволяет вам поддерживать различные наборы событий, будетиспользовать один тип параметра для типа функции и иметь несколько перегруженных operator() перегрузок, каждая из которых принимает различное количество аргументов.Эти перегрузки должны быть шаблонами, в противном случае вы получите ошибки компиляции для любой перегрузки, не соответствующей фактической арности события.Вот работающий пример:

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);
    }
};  

(я использовал здесь std::add_pointer из C ++ 0x, но этот модификатор типа также можно найти в Boost и C ++ TR1; он просто делает его немного чищеиспользовать шаблон функции, так как вы можете использовать тип функции напрямую; вам не нужно использовать тип указателя на функцию.) Вот пример использования:

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);
}
3 голосов
/ 05 марта 2011

Вы абсолютно можете. Джеймс МакНеллис уже связался с полным решением, но для вашего игрушечного примера мы можем сделать следующее:

#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);
}

Здесь я использовал частичную специализацию, чтобы разделить компоненты типа указателя функции, чтобы обнаружить тип аргумента. boost.signals делает это и многое другое, используя такие функции, как стирание типов и черты, чтобы определить эту информацию для вызываемых объектов с нефункциональным указателем.

Для N аргументов есть два подхода. «Простой» способ, который был добавлен для C ++ 0x, заключается в использовании шаблонов с переменным числом аргументов и нескольких других функций. Однако мы занимались этим еще до того, как эти функции были добавлены, и я не знаю, какие компиляторы, если таковые имеются Поддержка шаблонов с переменными параметрами пока что, поэтому мы можем сделать это трудным путем, то есть снова специализироваться:

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); }
   ...
};

Это, конечно, утомительно, поэтому есть такие библиотеки, как boost.signals, которые уже справились (и используют макросы и т. Д., Чтобы облегчить некоторую утомительную работу).

Чтобы использовать синтаксис в стиле MyEvent<int, int>, вы можете использовать метод, подобный следующему

 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;
 }

Тип NullEvent используется в качестве заполнителя, и мы снова используем частичную специализацию для определения арности.

1 голос
/ 14 мая 2017

РЕДАКТИРОВАТЬ : Добавлена ​​поточно-ориентированная реализация, основанная на этом ответе.Множество исправлений и улучшений производительности

Это моя версия, улучшающая версию Джеймса МакНеллиса, добавив: operator-=, шаблон с переменным числом аргументов для поддержки любого количества хранимых вызываемых объектов, удобные Bind(func, object) и Unbind(func, object) методылегко связывать объекты и функции-члены экземпляра, операторы присваивания и сравнение с nullptr.Я отказался от использования std::add_pointer, чтобы просто использовать std::function, который в моих попытках более гибок (принимает как lambdas, так и std :: function).Также я перешел на использование std::vector для более быстрой итерации и удалил возвращаемый *this в операторах, так как в любом случае он выглядит не очень безопасным / полезным.Все еще отсутствует в семантике C #: события C # не могут быть очищены извне класса, в котором они объявлены (было бы легко добавить это дружбой состояний к шаблонизированному типу).

Это следует за кодом, обратная связьдобро пожаловать:

#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...);
    }
};

Я проверил это с помощью:

#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;
}
1 голос
/ 05 марта 2011

Это возможно, но не с вашим текущим дизайном.Проблема заключается в том, что подпись функции обратного вызова заблокирована в аргументе вашего шаблона.Я не думаю, что вы должны пытаться поддерживать это в любом случае, все обратные вызовы в одном и том же списке должны иметь одинаковую подпись, не так ли?

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...