异常保证并按值传递

Exception guarantees and pass by value

本文关键字:按值传递 异常      更新时间:2023-10-16

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

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 并抛出。即使它在技术上没有发生在体内,而是在调用函数的行为中,函数是否仍然对此负责?

编辑:我要提到赫伯·萨特在这里谈论这个问题:https://youtu.be/xnqTKD8uD64?t=1h11m56s。他说,将这样的功能标记为noexcept 是"有问题的";我希望得到更详细的回应。

标准(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");
}

x noexcept没事。但是,y中的调用不是,因为它包含(可能抛出)隐式转换。因此,如果宣布f noexcept是不好的风格,那么x不是必须这样说吗?

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


关于第二个问题(有力保证)...我会说一个函数永远不能对它被调用的参数做出声明。此外,如果它从未被实际调用,它永远无法声明其潜在参数会发生什么。

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

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

OTOH,如果这样的主张对于强有力的保证具有实际意义不是必需的,那么我认为我没有理由"禁止"它。 例如

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

IMO这个函数的"自然"有力保证是它要么将所有元素附加到b a,要么,如果抛出异常,a保持不变。我永远不会假设保证包括任何关于b发生的事情的索赔。更不用说任何关于用于初始化b的参数会发生什么的主张了。

问题是:是否可以将f记录或描述为提供无抛掷保证?

C++标准不认为是这样。当应用于分配时,请考虑其对nothrow的处理。

如果对于TU,则is_assignable为真,则此表达式有效:std::declval<T>() = std::declval<U>() 。 如果这种表达是完全的,那么is_nothrow_assignable是正确的。复制/移动分配也是如此(显然具有相同的类型)。

operator=参数的初始化是表达式的一部分。因此,如果该初始化可以抛出,则赋值不是 nothrow。有一份工作组文件深入讨论了这个问题(主要是围绕移动分配)。

是否应该noexcept标记此功能的问题是一个单独的问题。但是,不应将仅仅noexcept函数与认为函数调用表达式的某些部分永远不会抛出的信念相混淆。

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