C++ 中的友元函数可以有一个默认参数,其类型有一个私有析构函数吗?

Can a friend function in C++ have a default argument whose type has a private destructor?

在下一个示例中,具有私有析构函数的 class U 具有友元函数 foo。这个友元函数的参数类型为 U,默认值为 U{}:

class U{ ~U(); friend void foo(U); };
void foo(U = {});

Clang 和 MSVC 接受此代码,但 GCC 拒绝它并显示错误

error: 'U::~U()' is private within this context
    2 | void foo(U = {});
      |                ^

演示:https://gcc.godbolt.org/z/eGxYGdzj3

哪个编译器就在这里,友情会扩展到 C++ 中的默认参数吗?

C++20 [class.access]/8 规定如下:

The names in a default argument (9.3.3.6) are bound at the point of declaration, and access is checked at that point rather than at any points of use of the default argument. Access checking for default arguments in function templates and in member functions of class templates is performed as described in 13.9.1.

然而,[expr.call]/8 表示:

... The initialization and destruction of each parameter occurs within the context of the calling function. [Example: The access of the constructor, conversion functions or destructor is checked at the point of call in the calling function. ...

虽然“Example”文本不是规范性的,但我相信它反映了意图;因此,为了和谐地阅读这两条规定,我们应该理解默认参数类型的析构函数(至少在我看来)不是“在默认参数中”的名称。相反,我们应该将对 friend 函数的调用视为发生在以下阶段:

  1. 默认参数 初始化器 被评估。由于 [class.access]/8,此步骤中的访问控制是从声明的上下文中完成的。
  2. 参数是从步骤 1 的结果复制初始化的。由于 [expr.call]/8,此步骤中的访问控制是从调用函数的上下文中完成的。
  3. 函数体被求值。
  4. 参数被销毁。同样,访问控制是从调用函数的上下文中完成的(不相关的注释:未完全指定销毁发生的确切时间)。

GCC 不应该拒绝声明 void foo(U = {}) 因为还没有实际使用析构函数;事实上,foo 可能只从有权访问 U::~U 的上下文中调用。但是,如果 foo 是从无法访问 U::~U 的上下文中调用的,则该程序应该是病式的。在这种情况下,我认为 Clang 和 MSVC 是错误的,因为他们仍然接受代码。

但是,还有 [dcl.fct.default]/5 的问题,其中指出:

The default argument has the same semantic constraints as the initializer in a declaration of a variable of the parameter type, using the copy-initialization semantics (9.4). The names in the default argument are bound, and the semantic constraints are checked, at the point where the default argument appears. ...

该标准从未定义“语义约束”的含义;如果假定包括对初始化和销毁​​的访问控制,那么这可能解释了为什么 Clang 和 MSVC 似乎允许从不应访问 U::~U.[=22 的上下文调用 foo =]

但仔细想想,我觉得这没有太大意义,因为这意味着默认参数在某种程度上是“特殊的”,我认为这不是故意的。即,考虑:

class U {
  public:
    U() = default;
    U(const U&) = default;
  private:
    ~U() = default;
    friend void foo(U);
};
void foo(U = {}) {}

int main() {
    auto p = new U();
    foo(*p);  // line 1
    foo();    // line 2
}

在这里,MSVC 接受第 1 行和第 2 行;考虑到 [expr.call]/8 如何要求可以从 main 访问析构函数,接受第 1 行显然是错误的。但是 Clang 接受第 2 行并拒绝第 1 行,这对我来说也很荒谬:我不认为标准的意图是选择使用默认参数(而不是自己提供参数)会免除调用者必须有权访问参数类型的析构函数。

如果 [dcl.fct.default]/5 似乎需要 Clang 的行为,那么我认为它应该被认为是有缺陷的。