C#-Like Delegates in C++
Asked Answered
B

6

9

I have done a bit of research on the matter but have not come to a concrete solution. I would really like to be able to do this:

    public delegate void VoidFloatCallback(float elapsedTime);
    public VoidFloatCallback OnEveryUpdate;
    public VoidFloatCallback OnNextUpdate;

    public virtual void Update(GameTime gameTime)
    {
        if (OnNextUpdate != null)
        {
            OnNextUpdate(gameTime);
            OnNextUpdate = null;
        }

        if (OnEveryUpdate != null)
        {
            OnEveryUpdate(gameTime);
        }

        this.OnUpdate(gameTime);
    }

But in C++ of course. I have found only one solution that provides me with such a feature; but has since been taken offline but I reposted it here http://codepad.org/WIVvFHv0. The only issue with the solution I have found is that it isn't modern C++11 code and lacks lambda support.

I know that I can use

    std::function

but the only issue with that is it does not support the operators "+=, -=, ==". Now I have thought about making my own Events class and having a

    vector<std::function>

with some templating but I found out that std::function does not implement the operator == so I couldn't make it look and feel like C# does.

Anyway, my question is this:

I would to know how I can implement such an event system using C++11 -- or if it is even possible. Or even if you know of a better/proper way to implement Callbacks that support multiple listeners (I'd like to avoid a full blown Observer Pattern implementation if at all possible.)

Update #1

My intentions for the operators were this:

    void some_func(float f) { /** do something with f **/ }
    void some_other_func(float f) { /** do something else with f **/ }
    OnNextUpdate += some_func();
    OnNextUpdate += some_other_func();

    OnNextUpdate(5.0f);
    // both some_func() and some_other_func() are called

    OnNextUpdate -= some_other_func();
    OnNextUpdate(5.0f);
    // only some_func() is called
Boschvark answered 31/5, 2014 at 19:17 Comment(17)
What exactly are you trying to do with += ?Josie
@Josie I updated my original question to include a better example.Boschvark
Just to clarify, you want this in native C++, not C++/CLI? For the latter, a discussion at #13072253Avifauna
The specific feature of .NET delegates you are asking about is called multicast, and there's an existing question about doing that in C++.Avifauna
Correct I'd like to use only native C++Boschvark
"I found out that std::function does not implement the operator "==" so I couldn't make it look and feel like C# does." Well C++ isn't C#, so making them look and feel the same is a terrible goal.Dorrie
Or even if you know of a better/proper way to implement Callbacks I was also looking for better solutions to the problem at hand. How about giving better advice rather than demeaning one's questions.Boschvark
Duplicare suggestions do not contain an answer to the asked question, which includes -= support, at least among the 3-4 answers I read.Gallicism
@Yakk: It provided an effective alternate, removing by token (the saved iterator) instead of by creating a new functor object. Which is additionally the right way to do it, because unlike the .NET approach, it can distinguish duplicate calls to the same target.Avifauna
Let's keep this duplicate question linked, even if OP can't use its answers: stackoverflow.com/questions/7887582Avifauna
@Mister: Is Alexandre's answer in the linked question, which removes based on a callback_id, suitable for your purposes?Avifauna
I think I would just use the observer pattern here, much more readable than the delegate template answerJosie
@paulm: note, however, that the template code is written just once. From then on you'd only use the code and I think the use is quite idiomatic.Bussy
I have to agree with @DietmarKühl , which is why I noted that I wanted to avoid having the Observer Pattern implemented. @BenVoigt -- Alexandre's answer was what I was thinking about doing from the start but I thought that you couldn't do some_list.erase() on a vector of functions -- does it not compare at that point and I thought you couldn't compare std::functionBoschvark
I guess, it just looks wrong in C++ though, the vector of functions makes more sense to meJosie
@Mister: the approach taken by Alexandre's answer is to return an ID when the function is being registered and to use this ID rather than the function itself to identify which function needs to be removed. The approach providing an ID works for all functions, even if they can't be compared as it only requires the IDs to be comparable.Bussy
It sounds like the Boost Signals library does exactly what you want.Eupheemia
R
21

The C++ function object mechanism is quite different from the C# approach. In particular, function objects are based on values rather than on references. The reason function objects can be identified when removing them in C++ is that the function object have an identity, i.e., the object they are called on and the member function being called. Also, in C++ it isn't possible to directly take the address of an object and a member function at once.

To make a system of delegates work which allows removal of functions, you could create something similar to std::function<Signature> but using multiple functions and requiring that each of the used functions is EqualityComparable. Below is a simple implementation of such a delegate system together with an example implementation how a binder for member functions could look like. There are many obvious extension opportunities as this implementation is only intended as a demo.

#include <algorithm>
#include <iostream>
#include <memory>
#include <utility>
#include <vector>

template <typename Signature>
struct delegate;

template <typename... Args>
struct delegate<void(Args...)>
{
    struct base {
        virtual ~base() {}
        virtual bool do_cmp(base* other) = 0;
        virtual void do_call(Args... args) = 0;
    };
    template <typename T>
    struct call: base {
        T d_callback;
        template <typename S>
        call(S&& callback): d_callback(std::forward<S>(callback)) {}

        bool do_cmp(base* other) {
            call<T>* tmp = dynamic_cast<call<T>*>(other);
            return tmp && this->d_callback == tmp->d_callback;
        }
        void do_call(Args... args) {
            return this->d_callback(std::forward<Args>(args)...);
        }
    };
    std::vector<std::unique_ptr<base>> d_callbacks;

    delegate(delegate const&) = delete;
    void operator=(delegate const&) = delete;
public:
    delegate() {}
    template <typename T>
    delegate& operator+= (T&& callback) {
        this->d_callbacks.emplace_back(new call<T>(std::forward<T>(callback)));
        return *this;
    }
    template <typename T>
    delegate& operator-= (T&& callback) {
        call<T> tmp(std::forward<T>(callback));
        auto it = std::remove_if(this->d_callbacks.begin(),
                                 this->d_callbacks.end(),
                                 [&](std::unique_ptr<base>& other) {
                                     return tmp.do_cmp(other.get());
                                 });
        this->d_callbacks.erase(it, this->d_callbacks.end());
        return *this;
    }

    void operator()(Args... args) {
        for (auto& callback: this->d_callbacks) {
            callback->do_call(args...);
        }
    }
};

// ----------------------------------------------------------------------------

template <typename RC, typename Class, typename... Args>
class member_call {
    Class* d_object;
    RC (Class::*d_member)(Args...);
public:
    member_call(Class* object, RC (Class::*member)(Args...))
        : d_object(object)
        , d_member(member) {
    }
    RC operator()(Args... args) {
        return (this->d_object->*this->d_member)(std::forward<Args>(args)...);
    }
    bool operator== (member_call const& other) const {
        return this->d_object == other.d_object
            && this->d_member == other.d_member;
    }
    bool operator!= (member_call const& other) const {
        return !(*this == other);
    }
};

template <typename RC, typename Class, typename... Args>
member_call<RC, Class, Args...> mem_call(Class& object,
                                         RC     (Class::*member)(Args...)) {
    return member_call<RC, Class, Args...>(&object, member);
}

// ----------------------------------------------------------------------------

void f(char const* str) { std::cout << "f(" << str << ")\n"; }
void g(char const* str) { std::cout << "g(" << str << ")\n"; }
void h(char const* str) { std::cout << "h(" << str << ")\n"; }

// ----------------------------------------------------------------------------

struct foo
{
    int d_id;
    explicit foo(int id): d_id(id) {}
    void bar(char const* str) {
        std::cout << "foo(" << this->d_id << ")::bar(" << str << ")\n";
    }
    void cbs(char const* str) {
        std::cout << "foo(" << this->d_id << ")::cbs(" << str << ")\n";
    }
};

// ----------------------------------------------------------------------------

int main()
{
    delegate<void(char const*)> d0;

    foo f0(0);
    foo f1(1);

    d0 += f;
    d0 += g;
    d0 += g;
    d0 += h;
    d0 += mem_call(f0, &foo::bar);
    d0 += mem_call(f0, &foo::cbs);
    d0 += mem_call(f1, &foo::bar);
    d0 += mem_call(f1, &foo::cbs);
    d0("first call");
    d0 -= g;
    d0 -= mem_call(f0, &foo::cbs);
    d0 -= mem_call(f1, &foo::bar);
    d0("second call");
}
Rile answered 31/5, 2014 at 21:6 Comment(10)
remove_if is wrong here, the behavior being requested removes zero or one matches, never multipleAvifauna
@BenVoigt: I'm not a C# programmer and don't know the exact semantics of the operation. If the operation should remove at most one function, using a combination of std::find_if() and a conditional d_callbacks.erase(it) would address that.Bussy
I'd like to say that this is the right answer lol -- judging purely off of how everything looks I would say yes. I just realized though... VS2012 doesn't fully support C++11... Gotta find an alternative for now. I will test it in a bit and let you know how it goesBoschvark
any particular reason why it passes here: ideone.com/G4Yybr but fails in VS2013 Express Windows Desktop?Boschvark
@Mister: I can't comment on VC++: I'm a UNIX guy. I tried the code with a version of clang and would think it also compiles with gcc (actually, it is missing an include for <algorithm> which I'll correct).Bussy
@DietmarKühl -- my example worked only if I put it in a struct and used your mem_call; either way I like this implementation and I appreciate your time and effort. Also, as an off topic question -- which IDE for C\C++ do you use?Boschvark
@Mister: it is weird that the code wouldn't work for function pointers. These tend to be simpler. Try using &f instead of f (although a function should decay into a function pointer). Re IDE: I use emacs (in viper mode: my fingers want to use vi but I like some of the emacs features) together with make and a shell. I guess, that's a bit niche nowadays (well, emacs in viper-mode seems to be niche already).Bussy
@DietmarKühl: Ok so changing it to &foobar fixed the issue. It is strange though. Ah well it seems to be in working order now. Last question for you -- is this something that you would do/use in your own project?Boschvark
@Mister: I'm using a class template quite similar to this in some places. It only support adding functions, though. I guess, I would enhance the implementation to yield an ID and also support removal of registered functions via this idea. The comparison based on function is fairly heavy: it requires construction of a function object and several dynamic_cast<>()s. It also doesn't work with typical function objects like lambdas, bound function, and std::function<...>. Thus, I would also consider supporting function objects which can't be compared which is relatively easy with C++11.Bussy
This worked very well in C++20 and even accepted lambdas.Wieland
J
2

What about using the observer pattern instead?

class IVoidFloatCallback
{
public:
   virtual ~IVoidFloatCallback() { }
   virtual void VoidFloatCallback(float elapsedTime) = 0;
};

class Game
{
public:
    std::vector<IVoidFloatCallback*> mOnEveryUpdate;
    std::vector<IVoidFloatCallback*> mOnNextUpdate;

    void Update(float gameTime)
    {
       for ( auto& update : mOnNextUpdate )
       {
          update->VoidFloatCallback(gameTime);
       }
       mOnNextUpdate.clear();


       for ( auto& update : mOnEveryUpdate )
       {
           update->VoidFloatCallback(gameTime);
       }

       OnUpdate(gameTime);
    }
};

class UpdateMe : public IVoidFloatCallback
{
public:
   virtual void VoidFloatCallback(float elapsedTime) final
   {
     // Do something
   }
};

void InitGame()
{
    Game g;
    UpdateMe someThing;
    g.mOnEveryUpdate.push_back(&someThing);
    g.Update(1.0f);
}

I think trying to make C++ look like C# isn't really the "thing" to do since it is pretty different. I'd take a look at the linked question about multicast too.

Josie answered 31/5, 2014 at 23:22 Comment(3)
I like the example but I did note that I wanted to avoid using the observer pattern. Ty though.Boschvark
It might help others that face the same issue and think this is better than the template based answerJosie
I do appreciate the example and your thoughts but the observer pattern is too tedious for me; especially when I want to quickly use a callback. I'd rather not go the way of Java and have Listeners all over the place -- just my way though.Boschvark
C
2

What about boost.signals2?

boost::signals2::signal<void (float)> onEveryUpdate;
boost::signals2::signal<void (float)> onNextUpdate;

virtual void Update(float gameTime)
{
    onNextUpdate(gameTime);
    onNextUpdate.disconnect_all_slots();
    onEveryUpdate(gameTime);
} 

The signal's connect function is basically what you mean by +=.

Codel answered 1/6, 2014 at 18:14 Comment(3)
I am looking into Boost.signals2 as well; I'm thinking of wrapping it in a template style manner like Dietmar's example -- as thats the style I am after.Boschvark
Why do you want to wrap it? The only difference is you need to use connect() instead of += and disconnect() instead of -=... Did I miss something?Codel
It is what I am after but I'd just prefer to use it in the style I am used to is all. This is what I ended up coming up with pastebin.com/HWRnqsf5Boschvark
C
1

There are numerous libraries out there providing this kind of thing. Some call operator+= for delegates something like "connect" or "subscribe". Examples are boost.signal2, poco AbstractEvent, libsigc++ or if you are using a GUI Qt's slot/signal (or if you are using gtk in c++, c++-gtk-utils Emitters).

Conventionality answered 1/6, 2014 at 21:13 Comment(0)
G
0

The Poco Library has support for delegates like this for example:

#include <iostream>
#include "Poco/Delegate.h"
#include "Poco/BasicEvent.h"

using namespace Poco;

struct Source {
    BasicEvent<int> theEvent;

    void fireEvent(int n) { theEvent(this, n); }
};

struct Target1 {
    void onEvent(const void * /*sender*/, int &arg) {
        std::cout << "onEvent from Target1: " << arg << std::endl;
    }
};

struct Target2 {
    void onEvent(const void * /*sender*/, int &arg) {
        std::cout << "onEvent from Target2: " << arg << std::endl;
    }
};

int main() {
    Source source;
    Target1 target1;
    Target2 target2;

    source.theEvent += delegate(&target1, &Target1::onEvent);
    source.theEvent += delegate(&target2, &Target2::onEvent);

    source.fireEvent(42);

    source.theEvent -= delegate(&target2, &Target2::onEvent);

    source.fireEvent(24);

    return 0;
}

Output:

onEvent from Target1: 42

onEvent from Target2: 42

onEvent from Target1: 24

Library wise, I would also recommend taking a look at Boost.Signals2.

Gwin answered 1/6, 2014 at 7:5 Comment(2)
Does the POCO Library's Basic event support lambdas and basic function pointers rather than member functions?Boschvark
@Boschvark I'm not sure, I just recalled this library used the same += syntax as C# but I believe it's not up to date with modern C++ so... I would recommed to take a look at Boost.Signals2, it would provide the same functionality I think, and support lambdas, just not the same syntax.Gwin
C
0

Disclaimer

I've been using C# for quite a while, but I'm still kinda new to C++ so take my answer with a grain of salt.


You can achieve something similar with a very simple implementation

#include <algorithm>
#include <vector>

template <typename T, typename... Args>
class Delegate final {
private:
    using Invokable = T(*)(Args...);
    std::vector<Invokable> _functions;

public:
    void operator+=(const Invokable func) {
        _functions.push_back(func);
    }

    void operator-=(const Invokable func) {
        auto it{ std::remove_if(_functions.begin(), _functions.end(),
                    [&func](const Invokable& f) { return f == func; })};

        _functions.erase(it, _functions.end());
    }

    void operator()(Args&&... args) const {
        for (const Invokable& func : _functions) {
            (*func)(std::forward<Args>(args)...);
        }
    }
};

Then you can add or remove as many functions as you want, as long as they match the delegate's signature. Here's some sample code.

#include <iostream>

void Foo(int val) { std::cout << "Foo function called with " << val << "\n"; }
void Bar(int val) { std::cout << "Bar function called with " << val << "\n"; }

int main() {
    Delegate<void, int> delegate;

    delegate += Foo;
    delegate += Bar;
    delegate(10);

    delegate -= Foo;
    delegate(20);

    return 0;
}

It will print

Foo function called with 10
Bar function called with 10
Bar function called with 20

Beware that my code is very barebone; you can extend this class to add more functionality.

Counterpart answered 15/7 at 15:28 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.