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

Fed*_*dor 16 c++ friend default-value language-lawyer

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

class U{ ~U(); friend void foo(U); };
void foo(U = {});
Run Code Online (Sandbox Code Playgroud)

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

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

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

哪个编译器就在这里,友谊是否扩展到 C++ 中的默认参数?

Bri*_*ian 9

C++20 [class.access]/8 提供如下:

默认参数 (9.3.3.6) 中的名称在声明点绑定,并且在该点而不是在使用默认参数的任何点检查访问。如 13.9.1 中所述,对函数模板和类模板的成员函数中的默认参数进行访问检查。

然而,[expr.call]/8 说:

... 每个参数的初始化和销毁​​发生在调用函数的上下文中。[示例:在调用函数的调用点检查构造函数、转换函数或析构函数的访问。...

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

  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 也存在问题,其中指出:

默认参数具有与参数类型变量声明中的初始化程序相同的语义约束,使用复制初始化语义(9.4)。默认参数中的名称是绑定的,并且在默认参数出现的地方检查语义约束。...

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

但是更多地考虑这一点,我觉得这没有太大意义,因为这意味着默认参数是“特殊的”,我认为这不是故意的。也就是说,请考虑:

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
}
Run Code Online (Sandbox Code Playgroud)

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

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