15

I wanted to implement a C# event in C++ just to see if I could do it. I got stuck, I know the bottom is wrong but what I realize my biggest problem is...

How do I overload the () operator to be whatever is in T, in this case int func(float)? I can't? Can I? Can I implement a good alternative?

#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;
}
Null
  • 1,950
  • 9
  • 30
  • 33
  • Please don't overload `operator+=` like C# does, I find that highly confusing. – fredoverflow Mar 05 '11 at 15:02
  • 2
    See http://www.codeproject.com/KB/cpp/ImpossiblyFastCppDelegate.aspx – Sergei Tachenov Mar 05 '11 at 15:03
  • 1
    One of your problem is that C++ doesn't directly support delegates, which are a central part of C# style events. http://www.codeproject.com/KB/cpp/FastDelegate.aspx Implements Delegates in a way that's non standard, but supported by most compilers. – CodesInChaos Mar 05 '11 at 15:05
  • @Sergey Thanks for posting that follow up article to the one I knew. – CodesInChaos Mar 05 '11 at 15:06
  • 1
    @FredOverflow: Why is `+=` confusing? I never had a problem with that however I almost never use events –  Sep 09 '12 at 11:38
  • Overloading operators should be highly justified. It's useful when implementing arithmetic operations in your own class or writing your own streams. Using operator just because it's your whim is often worse idea than creating a named method. – cubuspl42 Nov 27 '12 at 11:35

4 Answers4

24

If you can use Boost, consider using Boost.Signals2, which provides signals-slots/events/observers functionality. It's straightforward and easy to use and is quite flexible. Boost.Signals2 also allows you to register arbitrary callable objects (like functors or bound member functions), so it's more flexible, and it has a lot of functionality to help you manage object lifetimes correctly.


If you are trying to implement it yourself, you are on the right track. You have a problem, though: what, exactly, do you want to do with the values returned from each of the registered functions? You can only return one value from operator(), so you have to decide whether you want to return nothing, or one of the results, or somehow aggregate the results.

Assuming we want to ignore the results, it's quite straightforward to implement this, but it's a bit easier if you take each of the parameter types as a separate template type parameter (alternatively, you could use something like Boost.TypeTraits, which allows you to easily dissect a function type):

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

This requires the registered function to have a void return type. To be able to accept functions with any return type, you can change FuncPtr to be

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

(or use boost::function or std::tr1::function if you don't have the C++0x version available). If you do want to do something with the return values, you can take the return type as another template parameter to MyEvent. That should be relatively straightforward to do.

With the above implementation, the following should work:

void test(float) { }

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

Another approach, which allows you to support different arities of events, would be to use a single type parameter for the function type and have several overloaded operator() overloads, each taking a different number of arguments. These overloads have to be templates, otherwise you'll get compilation errors for any overload not matching the actual arity of the event. Here's a workable example:

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

(I've used std::add_pointer from C++0x here, but this type modifier can also be found in Boost and C++ TR1; it just makes it a little cleaner to use the function template since you can use a function type directly; you don't have to use a function pointer type.) Here's a usage example:

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);
}
James McNellis
  • 348,265
  • 75
  • 913
  • 977
  • I think this is a question about learning *how* to implement that type of callback system. – André Caron Mar 05 '11 at 15:04
  • Excellent answer. I tried copy/pasting the function but using 2 templates and it didnt like the fact that two classes with the same name are using different templates amount. I was thinking about it but theres no way to make it accept MyEvent, MyEvent by predefining the amount of templates? Like right now i have MyEvent0-9 (i wont need more then 9 parameters....... maybe i should make a void* list i can pass in as well). Is using MyEvent2 the solution and its impossible to use MyEvent AND MyEvent? –  Mar 05 '11 at 15:36
  • @acidzombie24: See the updated edit: you can go back to using a single type parameter and use several operator templates to handle different arities of events. – James McNellis Mar 05 '11 at 15:38
  • I am very impressed. So it seems to me operator()(TArg0 a1) is not evaluated unless used so it doesnt matter if the the code uses incorrect parameter amount? or is there some other reason behind it. I dont remember ever seeing void(float, float) but NICE!. I am so glad C++ can do this. –  Mar 05 '11 at 15:47
  • @acidzombie24: Right: member function templates aren't instantiated unless they are used. If you tried to use the one taking two arguments but `TFunc` was a function type that only took one argument, you'd get an error. The Boost.Signals2 library basically encapsulates all of this functionality. – James McNellis Mar 05 '11 at 15:55
  • I have one more question. When i compile this in Visual Studios 2010 its fine and i get no warnings. When i run the example code i get a `Unhandled exception at 0x000580e9 in cEventTest.exe: 0xC0000005: Access violation.` on `(*it)(a1);` Here is the exact code i am using. http://codepad.org/s41WyuzG –  Mar 05 '11 at 15:58
  • @acidzombie24: Oops; my apologies. I didn't consistently use `add_pointer`. See the updated code. – James McNellis Mar 05 '11 at 16:07
  • Excellent. I really like this code and enjoy learning it. I see where you made the mistake and i am pretty surprised the compiler would allow that AND CRASH. I dont see any casting so i am unsure what is wrong. But anyways obviously accepted and i'll be looking more into this code :) –  Mar 05 '11 at 16:23
  • Alright, so i understand how add_pointer works (its pretty much the simplest and most straightforward specialized template i have ever seen) and i see it adds a pointer to TFunc. But What is TFunc? I pass in this void(float). Really if you add the line `typedef typename std::add_pointer::type FuncPtr;` in your old example and replace FuncPtr in `MyEvent2& operator +=(TFunc f)` with FuncPtr it works. I'm trying to figure out why MSVC gets confuses and crashes. –  Mar 05 '11 at 16:38
  • I never seen void(int,int). Is it a function signature but byval/push through the stack? and FuncPtr makes it `void(*)(int,int)`? actually `void(int,int)*` may be more accurate? and the compiler didnt notice the difference but acted on the difference? hmm.... -edit- `void(int,int) v` and `void(int,int) *v` doesnt work. `void(*v)(int,int) ` obviously does. Writing `operator +=(TFunc* f)` fixes the problem too. So really, it looks like its an oversight on the compiler to allow that without a ptr. I wonder what happens on gcc, i really should get that installed. –  Mar 05 '11 at 16:39
  • 1
    @acidzombie24: It turns out it's a compiler bug. I'll report the bug to the Visual C++ team. Clang 2.9 and gcc 4.5 don't have any problem with it. The bug is where the function pointer is passed by reference in `push_back`. I was able to construct a minimal-repro. `void(float)` is a function type. When you have a parameter of type "function," it gets converted to type "pointer to function," so a parameter of type `void(float)` should be the exact same as a parameter of type `void(*)(float)` (sort of like how a parameter of type `int[]` is the same as one of type `int*`). – James McNellis Mar 05 '11 at 16:55
3

EDIT: Added thread safe implementation, based on this answer. Many fixes and performance improvements

This is my version, improving James McNellis' one by adding: operator-=, variadic template to support any ariety of the stored callable objects, convenience Bind(func, object) and Unbind(func, object) methods to easily bind objects and instance member functions, assignment operators and comparison with nullptr. I moved away from using std::add_pointer to just use std::function which in my attempts it's more flexible (accepts both lambdas and std::function). Also I moved to use std::vector for faster iteration and removed returning *this in the operators, since it doesn't look to be very safe/useful anyway. Still missing from C# semantics: C# events can't be cleared from outside the class where they are declared (would be easy to add this by state friendship to a templatized type).

It follows the code, feedback is welcome:

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

I tested it with this:

#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;
}
ceztko
  • 14,736
  • 5
  • 58
  • 73
  • If the above implementation is split into .h and .cpp file, the usage of 'Event Event1; Event Event2;' gives unresolved external symbol error on building. If the same definition in cpp file copied to h file, it works. Any idea what is causing this? – Anand Paul Nov 26 '19 at 11:52
  • I am not sure I understand what you are doing but the class `Event` is a class template so you can't have the definition in a cpp file. If this is what you were trying to do, just don't do it a keep all the class in the header. – ceztko Nov 26 '19 at 12:16
3

You absolutely can. James McNellis has already linked to a complete solution, but for your toy example we can do the following:

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

Here I've made use of partial specialization to tease apart the components of the function pointer type to discover the argument type. boost.signals does this and more, leveraging features such as type erasure, and traits to determine this information for non-function pointer typed callable objects.

For N arguments there are two approaches. The "easy' way, that was added for C++0x, is leveraging variadic templates and a few other features. However, we've been doing this since before that features was added, and I don't know which compilers if any, support variadic templates yet. So we can do it the hard way, which is, specialize again:

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

THis gets tedious of course which is why have libraries like boost.signals that have already banged it out (and those use macros, etc. to relieve some of the tedium).

To allow for a MyEvent<int, int> style syntax you can use a technique like the following

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

The NullEvent type is used as a placeholder and we again use partial specialization to figure out the arity.

Logan Capaldo
  • 39,555
  • 5
  • 63
  • 78
  • +1 for posting effectively the exact same thing I was working on editing into my answer. – James McNellis Mar 05 '11 at 15:20
  • The problem with everything i seen so far is not being able to specify the argument amount and type. Like right now even though arg is only one argument, what if i want two? Actually i have an idea but i dont think it will work. –  Mar 05 '11 at 15:20
  • Excellent answer. I tried copy/pasting the function but using 2 templates and it didnt like the fact that two classes with the same name are using different templates amount. I was thinking about it but theres no way to make it accept MyEvent, MyEvent by predefining the amount of templates? Like right now i have MyEvent0-9 (i wont need more then 9 parameters....... maybe i should make a void* list i can pass in as well). Is using MyEvent2 the solution and its impossible to use MyEvent AND MyEvent? –  Mar 05 '11 at 15:36
  • You and james are both complete win. Thanks for the solutions. –  Mar 05 '11 at 15:48
2

That is possible, but not with your current design. The problem lies with the fact that the callback function signature is locked into your template argument. I don't think you should try to support this anyways, all callbacks in the same list should have the same signature, don't you think?

André Caron
  • 44,541
  • 12
  • 67
  • 125