Sign in to follow this  
TheComet

Variadic templates - Deducing member function signature?

Recommended Posts

TheComet    3896

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;
Edited by TheComet

Share this post


Link to post
Share on other sites
Juliean    7068

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...

Share this post


Link to post
Share on other sites
TheComet    3896

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;
}
Edited by TheComet

Share this post


Link to post
Share on other sites
Juliean    7068

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.

Share this post


Link to post
Share on other sites
SeanMiddleditch    17565

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.

Share this post


Link to post
Share on other sites
TheComet    3896

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?

Share this post


Link to post
Share on other sites
TheComet    3896

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.

Edited by TheComet

Share this post


Link to post
Share on other sites
Juliean    7068

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.

Share this post


Link to post
Share on other sites
SeanMiddleditch    17565
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).

Share this post


Link to post
Share on other sites
l0calh05t    1796

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

Edited by l0calh05t

Share this post


Link to post
Share on other sites
TheComet    3896

It seems I was under the misconception that the "variable" part of variadic template arguments is required to be at the end. Must be because I'm used to python.

Thanks for clearing that up!

 

[EDIT] Can someone explain to me why you've decided to use an rvalue reference here?

template <typename ...P, typename ...O, typename R, typename C>
void stuff(C* self, R(C::*method)(P...), O&&... params)
Edited by TheComet

Share this post


Link to post
Share on other sites
l0calh05t    1796

 

It seems I was under the misconception that the "variable" part of variadic template arguments is required to be at the end. Must be because I'm used to python.

Thanks for clearing that up!

 

[EDIT] Can someone explain to me why you've decided to use an rvalue reference here?

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

 

It has to be at the end in a non-deduced context (such as class template parameters).

 

As to your edit: It isn't exactly an rvalue reference, but is usually called a "universal reference", because if you pass in an lvalue of type T, O would be deduced as T& and T& && is collapsed to T& according to the reference collapsing rules. An rvalue of type T would instead cause O to be deduced as T&& and T&& && is collapsed to T&&, hence a universal reference.

Share this post


Link to post
Share on other sites

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!

Register a new account

Sign in

Already have an account? Sign in here.

Sign In Now

Sign in to follow this