声明为 noexcept 的函数在其默认参数中引发异常时的奇怪行为

Plu*_*uto 16 c++ exception language-lawyer noexcept

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

struct B {
    B(B&&, int = (throw 0, 0)) noexcept {}
};
Run Code Online (Sandbox Code Playgroud)

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

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

noexcept( B(std::declval<B>()) );    // false
noexcept( B(std::declval<B>(), 1) ); // true
Run Code Online (Sandbox Code Playgroud)

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

struct D : public B { };
Run Code Online (Sandbox Code Playgroud)

我测试了类D

noexcept( D(std::declval<D>()) );  // true
Run Code Online (Sandbox Code Playgroud)

我已经阅读了标准,我认为根据标准,noexcept( D(std::declval<D>()) )应该返回false.

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

根据[expr.unary.noexcept]

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

所以现在我们需要判断表达式是否B(std::declval<B>())潜在抛出的

根据[except.spec]

表达式E是潜在的抛出,如果

  • E是一个函数调用,其 ...,具有潜在抛出异常规范,或
  • E隐式调用具有潜在抛出异常规范的函数(例如 ...),或
  • E是一个 throw 表达式,或者
  • E是一个dynamic_cast表达式...
  • E是一个typeid表达式...
  • 任何的直接的子表达式Ë是潜在的投掷。

在我的例子中,该表达式调用了Bis的移动构造函数noexcept,所以它不属于前两种情况。显然,不属于后三种情况。

直接子表达式的定义在[intro.execution] 中

表达式E直接子表达式是

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

根据该标准,默认参数(throw 0, 0)立即的子表达式B(std::declval<B>()),但不是立即的子表达式B(std::declval<B>(), 1),并且throw 0直接子表达式(throw 0, 0),这是一个潜在的投掷表达。So (throw 0, 0)andB(std::declval<B>())也是潜在的抛出表达式。noexcept( B(std::declval<B>()) )返回falsenoexcept( B(std::declval<B>(), 1) )返回是真的true

但我对最后一个例子感到困惑。为什么noexcept( D(std::declval<D>()) )返回trueD(std::declval<D>())will 隐式调用 的移动构造函数B,它满足立即子表达式的第二个要求。所以它也应该满足潜在投掷传递的要求。但结果恰恰相反。

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


编辑:

标准中有类似的例子。在[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
};
Run Code Online (Sandbox Code Playgroud)

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

D此举构造受以下因素影响B的析构函数“?可能不是。因为D的拷贝构造函数也受B析构函数的影响,但是D的拷贝构造函数是不抛出的。

此外,根据[except.spec]

即使在构造函数 ([except.ctor]) 的执行过程中抛出异常时调用完全构造子对象的析构函数,它们的异常规范对构造函数的异常规范没有贡献,因为从这样的析构函数会调用函数std?::?terminate而不是转义构造函数([except.throw]、[except.terminate])。

所以 的移动构造函数D真正受到 的移动构造函数的影响B

dfr*_*fri 2

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

\n
\n

D\xe2\x80\x8b::\xe2\x80\x8bD(D&&) 可能抛出异常,因为 B\ 的构造函数的默认参数可能会抛出异常

\n
\n

D\xe2\x80\x8b::\xe2\x80\x8bD(D&&)可能会在 [ except.spec]/12 示例中抛出,因为它的析构函数正在抛出,而不是因为 的默认参数B

\n

如果我们回到 OP 的示例(没有抛出 dtor),为了D::D(D&&)潜在地抛出,它应该满足[ except.spec]/7

\n
\n

类 的隐式声明的构造函数X,或者在其第一个声明中默认没有 noexcept 说明符的构造函数,\n且仅当以下任何构造可能\n可能抛出时,才\n具有潜在抛出的异常规范:

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

(7.1) 不适用:子对象是类型B,并且可行的构造函数B(B&&, int = (throw 0, 0)) noexcept作为构造函数(不是表达式)被声明为 noexcept,因此不具有可能引发异常的规范。

\n

(7.3) 不适用。

\n

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

\n

子表达式按照[intro.execution]/4

\n
\n

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

\n
\n

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

\n
\n

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

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

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

\n
\n

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

\n
\n

隐式定义的移动向量显式执行其基数的移动(就像用户提供的向量定义一样)。因此,我认为调用D::D(&&)不会隐式调用,从而在到达\ 的移动构造函数B(B&&, int = (throw 0, 0)) noexcept的抛出默认参数之前短路子表达式递归。BMeaningD::D(&&)没有可能引发异常的规范。

\n