Why is my code failing to compile? (perfect forwarding and parameter packs)

402 views Asked by At

Here is the program:

#include <memory>

struct A;

struct B {
    void fn(A* ptr) {}
};

template<typename ...Args>
void foo(B* b, void (B::*func)(Args...), Args&&... args)
{
    (b->*func)(std::forward<Args>(args)...);
}

struct A {
    void bar() { B b; foo(&b, &B::fn, this); } // fails
};

int main()
{
    A a;
    B b;
    foo(&b, &B::fn, &a); // passes
    return 0;
}

Here is the compiler output:

foo.cpp: In member function 'void A::bar()':
foo.cpp:16:47: error: no matching function for call to 'foo(B*, void (B::*)(A*), A* const)'
     void bar() { B b; foo(&b, &B::fn, this); } // fails
                                           ^
foo.cpp:16:47: note: candidate is:
foo.cpp:10:10: note: template<class ... Args> void foo(B*, void (B::*)(Args ...), Args&& ...)
     void foo(B* b, void (B::*func)(Args...), Args&&... args)
          ^
foo.cpp:10:10: note:   template argument deduction/substitution failed:
foo.cpp:16:47: note:   inconsistent parameter pack deduction with 'A*' and 'A* const'
         void bar() { B b; foo(&b, &B::fn, this); } // fails

I cannot figure out why one call works and one call fails.

EDIT : Changing the parameter pack to Args... from Args&&... solves the compiler problem. But, I would still like to know why it fails.

1

There are 1 answers

2
T.C. On BEST ANSWER

When a template parameter (or parameter pack) is used in two deduced contexts, deduction is done independently for each and the result must match. This makes things like

template<typename ...Args>
void foo(B* b, void (B::*func)(Args...), Args&&... args) { /* ... */ }

tricky to use at best, because Args is being deduced from both the member function signature and the subsequent pack of arguments; coupled with the special rules for forwarding references, you'll usually not be getting an exact match.

Now, in this case, you should get an exact match, because this is a prvalue of type A*, so Args will be deduced as the non-reference type A* according to the forwarding reference rules, and that happens to match the signature of B::fn. Unfortunately, due to bug 56701, GCC 4.8 considers this to have type A* const and deduces Args accordingly (and MSVC apparently has the same bug), causing a mismatch.

I recommend making func's type a template parameter, side-stepping the double-deduction issue entirely.

template<typename PMF, typename ...Args>
void foo(B* b, PMF func, Args&&... args) { /* ... */ }

Alternatively, you can constrain func to "pointer to member of B of some sort":

template<typename F, typename ...Args>
void foo(B* b, F B::* func, Args&&... args) { /* ... */ }

Both also have the benefit of handling pointer to const member functions properly, compared to the original.

If you really want to constrain func's type further, use two packs:

template<typename ...Args, typename... FArgs>
void foo(B* b, void (B::*func)(FArgs...), Args&&... args) 

Another possible workaround, for this case only, involves playing with this to try to remove the erroneous const-qualification; +this works with GCC 4.8 but not MSVC; &*this works with MSVC but not GCC 4.8; +&*this seems to work with both but is getting into line-noise territory...