C++异常安全妄想症:多少就是太多

C++ exception safety paranoia: how much is too much?

本文关键字:太多 多少 异常 安全 妄想症 C++      更新时间:2023-10-16

强大的异常安全保证表示,如果发生异常,操作不会更改任何程序状态。实现异常安全复制分配的一种优雅方法是复制和交换习惯用法。

我的问题是:

  1. 对一个类的每个变异操作使用复制和交换来变异非基元类型,这会不会太过分了?

  2. 性能真的是很好的异常安全的公平交易吗?

例如:

class A
{   
    public:
        void increment()
        {   
            // Copy
            A tmp(*this);
            // Perform throwing operations on the copy
            ++(tmp.x);
            tmp.x.crazyStuff();
            // Now that the operation is done sans exceptions, 
            // change program state
            swap(tmp);
        }   
        int setSomeProperty(int q)
        {   
            A tmp(*this);
            tmp.y.setProperty("q", q); 
            int rc = tmp.x.otherCrazyStuff();
            swap(tmp);
            return rc;
        }   
        //
        // And many others similarly
        //
        void swap(const A &a) 
        {   
            // Non-throwing swap
        }   
    private:
        SomeClass x;
        OtherClass y;
};

您应该始终以基本的异常保证为目标:确保在发生异常时,所有资源都被正确释放,并且对象处于有效状态(可以是未定义的,但有效)。

当您认为有意义时,应该实现强异常保证(即"事务"):您并不总是需要事务行为。

如果很容易实现事务操作(例如,通过复制和交换),那么就去做吧。但有时不是这样,或者它会对性能产生很大影响,即使是对赋值运算符这样的基本操作也是如此。我记得我实现了类似boost::variant的东西,在那里我不能总是在副本分配中提供强有力的保证。

您将遇到的一个巨大困难是移动语义。您在移动时确实需要事务,因为否则会丢失移动的对象。然而,您不能总是提供强有力的保证:想想std::pair<movable_nothrow, copyable>(并查看评论)。这就是你必须成为noexcept大师的地方,并使用大量令人不舒服的元编程。由于异常安全性,C++很难精确地掌握

作为工程的所有事项,它都是关于平衡的。

当然,const-ness/不变性和强大的保证可以增加对代码的信心(尤其是伴随测试)。它们也有助于减少错误的可能解释空间。

但是,它们可能会对性能产生影响。

就像所有的性能问题一样,我会说:配置文件并消除热点。复制和交换当然不是实现事务语义的唯一方法(它只是最简单的),所以分析会告诉你在哪里绝对不应该使用它,你必须找到替代方案。

这取决于你的应用程序将在什么环境中运行。如果你只是在自己的机器上运行它(光谱的一端),那么对异常安全过于严格可能是不值得的。如果你正在编写一个程序,例如用于医疗设备(另一端),你不希望在发生异常时留下无意的副作用。介于两者之间的任何东西都取决于对错误的容忍度和可用于开发的资源(时间、金钱等)

是的,您面临的问题是这个习惯用法很难扩展。其他答案都没有提到它,但亚历山德雷斯库发明了另一个非常有趣的成语,叫做scopeGuards。它有助于增强代码的经济性,并大大提高需要符合强大异常安全保证的函数的可读性。

作用域保护的概念是一个堆栈实例,它只允许将回滚函数对象附加到每个资源请求。当作用域保护被破坏(通过异常)时,将调用回滚。您需要在正常流中显式调用commit(),以避免在作用域出口处进行回滚调用。

查看我最近提出的这个问题,该问题与使用c++11功能设计安全的scopeguard有关。