4

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?

yeputons
  • 8,478
  • 34
  • 67

3 Answers3

6

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)
}
walnut
  • 21,629
  • 4
  • 23
  • 59
2

Clang at Godbolt was able to shed some light:

<source>:9:5: warning: use of function template name with no prior declaration in function call with explicit template arguments is a C++20 extension [-Wc++20-extensions]

    foo<void>(s);
    ^

1 warning generated.

Looks like such syntax was not part of the language until C++20 when P084R0 arrived and fixed it.

As foo() is completely implemented inside the S struct, there is no template foo visible in main(). Therefore, compiler does not understand that it needs to do ADL and is unable to find foo(). Like @0x499602D2 said.

One solution is to update the compiler, another solution is to implement foo outside of S and, optionally, add a forward declaration to provide default template arguments:

struct S;

template<typename T = void>
void foo(S); // (a)

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

template<typename T>
void foo(S) { // (c)
    // Needs S to be complete.
}

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

If you try to omit forward declaration (a) you will find that you cannot add default template arguments to (b) because of reasons, and you cannot add it to (c) because it's a redeclaration of (b) and hence cannot introduce default template arguments.

yeputons
  • 8,478
  • 34
  • 67
1

The fact that it's a template doesn't matter. Friend functions that are defined where they are declared can only be found via ADL lookup. When you use the template argments the compiler tries to look for a function template named foo with normal unqualified lookup, and it fails. foo(s) looks up foo with the associated namespace of s (the global namespace) and finds the friend function you've defined.

David G
  • 94,763
  • 41
  • 167
  • 253