Jump to content

  • Log In with Google      Sign In   
  • Create Account


Variadic templates - Deducing member function signature?


Old topic!
Guest, the last post of this topic is over 60 days old and at this point you may not reply in this topic. If you wish to continue this conversation start a new topic.

  • You cannot reply to this topic
11 replies to this topic

#1 TheComet   Members   -  Reputation: 1383

Like
0Likes
Like

Posted 07 May 2014 - 08:40 AM

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, 07 May 2014 - 08:42 AM.

YOUR_OPINION >/dev/null


Sponsor:

#2 Juliean   GDNet+   -  Reputation: 2227

Like
1Likes
Like

Posted 07 May 2014 - 09:19 AM

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



#3 TheComet   Members   -  Reputation: 1383

Like
1Likes
Like

Posted 07 May 2014 - 09:38 AM

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, 07 May 2014 - 09:38 AM.

YOUR_OPINION >/dev/null


#4 Juliean   GDNet+   -  Reputation: 2227

Like
0Likes
Like

Posted 07 May 2014 - 10:26 AM

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.



#5 SeanMiddleditch   Members   -  Reputation: 3857

Like
0Likes
Like

Posted 07 May 2014 - 10:29 AM

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.

#6 TheComet   Members   -  Reputation: 1383

Like
0Likes
Like

Posted 07 May 2014 - 01:08 PM

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?


YOUR_OPINION >/dev/null


#7 TheComet   Members   -  Reputation: 1383

Like
0Likes
Like

Posted 07 May 2014 - 02:17 PM

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, 07 May 2014 - 02:28 PM.

YOUR_OPINION >/dev/null


#8 Juliean   GDNet+   -  Reputation: 2227

Like
0Likes
Like

Posted 07 May 2014 - 06:44 PM

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.



#9 SeanMiddleditch   Members   -  Reputation: 3857

Like
3Likes
Like

Posted 07 May 2014 - 07:19 PM

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

#10 l0calh05t   Members   -  Reputation: 642

Like
0Likes
Like

Posted 08 May 2014 - 02:27 AM


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, 08 May 2014 - 02:30 AM.


#11 TheComet   Members   -  Reputation: 1383

Like
0Likes
Like

Posted 08 May 2014 - 06:19 AM

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, 08 May 2014 - 07:02 AM.

YOUR_OPINION >/dev/null


#12 l0calh05t   Members   -  Reputation: 642

Like
0Likes
Like

Posted 08 May 2014 - 08:11 AM

 

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.






Old topic!
Guest, the last post of this topic is over 60 days old and at this point you may not reply in this topic. If you wish to continue this conversation start a new topic.



PARTNERS