0

I'm trying to create a generalized a message handling a my code. Each message is identified by a integer id. Since all message handlers have similar deceleration and I like to handle each message quickly, I use a std::map to connect and find corresponding message handler for specific message ids. Then I call this handler and pass message to it. There are several was to do this and here is an example:

const std::map<int, void(*)(void*)> g_handlers = {
    {1, h1},
    {2, h2}
};

...
// message
int message_id = 2;
int data = 3;
// handle message
g_handlers[message_id](&data);

But there are few big limitation for this method:

  1. Since there are different messages, we need to generalize them by passing them as void* parameter. In this way, every message handler syntax will be void (*)(void*) and then we will be able to use it as value of map.
  2. There is no type checking for this message. If someone incorrectly add message handler of message id 1 for message id 2, we may not find this bug quickly.

I wanted to try something new, so I was trying to find a way to solve these problems and I have finally reached a working code. Here is the code:

class handler_base {
    public:
    template <typename U>
    void operator()(U* arg) {
        run(arg, typeid(U));
    }

    private:
    virtual void run(void* arg, const std::type_info& info) {}
};

template<typename T>
class handler : public handler_base {
    public:
    using type = T;
    handler(void (*f)(T*)) :func(f) {
    }

    private:
    void run(void* arg, const std::type_info& info) {
        assert(info.hash_code() == typeid(T).hash_code());
        func(static_cast<T*>(arg));
    }
    void (*func)(T*);
};

int main()
{
    // 2 different types of handlers
    handler h1(+[](double* v){ std::cout << "double called " << *v << "\n"; });
    handler h2(+[](int* v){ std::cout << "int called " << *v << "\n"; });

    const std::map<int, handler_base&> myhandler = {
        {1, h1},
        {2, h2}
    };

    double d = 1.5;
    int i = 3;

    myhandler.at(1)(&d);
    //myhandler.at(1)(&i);  // Error: failed assert due to type check
    //myhandler.at(2)(&d); // Error: failed assert due to type check
    myhandler.at(2)(&i);  
}

Now here are my question:

  1. Is using & as map value valid when map is const? I know it is not when map itself is not const but I wonder if it correct in this case or not.
  2. Is there any way simpler way to do this? providing different callback message handler syntax using same container with type checking?
  3. What do you think about this idea generally? Is it a good idea to add this complexity for type checking and heterogeneous callbacks? I personally always go for this rule of "simplicity is the best" and I normally select first approach (using generalized void(*)(void*) for callback), but I like to know what do you think about it.
xskxzr
  • 12,442
  • 12
  • 37
  • 77
Afshin
  • 8,839
  • 1
  • 18
  • 53
  • Have you looked at [`std::function`](https://en.cppreference.com/w/cpp/utility/functional/function)? – François Andrieux Mar 06 '20 at 15:51
  • @FrançoisAndrieux for `std::function` I will have a fixed parameter type. I cannot have different callbacks with different parameter types which is main problem. my 2nd approach handles this problem too. – Afshin Mar 06 '20 at 15:53
  • 1
    [Does this help](https://stackoverflow.com/questions/55930796/writing-a-generic-traverse-function-that-allows-the-flexibility-of-dealing-with/55930937#55930937)? Use `operator()` and move your parameters to members of the class instead of specifying those parameters in the call. – PaulMcKenzie Mar 06 '20 at 15:57
  • @Afshin Why are you using `std::map`? Do you expect the ids to not be consecutive? – walnut Mar 06 '20 at 15:57
  • @walnut yes. I get id from message itself and find correct message handler from `map`. – Afshin Mar 06 '20 at 15:58
  • @Afshin How do you determine the type to pass to the function at the call site if the values are runtime-dependent? – walnut Mar 06 '20 at 15:59
  • 1
    A heterogeneous container of callbacks has the same basic inherent problem as a heterogeneous container of anything else. You never know (statically, at any rate) what type of element you've got. It isn't quote clear how you plan `myhandler.at(1)(&d)` to work and `myhandler.at(2)(&d)` to fail *statically*. C++ doesn't work this way. – n. m. could be an AI Mar 06 '20 at 16:00
  • @walnut assume every message has a `int gettype()` method(polymorphic). I call that method to get type and them use map to find correct handler. – Afshin Mar 06 '20 at 16:01
  • @n.'pronouns'm. I never asked for statically. but at least this `assert` will show where the problem is with a run time type checking. – Afshin Mar 06 '20 at 16:03
  • what message you receive, how you parse them, *why they are typed*. IMHO It looks like you probably doing this at wrong level. – apple apple Mar 06 '20 at 16:04
  • Use a `std::variant` of various `std::function`s. A bit cumbersome to call, but preserves type-safety. – Sam Varshavchik Mar 06 '20 at 16:04
  • 1
    Boost [Signals2](https://www.boost.org/doc/libs/1_72_0/doc/html/signals2.html) is for multiple target callbacks, such as broadcasting events to listeners. However, if you are looking at inventing a very generalized solution, then this may be a more narrow domain than what you want. – Eljay Mar 06 '20 at 16:27

1 Answers1

2

I think you can completely skip the base class. You just store the function pointer directly as some function pointer for the round trip conversion. I also made it accept many parameters:

#include <unordered_map>
#include <iostream>
#include <cassert>

struct Handler
{
    template <typename T>
    Handler(T fn)
        : f((void(*)())(fn))
        , info(typeid(T))
    {
    }

    template <typename... Args>
    void operator()(Args&&... args)
    {
        using Fn = void(Args...);
        assert(info.hash_code() == typeid(Fn*).hash_code());
        return ((Fn*)(f))(std::forward<Args>(args)...);
    }
    void (*f)();
    const std::type_info& info;
};


int main()
{
    std::unordered_map<int, Handler> cbmap;
    cbmap.emplace(1, +[](int a, double b){std::cout << "1" << a << " " << b << "\n";});
    cbmap.emplace(2, +[](double a){std::cout << "2" << a << "\n";});
    cbmap.emplace(3, +[](double& a){std::cout << "3 " << a << "\n";});

    double x = 42.0;

    cbmap.at(1)(42,4.2);
    cbmap.at(2)(4.2);
    cbmap.at(3)(x);
}
Khal Buyo
  • 307
  • 1
  • 10
  • I'm not sure that cast from parametered function pointer to a function pointer with no parameter will be vaid. but someone more experienced should say if it is valid or not. Rather than that, this approach is cool :D – Afshin Mar 06 '20 at 18:35
  • I checked and it seems conversion should be valid. – Afshin Mar 06 '20 at 21:02