48

I am attempting to recreate the Observer pattern where I can perfectly forward parameters to a given member function of the observers.

If I attempt to pass the address of a member function which has multiple overrides, it cannot deduce the correct member function based on the arguments.

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

template<typename Class>
struct observer_list
{
    template<typename Ret, typename... Args, typename... UArgs>
    void call(Ret (Class::*func)(Args...), UArgs&&... args)
    {
        for (auto obj : _observers)
        {
            (obj->*func)(std::forward<UArgs>(args)...);
        }
    }
    std::vector<Class*> _observers;
};

struct foo
{
    void func(const std::string& s)
    {
        std::cout << this << ": " << s << std::endl;
    }
    void func(const double d)
    {
        std::cout << this << ": " << d << std::endl;
    }
};

int main()
{
    observer_list<foo> l;
    foo f1, f2;
    l._observers = { &f1, &f2 };

    l.call(&foo::func, "hello");
    l.call(&foo::func, 0.5);

    return 0;
}

This fails to compile with template argument deduction/substitution failed.

Note that I had Args... and UArgs... because I need to be able to pass parameters which are not necessarily the same type asthe type of the function signature, but are convertible to said type.

I was thinking I could use a std::enable_if<std::is_convertible<Args, UArgs>> call to disambiguate, but I don't believe I can do this with a variadic template parameter pack?

How can I get the template argument deduction to work here?

Xeo
  • 129,499
  • 52
  • 291
  • 397
Steve Lorimer
  • 27,059
  • 17
  • 118
  • 213
  • 4
    It doesn't matter how you change `call`, the problem lies at a level above that. Also, very important: Don't use `std::forward` inside of `call` for the arguments, as they are potentially forwarded multiple times that way, and only one of the observers receives an unmoved argument for rvalues. – Xeo Jul 26 '13 at 07:22

1 Answers1

55

The issue is here:

l.call(&foo::func, "hello");
l.call(&foo::func, 0.5);

For both lines, the compiler doesn't know which foo::func you are referring to. Hence, you have to disambiguate yourself by providing the type information that is missing (i.e., the type of foo:func) through casts:

l.call(static_cast<void (foo::*)(const std::string&)>(&foo::func), "hello");
l.call(static_cast<void (foo::*)(const double      )>(&foo::func), 0.5);

Alternatively, you can provide the template arguments that the compiler cannot deduce and that define the type of func:

l.call<void, const std::string&>(&foo::func, "hello");
l.call<void, double            >(&foo::func, 0.5);

Notice that you have to use double and not const double above. The reason is that generally double and const double are two different types. However, there's one situation where double and const double are considered as if they were the same type: as function arguments. For instance,

void bar(const double);
void bar(double);

are not two different overloads but are actually the same function.

bsdunx
  • 137
  • 7
Cassio Neri
  • 19,583
  • 7
  • 46
  • 68
  • I have tried your static_cast approach on the signature std::vector::iterator std::vector::at ( std::vector::iterator, const std::vector::value_type& ), and it does not work. I'd have expected that to be due to the constness, but there is not an identical overload without constness in the second argument. The compiler (msvc 13) complains that it can not convert from overloaded function to the non-overloaded signature. – JPHarford Feb 09 '15 at 00:33
  • @user2813810 There are only two overloads of `std::vector::at` which are `const_reference at(size_type n) const` and `reference at(size_type n)` (see, for instance, [here](http://en.cppreference.com/w/cpp/container/vector/at)). None of them takes or return iterators. So, I'm not sure I follow you. Why don't you post a new question (with a short self contained example) and cross-ref this post saying that it didn't solve your problem? May be I or somebody else could help. – Cassio Neri Feb 15 '15 at 17:27
  • Yep, the signature was the problem. I was working with every method in containers at the same time through a macro, was very sleepy, and got confused. Turns out the macro was incorrect, was combining two "method detectors", and the compiler error actually made it look like that's the signature for std::vector::at. Another case for macros being evil. So much has changed since then that I couldn't even reproduce it. I'd have come back to delete had I not forgotten about this post when I spotted the problem. You know how you get tunnel vision when you finally spot the bug? – JPHarford Feb 16 '15 at 09:35