Search code examples
c++templatesoverload-resolution

Function template argument deduction with variadic class template as function call parameter


All the examples are from here and here.

Specifically,

template<class...> struct Tuple { };
template<          class... Types> void g(Tuple<Types ...>);        // #1
// template<class T1, class... Types> void g(Tuple<T1, Types ...>);    // #2
template<class T1, class... Types> void g(Tuple<T1, Types& ...>);   // #3

g(Tuple<>());                     // calls #1
g(Tuple<int, float>());           // calls #2
g(Tuple<int, float&>());          // calls #3
g(Tuple<int>());                  // calls #3

With #2 uncommented, g()'s are resolved as described in the comments. What surprises me is that if I comment out the #2 line, the g()'s calls are resolved as follows:

g(Tuple<>());                     // calls #1
g(Tuple<int, float>());           // calls **#1 ???? why not #3????**
g(Tuple<int, float&>());          // calls #3
g(Tuple<int>());                  // calls #3

From the following examples and explanations below I can't see why g(Tuple<int, float>()); can't resolved to #3. It is the direction application of the following two rules:

If a parameter pack appears as the last P, then the type P is matched against the type A of each remaining argument of the call. Each match deduces the template arguments for the next position in the pack expansion.

template<class ... Types> void f(Types& ...);
void h(int x, float& y) {
const int z = x;
f(x, y, z); // P=Types&..., A1=x: deduces the first member of Types... to int
            // P=Types&..., A2=y: deduces the second member of Types... to float
            // P=Types&..., A3=z: deduces the third member of Types... to const int
           // calls f<int, float, const int>

If P has one of the forms that include a template parameter list <T> or <I>, then each element Pi of that template argument list is matched against the corresponding template argument Ai of its A. If the last Pi is a pack expansion, then its pattern is compared against each remaining argument in the template argument list of A. A trailing parameter pack that is not otherwise deduced, is deduced to an empty parameter pack.


Solution

  • There's a misconception here between your two examples. In the 2nd example with f, you are deducing reference arguments to a function. In the 1st example with g, you are deducing reference template parameters to an argument to a function. The latter must match exactly, but the former is deduced against the referred types. They are not the same.


    In your fist example,

    g(Tuple<int, float>());
    

    cannot call g(Tuple<T1, Types&...>). The template deduction process is about picking a deduced argument type that is identical to the called argument type. There are some exceptions (for referenced cv-qualifications, pointers, derived classes, arrays, functions), but none of those apply here. We simply need to pick T1 and Types... such that Tuple<T1, Types&...> is the same type as Tuple<int, float>. This is impossible as there is no such pack Types... for which Types&... is {float}, since float is not a reference!

    So once you comment out (2), there's only one viable candidate: (1).


    On the other hand,

    template<class ... Types> void f(Types& ...);
    void h(int x, float& y) {
        const int z = x;
        f(x, y, z);
    }
    

    Here, Types&... is actually the type of the parameter itself (rather than a template argument of it), so (temp.deduct.call):

    If P is a reference type, the type referred to by P is used for type deduction.

    We deduce Types... to match the arguments. This succeeds because all the arguments are lvalues, and we simply pick {int, float, const int}.