【发布时间】: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<D>()) ) 应该返回false。
现在我试着按照标准来分析一下结果。
noexcept运算符的结果是true,除非表达式可能抛出([except.spec])。
所以现在我们需要判断B(std::declval<B>())这个表达式是否是potentially-throw。
表达式 E 可能会抛出 if
- E 是一个函数调用,其 ...,具有可能引发异常的规范,或
- E 隐式调用具有潜在抛出异常规范的函数(例如...),或
- E 是一个 throw 表达式,或者
- E 是
dynamic_cast表达式 ...- E 是
typeid表达式 ...- E 的任何立即子表达式都是潜在抛出的。
在我的例子中,表达式调用了B的move构造函数,即noexcept,所以不属于前两种情况。显然不属于后面三种情况。
立即子表达式的定义在[intro.execution]:
表达式E的直接子表达式是
- E的操作数([expr.prop])的组成表达式,
- E 隐式调用的任何函数调用,
- 如果 E 是 lambda 表达式,...
- 如果E是一个函数调用或隐式调用一个函数,每个默认参数([dcl.fct.default])的组成表达式用在通话,或
- 如果 E 创建一个聚合对象 ...
根据标准,默认参数(throw 0, 0)是B(std::declval<B>())的立即子表达式,而不是B(std::declval<B>(), 1)和@987654350的立即子表达式 @ 是(throw 0, 0) 的直接子表达式,这是一个潜在的抛出表达式。所以(throw 0, 0) 和B(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>())会隐式调用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&&)应该是noexcept(false)... -
@TedLyngmo 我不认为在评估
noexcept函数的默认参数值时抛出意味着调用std::terminate。我的理解是,默认值是在函数上下文之外评估的,它基本上替换了调用站点的参数。如果你有void foo(int) noexcept和int bar(),那么如果你尝试foo(bar())和bar抛出,foo函数没有抛出,所以不需要std::terminate。在我看来这是同样的情况。
标签: c++ exception language-lawyer noexcept