声明为noexcept的函数在其默认参数中引发异常时的奇怪行为
这是一个关于我的问题的例子:
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
然后我添加了 class D,它继承自B但不提供移动构造函数。
struct D : public B { };
我测试了类D:
noexcept( D(std::declval<D>()) ); // true
我已经阅读了标准,我认为根据标准,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>()) )返回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
};
中的所有特殊成员函数A都是noexcept,而 的移动构造函数B是潜在抛出的,而 的析构函数B是noexcept(false)。
将D此举构造受以下因素影响B的析构函数“?可能不是。因为D的拷贝构造函数也受B析构函数的影响,但是D的拷贝构造函数是不抛出的。
此外,根据[except.spec]:
即使在构造函数 ([except.ctor]) 的执行过程中抛出异常时调用完全构造子对象的析构函数,它们的异常规范对构造函数的异常规范没有贡献,因为从这样的析构函数会调用函数
std?::?terminate而不是转义构造函数([except.throw]、[except.terminate])。
所以 的移动构造函数D真正受到 的移动构造函数的影响B。