【问题标题】:Can a friend function in C++ have a default argument whose type has a private destructor?C++ 中的友元函数可以有一个默认参数,其类型有一个私有析构函数吗?
【发布时间】:2021-10-13 10:57:59
【问题描述】:

在下一个示例中,带有私有析构函数的类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++ 中的默认参数上扩展?

【问题讨论】:

  • @HolyBlackCat 我也会这么说,但没有编译器在声明 void foo(U); 时抱怨
  • @TimRandall 我不确定了,在你回复之前删除了评论......虽然声明本身很好,它在调用之前不需要析构函数。
  • 我能找到的最好的是this。不过,这不涉及默认参数不在友元函数体中的可能性,因此无法访问构造函数。
  • U 参数不是在调用foo 的上下文中构造(和破坏)的,它是在调用点作为参数构造的(即使是默认的)。这是参数的默认参数的缺点之一:它们是在调用站点生成的,如果默认更改,调用者必须重新编译以获取更改。

标签: c++ language-lawyer default-value friend


【解决方案1】:

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

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

但是,[expr.call]/8 说:

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

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

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

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

但是,也存在 [dcl.fct.default]/5 的问题,其中指出:

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

该标准从未定义“语义约束”的含义;如果假设它包括初始化和销毁​​的访问控制,那么这可能解释了为什么 Clang 和 MSVC 似乎允许从不应访问 U::~U 的上下文调用 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 的行为,那么我认为它应该被认为是有缺陷的。

【讨论】:

  • 我从来没有真正完全理解过[class.friend]/7 中包含的“词法范围”,包括隐藏的朋友,尤其是w.r.t。默认参数。它是否限制在命名空间范围内为非隐藏朋友声明的默认参数的“语义约束”。 GCC 似乎考虑到了这一点,因为它喜欢 Clang 接受与此问答类似的示例,但是当使用隐藏的朋友(上面的“第 2 行”)(demo)时,这意味着不适用 [expr.call]/8拒绝 OP 的例子。
  • 在工作文件中采用P1787 后,我认为默认参数旨在“特殊”更加清楚。论文中关于 CWG600 决议(作为 DR 决议)的评论是相关的。此外,[class.access.general]/6-8 的更新形式更清楚地说明了在默认参数出现的声明中检查的内容 - 即所有内容。据我所知,Clang 的行为是预期的。
猜你喜欢
  • 2011-11-05
  • 1970-01-01
  • 2013-02-18
  • 1970-01-01
  • 2011-07-18
  • 1970-01-01
  • 1970-01-01
  • 2011-04-01
  • 1970-01-01
相关资源
最近更新 更多