11

I'm doing some kind of wrapper that looks like this:

#include <iostream>

template<class T, class Value>
void Apply(void (T::*cb)(Value), T* obj, Value v)
{
    (obj->*cb)(v);
}

class Foo
{
public:
    void MyFunc(const int& i)
    {
        std::cout << i << std::endl;
    }

    const int& GetValue()
    {
        return i_;
    }

private:
    int i_ = 14;
};

int main()
{
    Foo f;
    Apply(&Foo::MyFunc, &f, f.GetValue());
}

And I'm getting this error:

  • Apply: no matching overloaded function found.
  • void Apply(void (__thiscall T::* )(Value),T *,Value): template parameter Value is ambiguous, could be int or const int &.
  • void Apply(void (__thiscall T::* )(Value),T *,Value): could not deduce template argument for Value from const int.

So I get it that it comes from template parameter deduction, however I fail to understand how. Why would Value not evaluate to const int& both times?

HiroshimaCC
  • 486
  • 4
  • 11

2 Answers2

14

Why it fails

Currently, the template parameter Value is deduced in two different places in the call to Apply: from the pointer to member function argument and from the last argument. From &Foo::MyFunc, Value is deduced as int const&. From f.GetValue(), Value is deduced as int. This is because reference and top-level cv-qualifiers are dropped for template deduction. Since these two deductions for the parameter Value differ, deduction fails - which removes Apply() from the overload set, and as a result we have no viable overload.

How to fix it

The problem is that Value is deduced in two different places, so let's just prevent that from happening. One way is to wrap one of the uses in a non-deduced context:

template <class T> struct non_deduced { using type = T; };
template <class T> using non_deduced_t = typename non_deduced<T>::type;

template<class T, class Value>
void Apply(void (T::*cb)(Value), T* obj, non_deduced_t<Value> v)
{
    (obj->*cb)(v);
}

The last argument, v, is of type non_deduced_t<Value> which, as the name suggests, is a non-deduced context. So during the template deduction process, Value is deduced as int const& from the pointer to member function (as before) and now we simply plug that into the type for v.

Alternatively, you could choose to deduce cb as its own template parameter. At which point Apply() just reduces to std::invoke().

Barry
  • 286,269
  • 29
  • 621
  • 977
  • Thanks for the idea, I actually managed to do it using `std::decay`. – HiroshimaCC Nov 09 '16 at 21:34
  • @AlexandreBourlon `decay` is a bad choice here - if `Value` is a reference type you're now going to be passing a copy of the argument into `cb` instead, which isn't what you want. – Barry Nov 09 '16 at 21:40
  • Isn't it what also happen with your solution? That what I thought at first... Right now I'm using `std::decay_t const&`, so the type loose reference and cv-qualifier(s) if he has it, then I force a const& on the parameter. I know it's not a direct equivalent to the type passed at the call site, but it seems good enough for my use case here. I'm still open for better suggestion though. – HiroshimaCC Nov 09 '16 at 22:22
  • @AlexandreBourlon No it isn't. Consider what happens if `Value` is an non-const lvalue reference. You're going to take your argument by const lvalue reference and try that pass that through. – Barry Nov 10 '16 at 13:05
  • So I looked a little more into *non-deduced context* and found this other SO question: http://stackoverflow.com/questions/25245453/what-is-a-nondeduced-context. What do you think of `std::common_type` in this context? – HiroshimaCC Nov 10 '16 at 14:39
  • @AlexandreBourlon `common_type_t` is `decay_t`, so it has the same problem. – Barry Nov 10 '16 at 15:44
4

The expression f.GetValue() is an lvalue of type const int. When this is passed by value, template argument deduction deduces the type int. In general, deducing Value from Value v will never produce a reference or a type with top-level cv-qualification.

You'll probably want to have two separate template parameters in place of Value (one for the function type's argument, one for the actual argument type) and use SFINAE to disable Apply when cb isn't callable with v (or static_assert for a hard error).

Brian Bi
  • 111,498
  • 10
  • 176
  • 312