Variadic templates - Deducing member function signature?

Started by
10 comments, last by l0calh05t 9 years, 11 months ago

I'm looking for a more elegant way to write my generic listener dispatcher class. I also hope that this might be useful to others.

The core of the problem is: I want the following code to work:


int main()
{
    Foo a, b;
    GenericDispatcher<Listener> dispatcher;
    dispatcher.addListener(&a, "a");
    dispatcher.addListener(&b, "b");
    dispatcher.dispatch<&Listener::doThing>(6, 7);  // #1
    return 0;
}

I want dispatcher.dispatch<>() (#1) to be able to deduce the arguments and return type of the member function being passed as a template argument without me having to explicitly specify it.

I've had the need to write a lot of code to do with listeners with my latest project, and found I was re-writing a lot of those functions that handle registering/unregistering listeners, and dispatching events to listeners, so I had a go at a more generic approach.

Here's an example which works, but I'm forced to specify the return type and member function arguments when instantiating the dispatcher, meaning that I will only ever be able to dispatch stuff to functions with the same signature.


#include <iostream>
#include <string>
#include <map>

struct Listener
{
    virtual void doThing(int, int) = 0;
    virtual void doAnotherThing(int) = 0;
};

struct Foo : public Listener
{
    virtual void doThing(int a, int b)
    {
        std::cout << a*b << std::endl;
    }
    virtual void doAnotherThing(int a)
    {
        std::cout << a << std::endl;
    }
};

template <class LISTENER_CLASS, class RET_TYPE, class... PARAMS>
class GenericDispatcher
{
public:
    void addListener(LISTENER_CLASS* listener, std::string name)
    {
        m_Listeners[name] = listener;
    }

    template <RET_TYPE (LISTENER_CLASS::*func)(PARAMS...)>
    void dispatch(PARAMS... params)
    {
        for(auto it = m_Listeners.begin(); it != m_Listeners.end(); ++it)
            (it->second->*func)(params...);
    }
private:
    std::map<std::string, LISTENER_CLASS*> m_Listeners;
};

int main()
{
    Foo a, b;
    GenericDispatcher<Listener, void, int, int> dispatcher;  // This here sucks
    dispatcher.addListener(&a, "a");
    dispatcher.addListener(&b, "b");
    dispatcher.dispatch<&Listener::doThing>(6, 7);
    return 0;
}

What if I wanted to do this, for example?


int main()
{
    Foo a, b;
    GenericDispatcher<Listener, void, int, int> dispatcher;
    dispatcher.addListener(&a, "a");
    dispatcher.addListener(&b, "b");
    dispatcher.dispatch<&Listener::doThing>(6, 7);
    dispatcher.dispatch<&Listener::doAnotherThing>(6); // I want this to work as well
    return 0;
}

What I need is to be able to turn this:


GenericDispatcher<Listener, void, int, int> dispatcher;

int this:


GenericDispatcher<Listener> dispatcher;
"I would try to find halo source code by bungie best fps engine ever created, u see why call of duty loses speed due to its detail." -- GettingNifty
Advertisement

If you want the dispatcher to be able to call different functions with different signatures, then move the "class ... PARAMS" to the "dispatch" functions, and don't specifiy it in the template of the class at all. You don't seem to use the variadic paramters outside of this function anyway, you would only need to use it at class-scope if you intent to allow only one type of dispatch (like in a Signal<float> that only every allows calling methods with (float) signature). So it would look like this:


    template <RET_TYPE (LISTENER_CLASS::*func)(PARAMS...), class... PARAMS>
    void dispatch(PARAMS... params)
    {
        for(auto it = m_Listeners.begin(); it != m_Listeners.end(); ++it)
            (it->second->*func)(params...);
    }

I'm not entirely sure if this compiles, since PARAMS comes after the function def that uses it...

Hi Juliean,

Thanks for the help! You're definitely on the right track. Unfortunately, PARAMS does have to be declared before the function signature, and since PARAMS is of variable length, it must be declared at the very end.

I played around with it a little more now and I've found a solution (doesn't quite comply to the original requirements I had, but it's fine too).

The template parameters for the class now only contain the listener:


template <class LISTENER_CLASS>
class GenericDispatcher

And I modified the dispatcher function to be:


template <class RET_TYPE, class... PARAMS>
void dispatch(RET_TYPE (LISTENER_CLASS::*func)(PARAMS...), PARAMS... params)
{
    for(auto it = m_Listeners.begin(); it != m_Listeners.end(); ++it)
        (it->second->*func)(params...);
}

If you pass the member function pointer as an argument, it allows the compiler to deduce its signature.

The entire code:


#include <iostream>
#include <string>
#include <map>

struct Listener
{
    virtual void doThing(int, int) = 0;
    virtual void doAnotherThing(float) = 0;
};

struct Foo : public Listener
{
    virtual void doThing(int a, int b)
    {
        std::cout << a*b << std::endl;
    }
    virtual void doAnotherThing(float a)
    {
        std::cout << a << std::endl;
    }
};

template <class LISTENER_CLASS>
class GenericDispatcher
{
public:
    void addListener(LISTENER_CLASS* listener, std::string name)
    {
        m_Listeners[name] = listener;
    }

    template <class RET_TYPE, class... PARAMS>
    void dispatch(RET_TYPE (LISTENER_CLASS::*func)(PARAMS...), PARAMS... params)
    {
        for(auto it = m_Listeners.begin(); it != m_Listeners.end(); ++it)
            (it->second->*func)(params...);
    }

private:
    std::map<std::string, LISTENER_CLASS*> m_Listeners;
};

int main()
{
    Foo a, b;
    GenericDispatcher<Listener> dispatcher;
    dispatcher.addListener(&a, "a");
    dispatcher.addListener(&b, "b");
    dispatcher.dispatch(&Listener::doThing, 6, 7);
    dispatcher.dispatch(&Listener::doAnotherThing, 4.32f);
    return 0;
}
"I would try to find halo source code by bungie best fps engine ever created, u see why call of duty loses speed due to its detail." -- GettingNifty

Ah, sure, I was kind of confused by the use of the function-def as part of the template, of course just specifying return & params in the template is obviously the neatest way.

If you pass the member function pointer as an argument, it allows the compiler to deduce its signature.


This is a well-known problem in C++. You can't use function pointers as non-type template parameters without also specifying the return type, class type (if any), and parameter types, which prevents using them easily in many kinds of templates. There's a new paper submitted practically every year to the ISO committee on possible generalized ways of fixing this.

Sean Middleditch – Game Systems Engineer – Join my team!

A new problem has arisen, demonstrated by the following code:


#include <iostream>
#include <string>
#include <map>


struct Vec2
{
        float x;
        float y;
};


struct Listener
{
    virtual void printVec2(const Vec2& vec) = 0;
};


struct Foo : public Listener
{
    virtual void printVec2(const Vec2& vec)
    {
        std::cout << vec.x << "," << vec.y << std::endl;
    }
};


template <class LISTENER_CLASS>
class GenericDispatcher
{
public:
    void addListener(LISTENER_CLASS* listener, std::string name)
    {
        m_Listeners[name] = listener;
    }


    template <class RET_TYPE, class... PARAMS>
    void dispatch(RET_TYPE (LISTENER_CLASS::*func)(PARAMS...), PARAMS... params)
    {
        for(auto it = m_Listeners.begin(); it != m_Listeners.end(); ++it)
            (it->second->*func)(params...);
    }


private:
    std::map<std::string, LISTENER_CLASS*> m_Listeners;
};


int main()
{
    Foo a, b;
    Vec2 vec;
    vec.x = 6.4f; vec.y = 3.4f;
    GenericDispatcher<Listener> dispatcher;
    dispatcher.addListener(&a, "a");
    dispatcher.addListener(&b, "b");
    dispatcher.dispatch(&Listener::printVec2, vec);
    return 0;
}

Which gives me the following compilation error:


$ g++ -W -Wall -Wextra -std=c++0x -I. -o test main.cpp
main.cpp: In function ‘int main()’:
main.cpp:52:50: error: no matching function for call to ‘GenericDispatcher<Listener>::dispatch(void (Listener::*)(const Vec2&), Vec2&)’
main.cpp:52:50: note: candidate is:
main.cpp:34:10: note: template<class RET_TYPE, class ... PARAMS> void GenericDispatcher::dispatch(RET_TYPE (LISTENER_CLASS::*)(PARAMS ...), PARAMS ...) [with RET_TYPE = RET_TYPE, PARAMS = {PARAMS ...}, LISTENER_CLASS = Listener]

It seems as though qualifiers and references are not deduced correctly?

Adding the following method to the Dispatcher class only partially fixes the issue:


    template <class RET_TYPE, class... PARAMS>
    void dispatch(RET_TYPE (LISTENER_CLASS::*func)(const PARAMS&...), const PARAMS&... params)
    {
        for(auto it = m_Listeners.begin(); it != m_Listeners.end(); ++it)
            (it->second->*func)(params...);
    }

As soon as I try to call listener methods with signatures such as (const Vec2&, int) nothing works anymore.

Any advice?

"I would try to find halo source code by bungie best fps engine ever created, u see why call of duty loses speed due to its detail." -- GettingNifty

Update: Explicitly declaring the template parameters solves the issue, without having to modify the dispatcher class:


int main()
{
    Foo a, b;
    Vec2 vec;
    vec.x = 6.4f; vec.y = 3.4f;
    GenericDispatcher<Listener> dispatcher;
    dispatcher.addListener(&a, "a");
    dispatcher.addListener(&b, "b");
    dispatcher.dispatch<void, const Vec2&>(&Listener::printVec2, vec);
    return 0;
}

Is there some way to use the "explicit" keyword on member functions? That is probably what I'm after.

"I would try to find halo source code by bungie best fps engine ever created, u see why call of duty loses speed due to its detail." -- GettingNifty

Another thought, why do you even need the return-value to be a templated type? The dispatch-function won't return anything after all, being able to call a function of the dispatching-class that returns anything other than "void" seems unnecessary.

You are trying to use a single variadic parameter typelist in two different contexts. One context is the method signature. The other is the signature of the input parameters. These do not agree with each other as the signature takes `vec2 const&` but the parameter you're passing in is just a `vec2`. The compiler uses the latter for deduction, which ends up being the wrong type for the signature of the given method, so it fails. The easy fix it to use use two separate variadic type lists.

A simplified demo you should be able to extrapolate from is below. Notice that both `P` and `O` are variadic type lists. Because they're used in two different contexts the compiler allows this and can deduce both of them correctly.

http://goo.gl/4ejhxJ

template <typename ...P, typename ...O, typename R, typename C>
void stuff(C* self, R(C::*method)(P...), O&&... params)
{
  (self->*method)(params...);
}

struct foo {
  void bar(int, float) {}
};

int main()
{
  foo f;
  stuff(&f, &foo::bar, 1, 3);
}
Note that in most cases you want to use `std::forward<O>(params)...` if you're only going to be invoking one method in order to properly make use of movable types. That won't work in your case, though, since you wouldn't want the first method called to "consume" the movable values; the only option you have really is to just not support movable types. You should still use universal references to avoid needless copies, though (the `O&&...` bit).

Sean Middleditch – Game Systems Engineer – Join my team!


Thanks for the help! You're definitely on the right track. Unfortunately, PARAMS does have to be declared before the function signature, and since PARAMS is of variable length, it must be declared at the very end.

This is not true for deduced parameters (such as in a function template or a partial specialization). You aren't even limited to a single variadic pack in that case (as Sean demonstrates in the previous post).

This topic is closed to new replies.

Advertisement