【问题标题】:C++ exception safety paranoia: how much is too much?C++ 异常安全偏执狂:多少算太多?
【发布时间】:2012-04-12 04:06:58
【问题描述】:

强大的异常安全保证表明,如果发生异常,操作不会改变任何程序状态。实现异常安全复制分配的一种优雅方式是copy-and-swap idiom

我的问题是:

  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;
};

【问题讨论】:

  • 答案很大程度上取决于您的域,并且无法在域之外回答。如果生命依赖于它,答案是显而易见的:它值得付出性能代价。如果异常处理代码会在发生异常时破坏您的对象,那也没关系。现在你必须找到你的代码在中间的位置。
  • 如果您将代码正确地分解为单一职责组件,您通常会“免费”获得很多异常安全性。在您的示例中,为什么“增量”函数会做“疯狂的事情”?要么增量算法很复杂并且知道如何以事务方式进行计算,要么你需要将增量和疯狂填充分开。
  • 好吧,您可以使用相同的想法并将其应用于成员。不需要是全部。如果说你的类包含一个字符串向量,你只需要改变一个怎么办?不要复制/交换整个类、向量和所有内容。只需在一个字符串上执行即可。
  • 旁注:在开始在任何地方使用它们之前,请确保您确实需要异常。见this question

标签: c++ exception-safety


【解决方案1】:

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

【讨论】:

    【解决方案2】:

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

    强大的异常保证(即“事务”)是您认为有意义时应该实施的东西:您并不总是需要事务行为。

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

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

    【讨论】:

    • std::pair<copyable, movable_nothrow> 有什么问题?正如您所说,很难编写pair 使得所有 nothrow 操作要么最后完成,要么(更糟)在非 nothrow 操作抛出时反转。但在一个 nothrow 操作和一个本身提供强异常保证的情况下,确实存在正确的操作序列。
    • @SteveJessop:是的,好点。但是,std::pair 的移动构造函数不会是noexcept。另外,这也说明了我的观点:异常安全是困难的,并且移动语义需要大量的模板元编程赋。您将如何编写 std::pair<movable_nothrow, copyable> 的移动构造函数(注意参数的反转)?您想在first 之前构造second,但这要求它们按此顺序声明。
    【解决方案3】:

    就像所有工程问题一样,它关乎平衡。

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

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

    像所有性能问题一样,我会说:分析并摆脱热点。 Copy And Swap 肯定不是实现事务语义的唯一方法(它只是最简单的),所以分析会告诉你在哪里绝对不应该使用它,你必须找到替代方案。

    【讨论】:

      【解决方案4】:

      是的,您面临的问题是这个习语很难扩展。其他答案都没有提到它,但是 Alexandrescu 发明的另一个非常有趣的成语称为scopeGuards。它有助于提高代码的经济性,并极大地提高了需要遵守强大的异常安全保证的函数的可读性。

      作用域守卫的想法是一个堆栈实例,它允许将回滚函数对象附加到每个资源获取。当范围保护被破坏(通过异常)时,将调用回滚。您需要在正常流程中显式调用 commit() 以避免在范围退出时回滚调用。

      检查this recent question from me,它与使用 c++11 功能设计安全范围保护相关。

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2011-02-28
        • 2011-02-04
        • 1970-01-01
        • 2010-11-04
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多