在模板中定义友元函数的实例化

Instantiation of friend function defined inside a template

这是 this question 的跟进。最初的情况是另外一回事,但在我写了一个糟糕的答案和 OP 澄清的过程中,事实证明我们可能需要语言律师的帮助才能理解发生了什么。

Thinking in C++ - Practical Programming Vol 2 可以找到如下例子(意向矿,在线here):

//: C05:FriendScope3.cpp {-bor}
// Microsoft: use the -Za (ANSI-compliant) option
#include <iostream>
using namespace std;
 
template<class T> class Friendly {
    T t;
public:
    Friendly(const T& theT) : t(theT) {}
    friend void f(const Friendly<T>& fo) {
        cout << fo.t << endl;
    }
    void g() { f(*this); }
};
 
void h() {
    f(Friendly<int>(1));
}
 
int main() {
    h();
    Friendly<int>(2).g();
} ///:~

他们继续解释(强调我的):

There is an important difference between this and the previous example: f is not a template here, but is an ordinary function. (Remember that angle brackets were necessary before to imply that f( ) was a template.) Every time the Friendly class template is instantiated, a new, ordinary function overload is created that takes an argument of the current Friendly specialization. This is what Dan Saks has called making new friends. [68] This is the most convenient way to define friend functions for templates.

到目前为止一切顺利。令人费解的部分是“f在这里不是模板,而是一个普通函数”+“每次实例化 Friendly class 模板时,都会创建一个新的普通函数重载”,当你考虑这个例子时:

template <typename T>
struct foo {
    friend void bar(foo x){
        x = "123";
    }
};

int main() {
    foo<int> x;
    bar(x);
}

实例化foo<int>不会导致编译错误!仅调用 bar(x) 会导致 (gcc 10.2):

<source>: In instantiation of 'void bar(foo<int>)':
<source>:10:10:   required from here
<source>:4:11: error: no match for 'operator=' (operand types are 'foo<int>' and 'const char [4]')
    4 |         x = "123";
      |         ~~^~~~~~~
<source>:2:8: note: candidate: 'constexpr foo<int>& foo<int>::operator=(const foo<int>&)'
    2 | struct foo {
      |        ^~~
<source>:2:8: note:   no known conversion for argument 1 from 'const char [4]' to 'const foo<int>&'
<source>:2:8: note: candidate: 'constexpr foo<int>& foo<int>::operator=(foo<int>&&)'
<source>:2:8: note:   no known conversion for argument 1 from 'const char [4]' to 'foo<int>&&'

普通函数的实例化?只有在调用函数时才会失败?这是怎么回事?

bar真的是一个普通的函数吗?它仅在调用时实例化?为什么,当它是一个普通函数时?当实例化 foo<int> 时,bar 实际发生了什么(作者称之为“创建了一个新的、普通的函数重载”,不确定那是什么意思)?

抱歉打了很多?实在是太费解了。请不要错过 language-lawyer 标签,我想知道为什么/标准的哪些部分是这样的,而不仅仅是什么。

PS:为了确保我再次检查,当 bar 未被调用时,三个常见的嫌疑人都编译了示例而没有大的抱怨:https://godbolt.org/z/Wcsbc5qjv

[temp.inst]/2 The implicit instantiation of a class template specialization causes the implicit instantiation of the declarations, but not of the definitions, default arguments, or noexcept-specifiers of the class ... friends...

[temp.inst]/4 ... A function whose declaration was instantiated from a friend function definition is implicitly instantiated when it is referenced in a context that requires a function definition to exist...

像这样的构造是模板的一部分但本身不是模板,因此称为 templated 因为它们仍然遵守许多相同的规则(尤其是在方法和朋友的地方) class 模板分别实例化,赋予每个模板自己的“实例化状态”)。标准本身一直在慢慢使用更精确的语言来应对这种情况,部分原因是 constexpr-if 引入了模板化的 statements(因为它们必须单独实例化,以允许这样做只有一个分支),即使没有语句模板。 (可能对进一步研究有用的这些结构的旧术语是“temploids”。)