【问题标题】:Exception guarantees and pass by value异常保证和按值传递
【发布时间】:2016-01-15 05:30:05
【问题描述】:

我最近在几种情况下遇到过这个问题,对此表达的一些意见让我感到惊讶。这是第一个简单的例子:

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

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

再举一个涉及另一种异常保证的例子,考虑:

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 不提供强有力的保证,因为在调用 g 之前q 不是空的(记住std::move 实际上并没有移动任何东西)。它可能会失败:参数评估的顺序未指定,因此可以先构造 y 清空 q,然后构造 x 并抛出。即使它在技术上不是发生在体内,而是在调用函数的行为中,函数是否仍然对此负责?

编辑:我要提到 Herb Sutter 在这里谈论这个问题:https://youtu.be/xnqTKD8uD64?t=1h11m56s。他说标记这样的函数 noexcept 是“有问题的”;希望得到更详细的答复。

【问题讨论】:

  • 我现在找不到文本,但是函数异常规范肯定只有在输入函数体后才开始。调用者可以控制参数的行为,例如我希望noexcept(f(vec))falsenoexcept(f(std::move(vec)))true
  • set 的例子很糟糕。你会测试像noexcept(comp(a, b)) 这样的东西,这已经考虑到了复制。
  • @M.M 我同意。问题是它是否应该在精神上被标记为 noexcept,或者这是否只是利用一种边缘情况。调用者可以控制参数的行为,但是 noexcept 函数不应该与任何可以调用的参数一起抛出,对吧?
  • @T.C.那是个很好的观点。我手边没有更好的例子,但我肯定全神贯注。我认为尽管可以看到可能存在更好的示例。
  • 我不认为这就是它的意思。 noexceptf 仍然有用,因为调用代码仍然可以基于此进行优化,即使调用代码准备捕获构造参数时发生的异常。 (它知道不会有任何其他例外)

标签: c++


【解决方案1】:

标准(5.2.2 函数调用 [expr.call] §4)说参数初始化和销毁​​发生在调用函数的上下文中。所以是的,从技术上讲,将f 声明为noexcept 是合乎道德的。

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

同时考虑带有 const-ref 参数的函数:

void x(std::string const&) noexcept {}

void y() noexcept
{
    x("foo");
}

xnoexcept 好吧。但是,y 中的调用不是,因为它包含一个(可能抛出的)隐式转换。因此,如果将f 声明为noexcept 是不好的风格,那么x 是否也必须这样说?

所以我认为 C++ 程序员应该做以下两件事之一:不使用也不依赖 noexcept 规范,或者内化规则并始终对他们在调用站点所做的事情感到厌倦。


关于第二个问题(强力保证)... 我想说一个函数永远不能声称它被调用的 arguments。此外,如果它从未真正被调用过,它也永远无法声明其可能的参数会发生什么。

如果您心目中的强保证包括此类声明,那么我同意该功能不能提供这种强保证。

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

OTOH,如果这样的声明对于强有力的保证来说不是必要的,那么我认为我没有理由“禁止”它。例如

void StrongAppendAll(std::vector<T>& a, std::vector<T> b);

IMO 对此函数的“自然”强有力的保证是 它将b 中的所有元素附加到a,或者,如果抛出异常,则保持a 不变。我永远不会假设保证包括任何关于b 发生情况的索赔。更不用说关于初始化b的任何参数会发生什么。

【讨论】:

  • 不标记随机函数怎么样noexcept
  • 是的,怎么样?我不是在做广告标记一切,它是妈妈noexcept。我只是说我没有看到一个很好的理由反对声明此类功能noexcept。假设for 这样做是有充分理由的。
  • @PaulGroke 我觉得这对于 noexcept 案例来说非常重要,并且肯定在某种程度上改变了我对这些案例的看法。我仍然认为,尽管在第二个示例中,如果有人将 g 记录为具有强有力的保证,我会很生气。恕我直言,如果您想做出强有力的保证,没有真正的借口不只是参考。
  • @Nir Friedman:你问题的“强有力的保证”部分对我来说很难用语言表达,这就是我一开始忽略它的原因。而且我不确定我现在是否做得很好,即使在花了相当长的时间之后:)
  • @Paul Groke 我认为这是一个有见地的答案,感谢您抽出宝贵时间。
【解决方案2】:

问题是:将 f 记录或描述为提供不抛出保证是否可以接受?

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

is_assignable 为真,如果对于TU,此表达式有效:std::declval&lt;T&gt;() = std::declval&lt;U&gt;()is_nothrow_assignable 为真,如果该表达式 全部为 noexcept。复制/移动分配也是如此(显然具有相同的类型)。

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

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

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

【讨论】:

  • 我同意不将随机函数标记为 no except,但请考虑统一赋值运算符的示例,这是导致我提出这个问题的示例之一。我认为统一赋值运算符不好。如果移动和复制有单独的赋值运算符,则可以将前者标记为 noexcept,但不能将后者标记为 noexcept。然而,其他人认为将统一赋值运算符标记为 noexcept 是可以接受的。你对此有何感想?
  • 啊,我在另一个帖子上看到了你的回复,没关系。很好的答案,谢谢!
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2014-08-23
  • 2015-05-23
  • 2011-01-30
  • 2020-09-23
  • 2014-04-04
  • 1970-01-01
相关资源
最近更新 更多