【问题标题】:Good style for handling constructor failure of critical object处理关键对象的构造函数失败的良好风格
【发布时间】:2010-05-02 22:40:17
【问题描述】:

我正在尝试在实例化对象和处理对我的程序至关重要的对象的任何构造函数异常的两种方法之间做出决定,即如果构造失败,程序将无法继续。

我有一个 SimpleMIDIOut 类,它包含基本的 Win32 MIDI 函数。它将在构造函数中打开一个 MIDI 设备并在析构函数中关闭它。如果无法打开 MIDI 设备,它将在构造函数中抛出继承自 std::exception 的异常。

以下哪种方式捕获此对象的构造函数异常更符合 C++ 最佳实践

方法 1 - 堆栈分配的对象,仅在 try 块内的范围内

#include <iostream>
#include "simplemidiout.h"

int main()
{
    try
    {
        SimpleMIDIOut myOut;  //constructor will throw if MIDI device cannot be opened
        myOut.PlayNote(60,100);

        //.....
        //myOut goes out of scope outside this block
        //so basically the whole program has to be inside 
        //this block.
        //On the plus side, it's on the stack so 
        //destructor that handles object cleanup
        //is called automatically, more inline with RAII idiom?
    }
    catch(const std::exception& e)
    {
        std::cout << e.what() << std::endl;
        std::cin.ignore();
        return 1;
    }

    std::cin.ignore();
    return 0;   
}

方法 2 - 指向对象的指针、堆分配、更好的结构化代码?

#include <iostream>
#include "simplemidiout.h"

int main()
{
    SimpleMIDIOut *myOut;

    try
    {
        myOut = new SimpleMIDIOut();
    }
    catch(const std::exception& e)
    {
        std::cout << e.what() << std::endl;
        delete myOut;
        return 1;
    }

    myOut->PlayNote(60,100);

    std::cin.ignore();

    delete myOut;
    return 0;

}

我更喜欢方法 2 中代码的外观,不必将我的整个程序塞进一个 try 块中,但是方法 1 在堆栈上创建对象,因此 C++ 管理对象的生命周期,这在与 RAII 理念相协调不是吗?

我仍然是这方面的新手,因此非常感谢您对上述内容的任何反馈。如果在这种情况下有更好的方法来检查/处理构造函数失败,请告诉我。

【问题讨论】:

  • +1 表示第一个问题问得好。

标签: c++ exception constructor


【解决方案1】:

就个人而言,我更喜欢您使用的第一种样式 - 方法 1 - 将 SimpleMIDIOut 对象分配为 try-catch 块范围的本地对象。

  • 对我来说,try-catch 块的好处之一是它为错误处理代码提供了一个整洁的地方——catch 块——它允许你在一个很好的地方指定你的业务逻辑,可读,不间断的流程。

  • 其次,您指定的 try-catch 块足够通用,可以处理源自 std::exception 的任何异常 - 因此,将大部分程序代码包含在 try-catch 块中并不是坏事。如果最坏的情况发生并且抛出异常,它将防止您的程序意外终止。这可能是您不期望的异常,例如索引超出范围或内存分配异常。

  • 如果您出于可读性原因不喜欢在 try-catch 块中包含大量代码,那没关系,因为 refactor big lumps of code into functions 是很好的设计实践。通过这样做,您可以将主函数本身保持在最少的行数。

看方法二:

  • 您不需要在 catch 块中使用 delete。如果在构造过程中抛出异常,那么无论如何都没有要删除的对象。

  • 您是否考虑过计划如何满足myOut-&gt;PlayNote 可能抛出的任何std::exception?它超出了您的 try-catch 范围,因此这里的异常会意外终止程序。这就是我上面第二个子弹的目的。

如果您决定将大部分程序包装在该 try-catch 块中,但仍想动态分配 SimpleMIDIOut 对象,则可以通过使用 auto_ptr 来简化内存管理在发生异常时为您管理内存:

try
{
    std::auto_ptr<SimpleMIDIOut> myOut(new SimpleMIDIOut());
    myOut->PlayNote(60,100);
    std::cin.ignore();
} // myOut goes out of scope, SimpleMIDIOut object deleted
catch(const std::exception& e)
{
    std::cout << e.what() << std::endl;
    return 1;
}


return 0;

...但是您也可以将SimpleMIDIOut 对象创建为本地对象而不是动态对象。

【讨论】:

  • 感谢您的详细回复。深思熟虑。在美学层面上,将它全部嵌入到 try/catch 块中对我来说似乎很奇怪,我想对该代码结构进行一些健全性检查。
  • 这听起来可能很奇怪,但实际上将main 中的代码包含在try/catch 块中以防止意外终止(并记录异常以进行分析)是一种非常好的做法。我应该注意,这是一种绝望的措施,仅适用于 main 和类似 main 的函数,不要习惯将每个函数体都包含在 try/catch 中;)
【解决方案2】:

您能否说明您是否可以控制 SimpleMIDIOut 中的源代码?

如果您这样做,则不应从 CTOR 抛出异常。该类的 CTOR 中的代码应该用 try\catch 块包装。

如果你不这样做,那么为了清楚起见,我会使用方法 1 - 整个程序必须在这个 try 块中的事实可以通过重构为小方法来解决。

【讨论】:

  • 从构造函数中抛出异常是完全有效的,以向调用者指示由于某种原因无法完成构造。
  • 它是有效的——但在我看来这是一个糟糕的设计,可能导致运行时异常
  • @drelihan 另一种方法是允许构造一个处于不可用状态的对象,然后等待它在某处使用。恕我直言,这是一个更糟糕的设计选择。
  • @sgreeve 公平点 - 但是 C++ 只销毁完全构造的对象,并且对象在其构造函数完成之前不会完全构造。这可能会导致未定义的行为 - 特别是如果已分配新内存等...
  • @drelihan 也是一个非常公平的观点。一个编写良好的构造函数的参数,如果它失败,它会清理它分配的内存。
猜你喜欢
  • 2011-02-11
  • 1970-01-01
  • 1970-01-01
  • 2015-02-24
  • 2016-07-17
  • 2012-07-05
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多