6

I'm trying to create C# event in c++ for my game engine. I'm implementing the event system now but I don't know how to remove a std::function in a vector. Am I using the correct list?

I'm quite new in C++ but I'm a C# programmer for 10 years now. Is this possible in C++?

#include <algorithm>
#include <functional>
#include <iostream>
#include <vector>

struct Delegate {
    std::vector<std::function<void()>> funcs;

    template<class T> void operator+=(T mFunc)
    {
        funcs.push_back(mFunc);
    }

    template<class T> void operator-=(T mFunc)
    {
        // How?
        //funcs.erase(std::remove(funcs.begin(), funcs.end(), mFunc), funcs.end());
    }

    void operator()() {
        for (auto& f : funcs) f();
    }
};

void fun1()
{
    std::cout << "hello, ";
}

void fun2()
{
    std::cout << "Delete";
}

void fun3()
{
    std::cout << "world!" << std::endl;
}

int main() {
    Delegate delegate;
    delegate += fun1;
    delegate += fun2;
    delegate -= fun2;
    delegate += fun3;
    delegate();
}
Ðаn
  • 10,934
  • 11
  • 59
  • 95
Hey
  • 167
  • 1
  • 10
  • 1
    Is there something wrong with the code you have commented out? Did you get a compile error or warning? – Fred Larson Feb 13 '20 at 16:07
  • 3
    Seems to me the commented out code won't compile since `std::function` objects are not comparable (see e.g. [cppreference.com](https://en.cppreference.com/w/cpp/utility/functional/function/operator_cmp)). So you'll have to keep track of these functions you insierted some other way, e.g. using an index. – AVH Feb 13 '20 at 16:09
  • 1
    `std::function`s cannot be meaningfully compared. This means you cannot search for a particular one, in any container. You'll have to redesign this whole thing, and track things in some different way. – Sam Varshavchik Feb 13 '20 at 16:09
  • I see a vector of functions and no void pointers. – molbdnilo Feb 13 '20 at 16:11
  • 2
    Since you are mimicking how C# does events... before iterating over the funcs, make a copy of funcs first and iterate over that, which is what C# does. (Because a delegate may modify funcs, which would be bad.) C# add/remove uses reflection to inspect the delegate, C++ can't do that: you'll need some sort of token to associate with and identify the delegate (like a coat check token). Alternatively, Boost [Signals2](https://www.boost.org/doc/libs/1_72_0/doc/html/signals2.html) does this if you don't want to reinvent the wheel. – Eljay Feb 13 '20 at 16:14

3 Answers3

4

If you are willing to limit Delegate to only using function pointers the you can do it with what you have. That would look like

struct Delegate {
    std::vector<void(*)()> funcs;

    template<class T> void operator+=(T mFunc)
    {
        funcs.push_back(mFunc);
    }

    template<class T> void operator-=(T mFunc)
    {
        funcs.erase(std::remove(funcs.begin(), funcs.end(), mFunc), funcs.end());
    }

    void operator()() {
        for (auto& f : funcs) f();
    }
};

If you don't want to do so, then you need to change you approach. You could have operator += return an index to the inserted function, and then you can change operator -= to take that index and remove that element. see eerorika's answer for a suggestion on how to return iterators to the functions.

NathanOliver
  • 171,901
  • 28
  • 288
  • 402
  • 1
    Thank you. This is all I need. I'm just studying C++ again and I dunno why my post get's down voted for something about void*. Maybe my English is just really bad so everyone hates it. Anyway, thank you again. – Hey Feb 13 '20 at 16:21
  • Index suggestion would be quite tricky since if you erase one index, the index of all successive elements will change. – eerorika Feb 13 '20 at 16:21
  • @eerorika Yeah. I like your list approach better. That way you have stable iterators. – NathanOliver Feb 13 '20 at 16:22
  • @Hey No problem. My guess saying `void*` but not having one was where those votes came in. Unfortunately some people do bother actually trying t understand the Q. – NathanOliver Feb 13 '20 at 16:23
3

Is this possible in C++?

Not like this. Function wrappers cannot be compared for equality. This is a limitation in their design.

One option is to use function pointers. They can be compared for equality. But then you cannot use stateful function objects. NathanOliver shows an example of this.

Another alternative design would be to use a std::list as the container, and when ever you register a function, return iterator to it. Then, instead of erasing by passing the function, you can pass the iterator to be erased.

eerorika
  • 232,697
  • 12
  • 197
  • 326
2

std::function objects are not directly comparable, but if you are only using regular functions (not e.g. member functions or capturing lambdas), you can use target() method to extract underlying pointer.

void operator-=(void(*mFunc)())
{
    auto pred = [&mFunc](const std::function<void()>& func) { return mFunc == *func.target<decltype(mFunc)>(); };
    funcs.erase(std::remove_if(funcs.begin(), funcs.end(), pred), funcs.end());
}

I changed the type T to be function pointer explicitly, because std::function would need a different approach (calling target() on that too). You can overload your operator -= to handle std::function separately.

It's ugly, but it works.


Note: Above snippet doesn't really take into account type safety. target() will return nullptr if type declared by template does not match the actual type stored by std::function, and dereferencing nullptr will be disastrous. Since your code only seems to deal with free functions of signature void(), it shouldn't be much issue, but if you plan to use lambdas or something it may break.

Yksisarvinen
  • 18,008
  • 2
  • 24
  • 52
  • 1
    I liked this approach (so +1) but I was late so I'll piggy-back my version of it here: https://godbolt.org/z/pb7gwZ – Ted Lyngmo Feb 13 '20 at 16:51