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.
现在我试着按照标准来分析结果。
noexcept运算符的结果是,true除非表达式可能抛出([except.spec])。
所以现在我们需要判断表达式是否B(std::declval<B>())是潜在抛出的。
表达式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>()) )返回false和noexcept( B(std::declval<B>(), 1) )返回是真的true。
但我对最后一个例子感到困惑。为什么noexcept( D(std::declval<D>()) )返回true?D(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是潜在抛出的,而 的析构函数B是noexcept(false)。
将D此举构造受以下因素影响B的析构函数“?可能不是。因为D的拷贝构造函数也受B析构函数的影响,但是D的拷贝构造函数是不抛出的。
此外,根据[except.spec]:
即使在构造函数 ([except.ctor]) 的执行过程中抛出异常时调用完全构造子对象的析构函数,它们的异常规范对构造函数的异常规范没有贡献,因为从这样的析构函数会调用函数
std?::?terminate而不是转义构造函数([except.throw]、[except.terminate])。
所以 的移动构造函数D真正受到 的移动构造函数的影响B。
我认为[except.spec]/12的非规范示例中的以下代码注释充其量是不准确的。
\n\n\nD\xe2\x80\x8b::\xe2\x80\x8bD(D&&) 可能抛出异常,因为 B\ 的构造函数的默认参数可能会抛出异常
\n
D\xe2\x80\x8b::\xe2\x80\x8bD(D&&)可能会在 [ except.spec]/12 示例中抛出,因为它的析构函数正在抛出,而不是因为 的默认参数B。
如果我们回到 OP 的示例(没有抛出 dtor),为了D::D(D&&)潜在地抛出,它应该满足[ except.spec]/7:
\n\n类 的隐式声明的构造函数
\nX,或者在其第一个声明中默认没有 noexcept 说明符的构造函数,\n且仅当以下任何构造可能\n可能抛出时,才\n具有潜在抛出的异常规范:\n
\n- (7.1) 通过类的构造函数的隐式定义中的重载决议选择的构造函数
\nX来初始化\n可能构造的子对象,或者- (7.2)此类初始化的子表达式,例如默认参数表达式,或者,
\n- (7.3) 对于默认构造函数,默认成员初始值设定项。
\n
(7.1) 不适用:子对象是类型B,并且可行的构造函数B(B&&, int = (throw 0, 0)) noexcept作为构造函数(不是表达式)被声明为 noexcept,因此不具有可能引发异常的规范。
(7.3) 不适用。
\n因此,(7.2) 仍然存在,并且仅当throw 0是使用构造函数的初始化的子表达式时才适用D\xe2\x80\x8b::\xe2\x80\x8bD(D&&)。
子表达式按照[intro.execution]/4:
\n\n\n表达式 E 的子表达式是 E 的直接子表达式或 E 的直接子表达式的子表达式。
\n
并且,正如 OP 已经列出的,直接子表达式由[intro.execution]/3指定:
\n\n\n表达式 E 的直接子表达式是
\n\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
我无法找到“隐式调用”含义的正式规范,但基于[class.copy.ctor]/14:
\n\n\n非联合类 X 的隐式定义的复制/移动构造函数执行其基类和成员的成员复制/移动。[...]
\n
隐式定义的移动向量显式执行其基数的移动(就像用户提供的向量定义一样)。因此,我认为调用D::D(&&)不会隐式调用,从而在到达\ 的移动构造函数B(B&&, int = (throw 0, 0)) noexcept的抛出默认参数之前短路子表达式递归。BMeaningD::D(&&)没有可能引发异常的规范。