【问题标题】:RAII approach to catching constructor exceptions捕获构造函数异常的 RAII 方法
【发布时间】:2013-01-20 13:04:10
【问题描述】:

我有一个可以在其构造函数中抛出异常的类。如何在 try/catch 块中声明该类的实例,同时仍使其在正确的范围内可用?

try { MyClass lMyObject; }
catch (const std::exception& e) { /* Handle constructor exception */ }

lMyObject.DoSomething(); // lMyObject not in scope!

在尊重RAII 成语的同时,有没有其他方法可以做到这一点?

我不希望将init() 方法用于两阶段构建。我唯一能想到的其他事情是:

MyClass* lMyObject;

try { lMyObject = new MyClass(); }
catch (const std::exception& e) { /* Handle constructor exception */ }

std::shared_ptr<MyClass> lMyObjectPtr(lMyObject);
lMyObjectPtr->DoSomething();

工作正常,但我对范围和指针间接中的原始指针不满意。这只是另一个 C++ 疣吗?

【问题讨论】:

标签: c++ exception constructor raii


【解决方案1】:

如果构造函数抛出,则意味着对象无法初始化,因此无法开始存在。

MyClass* lMyObject;
try { lMyObject = new MyClass(); }
catch (std::exception e) { /* Handle constructor exception */ }

上面如果构造函数抛出异常,lMyObject 未初始化,换句话说,指针包含一个不确定的值。

详细解释见经典Constructor Failures

我们可以将 C++ 构造函数模型总结如下:

要么:

(a)构造函数到达其结束或返回语句正常返回,对象存在。

或者:

(b) 构造函数抛出异常退出,对象不仅现在不存在,而且从未存在过。

没有其他可能。

【讨论】:

    【解决方案2】:

    你不需要使用shared_ptr,使用unique_ptr

    std::unique_ptr<MyClass> pMyObject;
    try { pMyObject.reset(new MyClass()); }
    catch (std::exception &e) { /* Handle constructor exception */ throw; }
    MyClass &lMyObject = *pMyObject;
    
    lMyObject.DoSomething();
    

    显然,您有责任确保程序在未初始化 pMyObject 或退出函数(例如通过 returnthrow)的情况下不会落入 catch 块。

    如果可用,您可以使用 Boost.Optional 来避免使用堆内存:

    boost::optional<MyClass> oMyObject;
    try { oMyObject.reset(MyClass()); }
    catch (std::exception &e) { /* Handle constructor exception */ throw; }
    MyClass &lMyObject = *oMyObject;
    
    lMyObject.DoSomething();
    

    【讨论】:

    • @MaximYegorushkin 这完全取决于/* Handle constructor exception */ 的作用。
    • 对您的建议的一个很好的测试是将/* Handle constructor exception */ 替换为一个空字符串。您的建议未通过此测试。
    • @MaximYegorushkin 也不会取消引用 NULL 指针;每 20.7.1.2.4p1 get() != nullptroperator* 的先决条件,因此它实际上是未定义的行为。
    • @MaximYegorushkin 什么都不做是处理异常;它忽略了它。
    • 一旦控制流进入catch 块,异常被视为已处理。
    【解决方案3】:

    您可以设置 MyClass 的复制构造函数来接受垃圾输入,从而有效地用您的对象声明声明一个指针。然后你可以manually call try 块中的默认构造函数:

    MyClass lMyObject(null); // calls copy constructor
    try {
        new (lMyObject) MyClass(); // calls normal constructor
    }
    catch (const std::exception& e) { /* Handle constructor exception */ }
    
    lMyObject.DoSomething();
    

    【讨论】:

    • 它调用MyClass的构造函数两次,析构函数一次。
    • 正确 - 要使此模式起作用,您必须编写复制构造函数,使其不分配任何资源。这有点骇人听闻。
    【解决方案4】:

    构造函数用于将对象置于一致状态并建立类不变量。允许异常逃逸构造函数意味着该构造函数中的某些内容未能完成,因此现在对象处于某种未知的奇怪状态(现在也可能包括资源泄漏)。由于对象的构造函数还没有完成,编译器也不会导致它的析构函数被调用。也许您正在寻找的是在构造函数中捕获异常。假设它没有被重新抛出,这将导致构造函数完成执行,并且对象现在已经完全形成。

    【讨论】:

      【解决方案5】:

      编写代码的最佳方式是:-

      MyClass lMyObject; 
      lMyObject.DoSomething(); 
      

      没有尝试、捕获或指针。

      如果构造函数抛出异常,则无法调用 DoSomething。这是正确的:如果构造函数抛出,那么对象永远不会被构造。

      而且,重要的是,不要捕获(甚至捕获/重新抛出)异常,除非您对它们有建设性意义。让异常发挥它们的作用,并不断产生影响,直到知道如何处理它们的东西可以发挥作用。

      【讨论】:

        猜你喜欢
        • 2013-10-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2013-09-07
        • 1970-01-01
        • 2014-07-06
        • 1970-01-01
        相关资源
        最近更新 更多