【问题标题】:Providing strong guarantees in methods/classes that were not designed with exception-safety in mind在设计时未考虑异常安全的方法/类中提供强有力的保证
【发布时间】:2013-08-02 09:27:57
【问题描述】:

我有一个设计问题。先说这段代码

struct Trial{
    const double a;
    Trial(double a_) : a(a_){}
};

int main(){
    Trial t1(1.);
    Trial t2(2.);
    t1 = t2;
}

不编译,因为Trial::operator= 不是由编译器默认构建的,因为Trial::aconst。这非常明显。

现在重点是,代码优先

struct Trial{
    const double a;
    Trial(double a_) : a(a_){}
};

struct MyClass : private Trial{
    MyClass(double a_) : Trial(a_), i(0) {};
    void wannaBeStrongGuaranteeMemberFunction(){
        MyClass old(i);
        bool failed = true;
        //... try to perform operations here
        if(failed)
            *this = old;
    }
    unsigned int i;
};

int main(){
    MyClass m1(1.);
    m1.wannaBeStrongGuaranteeMemberFunction();
}

我需要为类的某些方法提供强大的异常安全性,该类派生自Trial。此类方法对无穷无尽的成​​员系列(示例中的i)执行一系列无穷无尽的操作,这使得“手动”恢复操作变得不切实际。因此,我决定最好复制整个类,如果出现任何问题,再将其复制回来。

小括号,代码只是一个例子。遗留的真实世界代码中的一切都复杂得多。 在此示例中,仅复制 i 就可以了,但在实际代码中并非如此。 此外,这些操作具有多个(且复杂的)执行路径,因此将它们“读取”为“事务”会很痛苦。 此外,我正在使用范围保护来实现这一点,因此可以在实际代码中正确管理异常。

当然整个事情没有编译,因为*this = old这一行。

您将如何解决问题/解决问题?

【问题讨论】:

  • 疯狂的想法:this->~MyClass(); new (this) myClass(old);
  • @StefanoFalasca 你需要include <new>
  • @jrok 非常疯狂。如果副本通过异常退出会发生什么?你留下了一个你不能调用析构函数的对象。如果是本地对象,你就完蛋了,如果是动态分配的,你不能删除它,所以至少,你有内存泄漏。
  • 重点是placement new调用的复制构造函数可能会抛出,此时*this的析构函数已经被调用。
  • @StefanoFalasca 我明确指出:如果您因异常离开复制构造函数会发生什么。您已经破坏了对象,但还没有重建它,因此如果没有未定义的行为(可能还有实践中的各种问题),您无法再次破坏它。

标签: c++ exception-handling error-handling


【解决方案1】:

显而易见的答案是修改Trial,使其支持 任务也是。除此之外,如果你的唯一原因是 想支持任务就是提供强有力的保障, 你可以实现自己的赋值运算符,最好 私有的,它忽略了基类;因为你知道 两个基类将相同,无需分配 他们之间。

请注意,强保证通常涉及交换而不是 任务。这不会改变问题:你不能交换 Trial 的两个版本。你很可能有 类似:

class MyClass : private Trial
{
    class Protected
    {
        bool myCommitted;
        MyClass* myOwner;
        MyClass myInstance;
    public:
        MyClass( MyClass& owner )
            : myCommitted( false )
            , myOwner( &owner )
            , myInstance( owner )
        {
        }
        ~MyClass()
        {
            if ( myCommitted ) {
                myOwner->swap( myInstance );
            }
        }
        MyClass& instance() { return myInstance; }
        void commit() { myCommitted = true; }
    };

public:

    void protectedFunc()
    {
        Protected p( *this );
        p.instance().unprotecedVersionOfFunc();
        //  ...
        p.commit();
    }

任何异常都会使对象保持不变。 (你可以, 当然,颠倒逻辑,对this进行修改, 如果未提交则交换。)做事的优势 这样,您还将“撤消”内存中的任何更改 分配等

最后:你真的想在Trial 中使用一个 const 成员吗? 实现这一点的通常方法是使a 非常量但私有,并且只提供一个吸气剂。这表示 Trial::a 实际上是 const,except 表示完整 任务。

【讨论】:

  • 你说得对。使成员 const 的唯一一点是,我想确保没有其他人试图修改它而不知道他/她不应该这样做。重点是:该类是从 Trial 派生的,其唯一目的是继承其受保护的成员。我不希望派生类修改这些成员。我知道这很糟糕,但这不是我的设计。
  • 不颠倒逻辑的另一个好处是,在最坏的情况下(正常操作)您只复制一份。
  • 受保护的析构函数应该是 no-throw
  • @StefanoFalasca 我通常只复制一份,不管怎样。我会在最后得到我想要的正确版本(而且我的交换功能非常便宜,因为浅交换总是可以接受的)。
  • @StefanoFalasca All 析构函数应该是 no-throw。 (嗯,几乎所有。)但我不会这样声明它们,因为如果我写throw(),一些编译器可能会警告这是不推荐使用的,如果我写noexcept,其他编译器将无法理解。 (当然,swap 也是如此。使用swap 的要点之一是它总是可以浅层实现,因此它永远不必获取新资源,或者做任何可能失败的事情。)跨度>
猜你喜欢
  • 1970-01-01
  • 2021-03-24
  • 1970-01-01
  • 1970-01-01
  • 2011-10-26
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多