【问题标题】:Catching exception thrown by constructor seems impossible捕获构造函数抛出的异常似乎是不可能的
【发布时间】:2016-05-12 16:19:04
【问题描述】:

我正在使用一个库,该库需要执行某些函数来初始化库,并且需要执行其他函数来执行“清理”。具体来说,库是OpenGL(带有GLFW和GLEW),初始化函数包括glfwInit()glewInit(),清理函数是glfwTerminate()。不过,确切的库应该无关紧要。

本着 RAII 的精神,我创建了一个 LibraryGuard 类,其构造函数初始化库,其析构函数调用必要的清理函数。

当然,库可能无法初始化。例如,可能没有适当的硬件支持,可能缺少动态库等。为了处理这些情况,我定义了LibraryGuard的构造函数,如果无法初始化库,则抛出异常。

问题是我不知道如何实际捕获这个异常。显而易见的

try {
    LibraryGuard lg;
}
catch () {
    // exit gracefully
}

因为LibraryGuard的析构函数在try块的末尾被调用,如果lg被成功创建,这意味着库清理函数被调用,将不起作用。

我能想到的唯一其他解决方案是 1) 不捕获异常;或 2) 将我的整个 main 函数包含在 try 块中。这两种选择都不是特别可口。

【问题讨论】:

  • 我不确定我是否理解您的意思,但如果构造函数抛出异常,则不会调用析构函数。构造函数没有完成(由return}),所以该对象不被认为是“活动的”
  • 我不太明白你的问题:stackoverflow.com/a/10212864/390913
  • 如果你能想象到的唯一点是 try/catch 块是在整个 main 周围,那么你最好不要捕获异常并让它终止程序。无论如何,这就是你要做的。
  • @KerrekSB,不同意。您应该在 main 中捕获异常,在其中打印消息,然后 abort。这就是我会做的。
  • @AlessandroPower -- 您在 { } 块中声明了一个局部变量,因此您需要在该块中使用它。不管是try / catch 块、if 块、功能块等。仅仅因为它恰好是try / catch 不会改变C++ 的作用域规则。

标签: c++ exception


【解决方案1】:

所有这些都表明正确的解决方案是在您的LibraryGuard 的构造函数中包含这个try{}catch,并确保库保护实际上处理失败的库初始化——这就是您发明它的目的!

所以,当您检测到构造函数中的初始化失败时,请执行您需要执行的操作; 然后抛出异常让你main知道事情已经走下坡路了。

【讨论】:

  • 也许我遗漏了一些东西,但我不明白这有什么帮助。根据gotw.ca/gotw/066.htm,构造函数中抛出的异常,即使被捕获,仍然会导致构造函数抛出。所以我仍然有弄清楚如何捕获构造函数的潜在异常的问题。
  • @AlessandroPower 这与funcntion try-catch block 有关。您可以在构造函数主体中使用通用的,它会正常工作。
  • @AlessandroPower 左轮豹猫是对的——让构造方知道构造函数失败的正确方法是让构造函数抛出异常。但是,如果你有一些东西可以处理你的库的状态,那么即使构造函数失败,该实例也应该负责清理。
【解决方案2】:

不使用构造函数并要求将库初始化为特定方法怎么办?

类似

int main ()
 {
   LibraryGuard  lg;  // constructor do nothing

   // ....

   try
    {
      lg.initialize(); // library initialization
    }
   catch (...)
    {
      // case of initialization failure
    }

ps:对不起,我的英语不好。

【讨论】:

  • 如果没有更好的选择我可能会这样做,但我认为如果我可以让构造函数进行库初始化会更好。
  • @Alessandro Power - 我明白了。好吧,我很好奇这是否可能。
  • @Alessandro Power - 您接受 C++11 解决方案还是也必须使用 C++98?
  • C++11 实际上是首选:)
  • @Alessandro Power - 在 C++11 中有一个缺陷很明显的解决方案;给我几分钟。
【解决方案3】:

我认为,这个问题实际上更笼统,根本问题是关于例外政策。什么构成您的应用程序中的异常?要回答这个问题,您需要回答以下问题:

  1. 什么是不可恢复、不变量杀死错误?
  2. 你可以从他们那里恢复到什么水平?

现在,您的 OpenGL 初始化失败。我不是你的应用程序,但我想,对于首先需要 OpenGL 的任何应用程序,加载 OpenGL 的失败应该是不可恢复的。似乎很难想象你能从中恢复到什么水平?你会恢复什么?

我想说,(对您的应用知之甚少)您最好的做法是在 main 中捕获 const std::exception& e,打印 e.what()std::terminate。至少这是我的风格。

【讨论】:

    【解决方案4】:

    您的解决方案是完全正确的,但有一点遗漏。

    void do_all_my_stuff_with_library() {
        // whatever
    }
    
    int main() {
        try {
            LibraryGuard lg;
            do_all_my_stuff_with_library();
        }
        catch () {
            // exit gracefully
        }
        return 0;
    }
    

    【讨论】:

      【解决方案5】:

      清理函数需要从初始化函数中获取一些东西吗?

      如果不是这样,您可以将您的班级分成 2 个班级:

      1) 一个类 LibraryIn,在构造函数中初始化

      2) 一个类 LibratyOut,其析构函数清理库。

      所以

      int main ()
       {
         LibraryOut  lo;  // constructor do nothing
      
         // ...
      
         try 
          {
            LibraryIn  li;  // constructor initialize library
      
            // destructor of li do nothing
          }
         catch (...)
          {
            // in case of library initialization failure
          }
      
         // ...
      
         return EXIT_SUCCESS;
      
         // destructor of lo clean-up the library
       }
      

      【讨论】:

        【解决方案6】:

        某种指针:

        std::unique_ptr<LibraryGuard> lg;
        try {
            lg.reset(new LibraryGuard());
        }
        catch () {
            // exit gracefully
            lg.release(); // don't know if this is necessary
        }
        

        【讨论】:

        • 这不是违背了 RAII 的目的吗?现在我必须明确地delete lg 而不是让编译器为我做这件事。
        • 你说得有道理,但我说的是某种指针,智能的或原始的
        • @AlessandroPower 这个答案是正确的,除其他外(比如我的^^),基本上你通过设计/必要条件破坏了 RAII 的目的,你通过告诉“问题在于那些是 RAII”.. 你不希望调用析构函数,但这正是 RAII 的含义。 RAII = 调用析构函数(但你不想要..).. perreal 只是使用了一个指针,但最终它的代码完全等同于其他答案的代码。
        • 除非有我没有看到的问题,否则我认为这是最好的解决方案。 @perreal 如果您更新答案以使用智能指针,我会接受。
        • nullptr 不在std 命名空间中,否则我认为这个解决方案很好。
        【解决方案7】:

        您可以移动启用您的类并使用工厂功能:

        LibraryGuard init_library()
        {
            try {
                return LibraryGuard{};
            } catch(/*...*/) {
                //Log stuff
                //Terminate application
            }
        }
        
        //Usage
        auto lg = init_library();
        

        【讨论】:

        • 老实说:这在功能上与我的答案相同,但不那么优雅。为什么不直接把问题放到你的 ctor 中呢?
        • @MarcusMüller 我能看到的唯一原因是,有时允许异常传播可能更可取——库初始化失败并不总是致命的,或者那里可能没有处理它的信息。
        【解决方案8】:

        如果你可以使用C++11,你可以使用std::unique_ptr,所以

        int main ()
         {
           std::unique_ptr<LibraryGuard>  up;
        
           try
            {
              up.reset(new LibraryGuard());
            }
           catch (...)
            {
              // in case of library initialization failure
            }
        
           return 0;
        
           // destructor of LibraryGuard()
         }
        

        【讨论】:

        • 这是一个很好的解决方案,但@perreal 首先提供了“智能指针”解决方案,所以我接受了他的回答。
        • @Alessandro Power - 我没看到;对不起。
        【解决方案9】:

        您可以执行以下操作,并且仍然使用std::unique_ptr 遵守 RAII:

        #include <iostream>
        #include <memory>
        
        class LoggingLibrary
        {
            public:
                LoggingLibrary(int trigger=0) // for demonstration purposes
                { 
                   if ( trigger > 0)
                       throw "Error"; 
                }
        };
        
        using namespace std;
        
        int main() 
        {
            unique_ptr<LoggingLibrary> logLib;
            try 
            {
                logLib = make_unique<LoggingLibrary>(0);
            }
            catch(const char *msg)
            {
                cout << msg << " -- Didn't initialize";
            }
            if ( logLib )
            {
                cout << "Hey I'm ok"; // no exception thrown, so you can do work here
            }
        }
        

        Live Example (no exception thrown)

        Live Example (exception thrown)

        logLib 超出范围时,std::unique_ptr 将调用LoggingLibrary 的析构函数。如果抛出异常,则进入catch 块。

        但是如果你看一下if (logLib)——这会测试智能指针是否有一个关联的对象,如果抛出异常则没有一个std::unique_ptr有一个operator bool()来处理检查这个场景。

        【讨论】:

        • 这是一个很好的解决方案,但@perreal 首先提供了“智能指针”解决方案,所以我接受了他的回答。
        • 我给出的解决方案有一个额外的步骤,可能对你有帮助。查看catch 后面的if 块。从您的原始帖子中,您似乎希望在 try 块中有一个“本地”,然后在构造函数没有失败的情况下在 try / catch 之外使用它——嗯......
        【解决方案10】:

        如果您使用的是类 C 库(GLEW、GLFW),则不会引发异常,因此您会得到的唯一异常是您自己提出的异常,或者您希望捕获这些异常在 main 中,或者您不希望出现异常,

        解决方案是:

        1. 如果您不想处理异常,请不要抛出异常,因为那些 C 类库​​不会自行抛出异常。
        2. 你想抛出异常,那么你必须确保你的对象是异常安全的

        这是最简单的解决方案(如果你想要例外)

        class LibraryGuard{
        
        bool initializedGLFW;
        //other flags
        
             void error(const char* message)}{
                  shutDown();
                  throw std::exception(message);
             }
        
        public:
        
             LibraryGuard(){
                 initializedGLFW = false;
        
                 if(glfwInit()!=GL_TRUE)
                     error("GLFW not initialized");
                 initializedGLFW = true;
        
                 //other libraries
             }
        
             ~LibraryGuard(){
                   shutDown();
             }
        
             void shutDown(){
                   //other libraries (order reversed, sometimes order matter)
        
                   if(initializedGLFW){
                       initializedGLFW = false;
                       glfwTerminate();
                   }
             }
        
        }
        

        如果调用析构函数,这种方式不再有问题(这是非常RAII的方式)。基本上你想创建一个对象,如果对象被完全初始化,那么当你退出“try”块时它的析构函数被调用,如果对象被部分初始化,那么error函数无论如何都会取消初始化它。

        int main(){  
        
            try{
               LibraryGuard lg;
            }catch( std::exception e){
        
            }
        
        
        }
        

        编辑:

        看来你想做这样的事

        //Your new main so you can keep the "lg" alive around ^^
        int runApp(){
        
            LibraryGuard lg;
        
        }
        
        int main(){
            try{
                    runApp();
            }catch{
        
            }
        }
        

        无论你想让图书馆保持活力的原因是什么,你仍然可以执行以下操作(如果这有意义的话),我不是说这是一个好主意,看起来仍然是你想要的行为。

        int main(){ 
            try{
                LibraryGuard lg;
                // object alive here
                try{
                    //other code
        
                }catch(...){
        
                }
            }catch(...){
        
            }
        
        }
        

        【讨论】:

        • Downvoters 像往常一样邀请反馈,OP 明确表示他不想调用析构函数,因为它调用“终止”,但简单的解决方法是我们可以使用库的标志进行跟踪已初始化,因此需要清理。该解决方案有效,如果有问题,OP 将发表评论。我在 excpetion 安全代码方面有可靠的经验,所以我知道自己的方式。
        • 我认为 OP 的问题是 OP 不希望 try 块包含整个 main(),如果他将其缩小,则库不能在 try 块之后使用(调用析构函数并且库取消初始化)
        • 我想你误解了我的问题。您的解决方案仍然会导致构造函数可能抛出;我的问题是我不知道如何捕捉这个异常。
        • 再试一次,我同时编辑了我的答案。但是为什么我被否决了,尝试解决方案会起作用,最重要的是,您应该提供一个代码 sn-ps,说明您想要的主要方式。考虑编辑您的问题,而不是指出其他答案中的问题
        • 我看不出您的提案将如何运作。如果库初始化成功,initializedGLFW就是true,也就是说当lg的析构函数被调用时(而lg的dtor会在try块的末尾被调用),@987654333 @ 将被执行,这会取消初始化库。顺便说一句,我不会对你投反对票。
        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2017-11-07
        • 2014-08-12
        • 2011-11-04
        • 2019-07-11
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多