【问题标题】:Strange behavior when a function declared as noexcept throws an exception in its default argument声明为 noexcept 的函数在其默认参数中引发异常时的奇怪行为
【发布时间】:2021-08-11 13:04:38
【问题描述】:

这是一个关于我的问题的例子:

struct B {
    B(B&&, int = (throw 0, 0)) noexcept {}
};

我知道这是一段非常奇怪的代码。它只是用来说明问题。 B 的移动构造函数有一个 noexcept 说明符,而它有一个默认参数会引发异常。

如果我使用noexcept 运算符来测试移动构造函数,它将返回false。但是如果我提供第二个参数,它将返回“true”(在 GCC 和 Clang 上):

noexcept( B(std::declval<B>()) );    // false
noexcept( B(std::declval<B>(), 1) ); // true

然后我添加了类D,它继承自B,并且不提供移动构造函数。

struct D : public B { };

我测试了D类:

noexcept( D(std::declval<D>()) );  // true

我已阅读标准,我认为根据标准,noexcept( D(std::declval&lt;D&gt;()) ) 应该返回false

现在我试着按照标准来分析一下结果。

根据[expr.unary.noexcept]

noexcept 运算符的结果是 true,除非表达式可能抛出([except.spec])。

所以现在我们需要判断B(std::declval&lt;B&gt;())这个表达式是否是potentially-throw

根据[except.spec]

表达式 E 可能会抛出 if

  • E 是一个函数调用,其 ...,具有可能引发异常的规范,或
  • E 隐式调用具有潜在抛出异常规范的函数(例如...),或
  • E 是一个 throw 表达式,或者
  • Edynamic_cast 表达式 ...
  • Etypeid 表达式 ...
  • E 的任何立即子表达式都是潜在抛出的。

在我的例子中,表达式调用了B的move构造函数,即noexcept,所以不属于前两种情况。显然不属于后面三种情况。

立即子表达式的定义在[intro.execution]:

表达式E直接子表达式

  • E的操作数([expr.prop])的组成表达式,
  • E 隐式调用的任何函数调用,
  • 如果 E 是 lambda 表达式,...
  • 如果E是一个函数调用或隐式调用一个函数,每个默认参数([dcl.fct.default])的组成表达式用在通话,或
  • 如果 E 创建一个聚合对象 ...

根据标准,默认参数(throw 0, 0)B(std::declval&lt;B&gt;())立即子表达式,而不是B(std::declval&lt;B&gt;(), 1)和@987654350的立即子表达式 @ 是(throw 0, 0)直接子表达式,这是一个潜在的抛出表达式。所以(throw 0, 0)B(std::declval&lt;B&gt;()) 也是潜在的抛出表达式。确实noexcept( B(std::declval&lt;B&gt;()) ) 返回falsenoexcept( B(std::declval&lt;B&gt;(), 1) ) 返回true

但我对最后一个例子感到困惑。为什么noexcept( D(std::declval&lt;D&gt;()) ) 返回trueD(std::declval&lt;D&gt;())会隐式调用B的move构造函数,满足立即子表达式的第二个要求。所以它还应该满足potentially-throw传递的要求。但结果恰恰相反。

那么我对前两个结果的原因的解释是否正确?第三个结果的原因是什么?


编辑:

标准中有一个类似的例子。在[except.spec]:

struct A {
  A(int = (A(5), 0)) noexcept;
  A(const A&) noexcept;
  A(A&&) noexcept;
  ~A();
};
struct B {
  B() noexcept;
  B(const B&) = default;        // implicit exception specification is noexcept(true)
  B(B&&, int = (throw 42, 0)) noexcept;
  ~B() noexcept(false);
};
int n = 7;
struct D : public A, public B {
    int * p = new int[n];
    // D​::​D() potentially-throwing, as the new operator may throw bad_­alloc or bad_­array_­new_­length
    // D​::​D(const D&) non-throwing
    // D​::​D(D&&) potentially-throwing, as the default argument for B's constructor may throw
    // D​::​~D() potentially-throwing
};

A 中的所有特殊成员函数都是noexcept,而B 的移动构造函数是潜在抛出的,B 的析构函数是noexcept(false)

D 的移动构造函数会受到B 的析构函数的影响吗?可能不是。因为D的拷贝构造函数也受到B的析构函数的影响,但是D的拷贝构造函数是不抛出的。

另外,根据[except.spec]

即使在构造函数([except.ctor])执行期间抛出异常时调用完全构造的子对象的析构函数,它们的异常规范不会影响构造函数的异常规范,因为这样的析构函数抛出的异常会调用函数std​::​terminate,而不是转义构造函数([except.throw],[except.terminate])。

所以D的移动构造函数确实受到B的移动构造函数的影响。

【问题讨论】:

  • @TedLyngmo 从noexcept 抛出被定义为调用std::terminate(或std::unexpected,取决于C++ 的版本),它不是UB。
  • @FrançoisAndrieux 啊,谢谢 - 是的,我确实在我自己提供的日志中看到了 std::terminate 调用 :-)
  • 我一直认为默认参数是在调用者的上下文中评估的,而不是作为函数的一部分。我希望在评估默认参数值时抛出不会受到 noexcept 函数的影响。编辑:再想一想,我猜无论哪种方式都是noexcept 运算符上下文中的默认参数,因此结果也将取决于默认参数是有道理的。
  • 如果我对except.spec#7的理解正确,D(D&amp;&amp;)应该是noexcept(false)...
  • @TedLyngmo 我不认为在评估noexcept 函数的默认参数值时抛出意味着调用std::terminate。我的理解是,默认值是在函数上下文之外评估的,它基本上替换了调用站点的参数。如果你有void foo(int) noexceptint bar(),那么如果你尝试foo(bar())bar 抛出,foo 函数没有抛出,所以不需要std::terminate。在我看来这是同样的情况。

标签: c++ exception language-lawyer noexcept


【解决方案1】:

我认为[except.spec]/12 的非规范示例中的以下代码注释充其量是不准确的。

D​::​D(D&&) 可能抛出,因为 B 的构造函数的默认参数可能会抛出

D​::​D(D&amp;&amp;) 可能会抛出 [except.spec]/12 示例,因为它的析构函数正在抛出,而不是因为 B 的默认参数。

如果我们回到 OP 的示例(没有投掷 dtor),对于 D::D(D&amp;&amp;) 潜在投掷,它应该满足 [except.spec]/7

X的隐式声明构造函数,或 构造函数没有默认的 noexcept 说明符 第一个声明,有一个潜在的抛出异常规范 当且仅当以下任何构造是 潜在投掷:

  • (7.1) 在类X 的构造函数的隐式定义中通过重载决议选择的构造函数初始化一个 可能构造的子对象,或
  • (7.2) 此类初始化的子表达式,例如默认参数表达式,或者,
  • (7.3) 用于默认构造函数,默认成员初始值设定项。

(7.1) 不适用:子对象的类型为B,可行的构造函数为B(B&amp;&amp;, int = (throw 0, 0)) noexcept,作为构造(不是表达式)被声明为noexcept,因此没有潜在的-抛出异常规范。

(7.3) 不适用。

因此,(7.2) 仍然存在,并且仅当 throw 0 是使用 D​::​D(D&amp;&amp;) 构造函数的初始化的子表达式时才适用。

子表达式根据[intro.execution]/4:

表达式 E 的子表达式是 E 的直接子表达式或 E 的直接子表达式的子表达式。

并且,正如 OP 已经列出的那样,直接子表达式由 [intro.execution]/3 指定:

表达式 E 的直接子表达式是

  • (3.1)E的操作数([expr.prop])的组成表达式,
  • (3.2) E 隐式调用的任何函数调用,
  • (3.3) 如果 E 是 lambda 表达式,则复制捕获的实体的初始化以及 初始化捕获的初始化器,
  • (3.4) 如果 E 是函数调用或隐式调用函数,则每个默认参数的组成表达式 ([dcl.fct.default]) 在通话中使用,或
  • (3.5) 如果 E 创建一个聚合对象 ([dcl.init.aggr]),每个默认成员初始化器的组成表达式 ([class.mem]) 用于初始化。

我无法找到“隐式调用”含义的正式规范,但基于[class.copy.ctor]/14

非联合类 X 的隐式定义的复制/移动构造函数执行成员方式复制/其基类和成员的移动。 [...]

隐式定义的移动 ctor 执行例如显式移动其基础(就像用户提供的 ctor 定义一样)。因此,我认为D::D(&amp;&amp;) 的调用不会隐式 调用B(B&amp;&amp;, int = (throw 0, 0)) noexcept,从而在到达B 的移动构造函数的抛出默认参数之前短路子表达式递归。意思是D::D(&amp;&amp;) 没有潜在抛出的异常规范。

【讨论】:

  • 我会说“这样的初始化”是指7.1描述的初始化;那是提到初始化的部分; D(D&amp;&amp;) 被称为“类 X 的隐式声明的构造函数”,而不是在初始化方面。这意味着 7.2 也在谈论可能构造的子对象的初始化,所以问题是 throw 0 是否是使用默认参数调用 B 的构造函数的子表达式,而答案是,到目前为止据我所知,是的。
  • "D​::​D(D&amp;&amp;) 可能会抛出 [except.spec]/12 示例,因为它的析构函数正在抛出": "它的析构函数",如 D::~D()?据我了解,潜在抛出的析构函数不会使类的构造函数潜在抛出(创建该类型临时的表达式可能是潜在抛出的,但这是不同的)。如果“它的析构函数”指的是B::~B(),那么请参阅第 7 段的注 2 - 这也不会使 D​::​D(D&amp;&amp;) 潜在地抛出。另外,请注意D::D(const D&amp;) 是非抛出的,尽管涉及相同的析构函数。
  • 是的,我也对“隐式调用”的概念感到困惑。但我也认为博德根的评论是有道理的。对于标准中的例子,如果D的移动构造函数受到析构函数的影响,为什么D的复制构造函数不受影响?这是否意味着D 的移动构造函数确实受到B 的移动构造函数的影响?但是标准要求D的移动构造函数是潜在抛出的,这与编译器的结果不同。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2016-07-03
  • 1970-01-01
相关资源
最近更新 更多