为什么我不能使用显式模板参数调用模板友元函数?

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

考虑以下示例:

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

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

我的 GCC 9.2.0 无法编译 (2),出现以下错误:

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);
      |         ^~~~

但是,(1) 工作正常。为什么是这样?如何使用显式模板参数调用 foo

它是模板这一事实并不重要。在声明位置定义的友元函数只能通过 ADL 查找找到。当您使用模板参数时,编译器会尝试使用正常的非限定查找来查找名为 foo 的函数模板,但失败了。 foo(s) 使用 s 的关联命名空间(全局命名空间)查找 foo 并找到您定义的友元函数。

Clang at Godbolt 能够阐明一些问题:

<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.

看起来这种语法直到 C++20 才成为语言的一部分,当时 P084R0 到达并修复了它。

由于 foo() 完全在 S 结构中实现,因此 main() 中看不到模板 foo。因此,编译器不理解它需要做 ADL 并且无法找到 foo()。就像@0x499602D2 说的。

一个解决方案是更新编译器,另一个解决方案是在 S 之外实现 foo,并且可以选择添加前向声明以提供默认模板参数:

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);
}

如果您尝试省略前向声明 (a) 您会发现您无法将默认模板参数添加到 (b),因为 ,并且您无法将其添加到 [=21] =] 因为它是 (b) 的重新声明,因此不能引入默认模板参数。

friend class 主体中的函数定义不会使 friend 函数在封闭的命名空间范围内对通常的非限定名称查找可见(尽管它们被放置在这个命名空间范围内).

为了使其可见,您需要在命名空间范围内为模板添加声明(无论是在定义之前还是之后都无所谓):

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)
}

现在的问题是为什么 foo(s) 有效。这是因为依赖于参数的查找。在没有嵌套名称说明符的函数调用中,还会搜索 classes 和调用参数类型(以及其他)的封闭名称空间以查找匹配的函数重载。对于依赖于参数的查找,仅在 class 主体中声明的 friend 是可见的。通过这种方式,找到了调用 foo(s) 的匹配函数。

foo<void>(s) 应该以相同的方式工作,因为名称是不合格的并且 sS 类型,所以 ADL 应该再次找到朋友 fooS.

但是,还有一个问题需要考虑。当编译器读取 foo 时,它必须决定 foo 是否可以是模板,因为它改变了 foo 之后 < 的解析。

为了决定这一点,在 foo 上进行了非限定名称查找。在 C++20 之前,仅当此查找找到具有该名称的某种模板时,foo 才会被视为模板名称。但是不合格的名称查找在您的情况下找不到任何东西,因为只有 foo 对于普通的不合格名称查找是不可见的。因此 foo 不会被视为模板,并且 foo<void> 不会被解析为模板 ID。

在 C++20 中,规则已更改,如果非限定名称查找找到具有该名称的普通函数或什么都没有,则 foo<void> 将也被视为模板 ID。在这种情况下,调用的以下 ADL 将找到 foo 并且调用将成功。

所以代码将像在 C++20 和 pre-C++20 中一样工作你实际上只需要通过名称 foo 声明 any 模板让 foo<void>(s) 通过 ADL 找到好友 foo。例如:

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)
}