可能失败的函数的接收参数和移动语义(强异常安全性)

Sink arguments and move semantics for functions that can fail (strong exception safety)

本文关键字:语义 异常 安全性 移动 参数 失败 函数      更新时间:2023-10-16

我有一个函数,它对作为sink参数传入的一大块数据进行操作。我的BigData类型已经支持C++11,并且带有功能齐全的移动构造函数和移动赋值实现,所以我可以不用复制该死的东西:

Result processBigData(BigData);
[...]
BigData b = retrieveData();
Result r = processBigData(std::move(b));

这一切都很好。但是,我的处理函数可能会在运行时偶尔失败,从而导致异常。这不是一个真正的问题,因为我可以修复东西并重试:

BigData b = retrieveData();
Result r;
try {
    r = processBigData(std::move(b));
} catch(std::runtime_error&) {
    r = fixEnvironmnentAndTryAgain(b);
    // wait, something isn't right here...
}

当然,这是行不通的。

由于我已将数据移动到处理函数中,所以当我到达异常处理程序时,b将不再可用。

这可能会大大降低我通过价值传递水槽论点的热情。

所以问题来了:在现代C++代码中,如何处理这样的情况?如何检索对以前移动到未能执行的函数中的数据的访问权限?

您可以根据需要更改BigDataprocessBigData的实现和接口。然而,最终的解决方案应该尽量减少原始代码在效率和可用性方面的缺陷。

我同样对这个问题感到困惑。

据我所知,目前最好的习惯用法是将pass-by值划分为一对pass-by引用。

template< typename t >
std::decay_t< t >
val( t && o ) // Given an object, return a new object "val"ue by move or copy
    { return std::forward< t >( o ); }
Result processBigData(BigData && in_rref) {
    // implementation
}
Result processBigData(BigData const & in_cref ) {
    return processBigData( val( in_cref ) );
}

当然,在出现异常之前,可能已经移动了参数的各个部分。问题传播到processBigData调用的任何对象。

我得到了一个灵感,开发了一个在某些例外情况下将自己移回源的对象,但这是我的一个项目中即将出现的特定问题的解决方案。它可能最终过于专业化,也可能根本不可行。

显然,这个问题在最近的2014年CppCon上得到了热烈讨论。赫伯·萨特在他的闭幕词《回到基础!现代C++风格要点(幻灯片)。

他的结论很简单:不要对sink参数使用pass-by-value

首先使用这种技术的论点(正如Eric Niebler的Meeting C++2013主题演讲C++11 Library design(幻灯片)所推广的那样)似乎被缺点所压倒。按值传递sink参数的最初动机是为了消除由于使用const&/&&而导致的函数重载的组合爆炸。

不幸的是,这似乎带来了一些意想不到的后果。其中之一是潜在的效率缺陷(主要是由于不必要的缓冲区分配)。另一个是这个问题中的异常安全问题。赫伯的演讲中讨论了这两个问题。

Herb的结论是使用传递值作为sink参数,而是依赖于单独的const&/&&(默认为const&&&保留用于需要优化的少数情况)。

这也符合@Potatoswatter的回答。通过&&传递sink参数,我们可能能够将数据从参数的实际移动推迟到可以提供noexcept保证的点。

我有点喜欢按价值传递水槽论点的想法,但它在实践中似乎并不像每个人希望的那样有效。

思考了5年后的更新:

我现在确信,我的激励性例子是对move语义的滥用。在调用processBigData(std::move(b));之后,我永远不应该被允许假设b的状态是什么,即使函数异常退出。这样做会导致代码难以遵循和维护。

相反,如果b的内容在错误情况下应该是可恢复的,则需要在代码中明确这一点。例如:

class BigDataException : public std::runtime_error {
private:
    BigData b;
public:
    BigData retrieveDataAfterError() &&;
    // [...]
};

BigData b = retrieveData();
Result r;
try {
    r = processBigData(std::move(b));
} catch(BigDataException& e) {
    b = std::move(e).retrieveDataAfterError();
    r = fixEnvironmnentAndTryAgain(std::move(b));
}

如果我想恢复b的内容,我需要沿着错误路径显式地传递它们(在本例中,封装在BigDataException中)。这种方法需要一些额外的样板,但它更惯用,因为它不需要对移动的对象的状态进行假设。