Search code examples
c++templatesfriendfriend-function

Why can't I call a template friend function with explicit template arguments?


Consider the following example:

struct S {
    template<typename T = void>
    friend void foo(S) {
    }
};

int main() {
    S s;
    foo(s); // (1)
    foo<void>(s); // (2)
}

My GCC 9.2.0 fails to compile (2) with the following error:

a.cpp: In function 'int main()':
a.cpp:10:5: error: 'foo' was not declared in this scope
   10 |     foo<void>(s);
      |     ^~~
a.cpp:10:9: error: expected primary-expression before 'void'
   10 |     foo<void>(s);
      |         ^~~~

However, (1) works fine. Why is this? How do I call foo with explicit template arguments?


Solution

  • friend function definitions in a class body do not make the friend function visible in the enclosing namespace scope to usual unqualified name lookup (although they are placed into this namespace scope).

    In order to make it visible, you need to add a declaration for the template in the namespace scope (it doesn't matter whether this happens before or after the definition):

    struct S {
        template<typename T = void>
        friend void foo(S) {
        }
    };
    
    template<typename T>
    void foo(S);
    
    int main() {
        S s;
        foo(s); // (1)
        foo<void>(s); // (2)
    }
    

    Now the question is why foo(s) works. This is because of argument-dependent lookup. In a function call without nested name specifier the classes and enclosing namespaces of the types of a call's argument (and others) are also searched for matching function overloads. For argument-dependent lookup friends declared only in the class body are visible. In this way a matching function for the call foo(s) is found.

    foo<void>(s) should work the same way, because the name is unqualified and s is of type S, so ADL should again find the friend foo inside S.

    However, there is another issue to consider. When the compiler reads foo it must decide whether foo can be a template or not, because it changes the parsing of < following foo.

    To decide this, unqualified name lookup is done on foo. Before C++20, foo would be considered a template name only if this lookup finds a template of some kind by that name. But unqualified name lookup doesn't find anything in your case, because the only foo is not visible to ordinary unqualified name lookup. Therefore foo will not be considered a template and foo<void> will not be parsed as a template-id.

    In C++20, the rule was changed and if unqualified name lookup finds either an ordinary function by that name or nothing at all, then foo<void> will be considered a template-id as well. In that case the following ADL for the call will find foo and the call will succeed.

    So the code will work as is in C++20 and pre-C++20 you actually only need to declare any template by the name foo to get foo<void>(s) to find the friended foo by ADL. E.g.:

    struct S {
      template <typename T = void>
      friend void foo(S) {}
    };
    
    template<int>
    void foo();
    
    int main() {
      S s;
      foo(s);        // (1)
      foo<void>(s);  // (2)
    }