异常保证和值传递

时间:2016-01-15 05:30:05

标签: c++

我最近在几个背景下遇到过这个问题,并且表达的一些观点让我感到惊讶。这是第一个简单的例子:

void f(std::vector<double> x) {};

问题是:将f记录或描述为提供无投保证是否可以接受?同样,我怀疑由于异常不是从f的身体生成的,因此使用noexcept在技术上是犹太洁食。但是应该标记它noexcept吗?例如,set的优化版本会以某种方式发现添加模板化比较器不能抛出的要求很有用。它在编译时使用静态断言检测到这种情况并导致错误。然而,有人可以为按值获取的向量编写比较器,并将其标记为noexcept,并将其与此版本的set一起使用。如果这导致不良行为,那么容器作者的错吗?或者标记比较器的人没有?

要采用涉及其他类型的异常保证的另一个示例,请考虑:

void g(std::vector<double> x, std::unique_ptr<int> y);

这个功能能提供强有力的保证吗?

std::vector<double> p{1.0, 2.0};
auto q = std::make_unique<int>(0);
bool func_successful = true;

try {
  g(p, std::move(q));
}
catch (...) {
  func_successful = false;
}

if (!func_successful)
  assert(q);

我认为如果断言可能会失败,那么g就不会提供强有力的保证,因为q在调用g之前不是空的(记住std::move实际上并没有移动任何东西)。并且它可能失败:参数评估的顺序是未指定的,因此可以首先构造y以清空q,然后构造和抛出x。功能是否仍然对此负责,即使它在技术上不是在身体中发生,而是在调用函数的行为中?

编辑:我要提到Herb Sutter在这里谈到这个问题:https://youtu.be/xnqTKD8uD64?t=1h11m56s。他说,将这样的功能标记为“不存在”是“有问题的”;我希望得到更详细的回复。

2 个答案:

答案 0 :(得分:2)

标准(5.2.2函数调用[expr.call]§4)表示参数初始化和销毁​​发生在调用函数的上下文中。所以是的, 在技术上是犹太人将f声明为noexcept

虽然一开始就反直觉,但确实有意义。可以使用rvalue调用f,在这种情况下,move-ctor将用于初始化参数,因此即使整个调用函数的构造(包括参数设置和拆除)仍然是{{ 1}}。或者noexcept可以使用空向量调用,在这种情况下,整个构造仍然有效地f - 至少我认为“合理”的库实现。

还要考虑带有const-ref参数的函数:

noexcept

void x(std::string const&) noexcept {} void y() noexcept { x("foo"); } x好吧。然而,noexcept中的调用不是,因为它包含(可能抛出)隐式转换。因此,如果将y声明为f是不好的风格,那么对于noexcept是否也不一样?

所以我认为C ++程序员应该做两件事之一:不使用而不是依赖x规范,或者内化规则并且总是厌倦他们在呼叫站点所做的事情。

关于第二个问题(强有力的保证)...... 我会说一个函数永远不会声称它被调用的参数。此外,如果从未实际调用它,它永远不会声称它的参数会发生什么。

如果你想到的强烈保证包括这样的主张,那么我会同意这个功能不能给予那种强有力的保证。

虽然我不一定同意强有力的保证 - 对于具有这种签名的功能 - 应该包括此类声明。 我 所说的是:如果这样的声明对于强制保证具有任何实际意义是必要的,那么该函数不应该使用按值参数。

OTOH,如果这种说法对于实际意义上的强有力保证不是必要的,那么我认为没有理由“禁止”它。 E.g。

noexcept

IMO对此功能的“自然”强有力保证是它将void StrongAppendAll(std::vector<T>& a, std::vector<T> b); 中的所有元素附加到b,或者,如果抛出异常,则离开{{1 。我永远不会认为担保包括对a发生的事情的任何声明。对于用于初始化a的任何参数所发生的任何声明,甚至更少。

答案 1 :(得分:1)

  

问题是:记录或描述f提供无投保证是否可以接受?

C ++标准不相信它。在应用于作业时考虑它对nothrow的处理。

如果对于is_assignableT,此表达式有效,则{p> U为真:std::declval<T>() = std::declval<U>()。如果表达式是完整的noexcept,则is_nothrow_assignable为真。复制/移动分配也是如此(显然具有相同的类型)。

operator=参数的初始化是表达式的一部分。因此,如果初始化可以抛出,那么赋值不是没有。有一个working group paper discussing this matter in some depth(主要围绕移动分配)。

是否应该标记此功能noexcept的问题是另一回事。但是一个函数只是noexcept不应该与函数调用表达式的部分永远不会抛出的信念相混淆。

我个人对此事的感觉是你不应该开始标记随机函数noexcept。您应该将它们与特殊成员函数和其他具有明确需要的特定位置一起使用。也就是说,它应该不仅仅是用户的语义标记。当某人实际要进行元编程并选择根据是否noexcept来调用或不调用该函数时,请使用noexcept