异常保证并按值传递
Exception guarantees and pass by value
我最近在几种情况下遇到了这个问题,对此表达的一些意见让我感到惊讶。下面是第一个简单示例:
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的处理。
如果对于T
和U
,则is_assignable
为真,则此表达式有效:std::declval<T>() = std::declval<U>()
。 如果这种表达是完全的,那么is_nothrow_assignable
是正确的。复制/移动分配也是如此(显然具有相同的类型)。
operator=
参数的初始化是表达式的一部分。因此,如果该初始化可以抛出,则赋值不是 nothrow。有一份工作组文件深入讨论了这个问题(主要是围绕移动分配)。
是否应该noexcept
标记此功能的问题是一个单独的问题。但是,不应将仅仅noexcept
函数与认为函数调用表达式的某些部分永远不会抛出的信念相混淆。
我个人对此事的感觉是,您不应该一开始就标记随机函数noexcept
。您应该将它们与特殊的成员函数一起使用,以及您明确需要使用它的其他非常具体的地方。也就是说,它应该不仅仅是用户的语义标记。当有人实际要进行元编程并根据是否noexcept
选择调用或不调用该函数时,请使用noexcept
。
- 何时应通过引用传递矢量参数而不是按值传递矢量参数?
- C++类 - 初始化列表 - 递归 - 按值传递
- 将函数参数完美转发到函数指针:按值传递呢?
- 棘手的按值传递和按引用递归问题传递
- 不同于按值传递和常量引用传递的程序集
- 按值传递变量与按引用传递变量具有相同的结果
- 为什么按值传递QStringView比引用常量更快?
- 获取 std::函数以推断按引用传递/按值传递
- 在函数中按值传递 unordered_map/unordered_set 是否有效? C++
- C++/11 auto 关键字是在更有效时推导参数进行按引用传递,还是始终按值传递?
- 使用 enable_if 在按值传递与按引用传递之间更改函数声明
- 防止在按值传递对象(继承)时进行切片
- 按值传递类和结构
- C++按引用传递还是按值传递?
- 为什么在按值返回时创建临时对象,而不是在按值传递给函数参数时创建临时对象
- 可移动但不可复制的对象:按值传递还是按引用传递?
- const-ref传递的模板化参数是否经过优化,以便在足够小时按值传递
- 在C++中指针是按值传递的吗
- 异常保证并按值传递
- 按值传递异常