Search code examples
c++c++11language-lawyervariadic-templatesoverload-resolution

Weird overload resolution with variadic function templates


I have the following code:

#include <iostream>

template <typename... Args>
void f(int a, int b, Args... args) { 
    std::cout << b << '\n';
    f(a, args...);
}
void f(int, int b) {
    std::cout << b << '\n';     
}

int main() {
  f(1, 2);
  //f(1, 2, 3);
}

While f(1, 2) compiles, f(1, 2, 3) does not. From the error message produced by the compiler, I see that f<> is being instantiated somehow. Within the instantiation, the call f(a) is made and thus the error. What makes the compiler not to use f(int, int) but try to instantiate f<>(int, int) during the process of parsing the call f(1, 2, 3)?


Solution

  • Within the variadic function template f(), the f in the recursive call is a dependent name due to [temp.dep], emphasis mine:

    In an expression of the form:

    postfix-expression ( expression-listopt)
    

    where the postfix-expression is an unqualified-id, the unqualified-id denotes a dependent name if
    (1.1) — any of the expressions in the expression-list is a pack expansion (14.5.3),

    And, according to [temp.dep.res], emphasis mine:

    In resolving dependent names, names from the following sources are considered:
    (1.1) — Declarations that are visible at the point of definition of the template.
    (1.2) — Declarations from namespaces associated with the types of the function arguments both from the instantiation context (14.6.4.1) and from the definition context.

    There is only one declaration of f that is visible at the point of definition of template <typename... Args> void f(int, int, Args...) and that is itself. The second point doesn't apply here because all of your arguments are ints and there's no associated namespaces for fundamental types. Since that function template cannot be called with a single argument, you get a compile error.

    The solution is to restructure your code so that your base case is visible at the point of definition, that is:

    // this can be just the declaration
    void f(int, int ) { /* ... */ } 
    
    template <typename... Args>
    void f(int a, int b, Args... args) 
    { 
        std::cout << b << '\n';
        f(a, args...); // now this will call f(int, int) 
                       // if sizeof...(Args) == 1
    }
    

    An example in which (1.2) applies would be the following:

    #include <iostream>
    
    template <typename A, typename... Args>
    void f(A a, int b, Args... args) { 
        std::cout << b << '\n';
        f(a, args...);
    }
    
    template <typename A>
    void f(A a, int b) {
        std::cout << b << '\n';     
    }
    
    struct bar {};
    
    int main() {
        //f(1,2,3);     // still doesn't compile, same reasoning
        f(bar{}, 2, 3); // OK. bar is in the global namespace, so declarations
                        // from the global namespace in both instantiation 
                        // and definition context are considered, which includes
                        // the second `f`.
    }