如何使用std :: exception - 堆栈跟踪加内存泄漏

时间:2015-11-03 06:35:10

标签: c++

我正在努力找出处理异常的最佳方法。

C ++表示任何数据类型都可以作为异常抛出,但是没有办法告诉抛出了什么数据类型,这使得异常基本上毫无价值。但是,我假设这些天每个人都抛出std :: exception,因此在catch中是安全的。这是一个公平的假设吗?

(我正在构建一个将被其他人使用的库,因此必须使用我无法控制的其他代码。)

Exception.what函数返回一个char *(不是字符串),无法释放它。当然,该字符串应包含有关异常的信息,例如,如果找不到文件,则查找未找到的文件的名称。我假设我们只是在抛出异常时容忍一个小的内存泄漏。正确的吗?

目前还不清楚如何可靠地以一般方式生成堆栈跟踪并将其发送到日志文件。 (在出现问题的生产环境中,而不是在调试器中。)所以我想在代码中的有趣位置插入其他多余的try / catch块,这些位置可以添加日志记录并将额外信息附加到什么文本然后重新抛出新的例外。但是,该过程当然会破坏任何堆栈跟踪。例如

foo = stuff();
try {
  processStuff()
} catch (std::exception& ex) {
  string msg = "While processing " + foo.toString() + ": " + ex.what;
  log << msg;
  throw std::exception((char[])msg);
}

这是最好的方法吗?

有很多文章描述了基本课程,但我没有找到关于如何真正让它们在实践中发挥作用的文章。链接赞赏。

(这里我再次尝试用C ++编写Java / .Net。这是一个无望的原因吗?)

2 个答案:

答案 0 :(得分:3)

例外情况
可以抛出什么类型的异常对象是函数/方法的公共契约的一部分。

通常,库可能会引入自己的异常类型,这些异常类型可能会也可能不会从std::exception继承。

推荐:抛出std :: exception或从中继承的异常。记录抛出的异常类型。通常,您将拥有&#34;库默认&#34;,例如

  

除非另有说明,否则此库中的所有方法和函数都可能抛出类型mylib::exception的异常或从其继承的类型。

char * what()
(暗示?)假设只要异常对象有效,指针就是有效的。没有必要手动释放字符串,没有泄漏的理由。

如果您需要存储该文本以供日后使用,请复制该文本 - 例如通过分配到std::string

堆栈痕迹
...不是C ++标准的一部分。此外,大多数编译器实现允许生成不允许重新发现堆栈跟踪的代码。 (相应的选项通常会说&#34;堆栈帧&#34;)。

但是,对于大多数编译器,存在可以添加此堆栈跟踪的库,但这通常需要访问调试符号。

<强>&#34;冗余&#34;尝试/捕获
无论如何你可能还需要它们 完美的例外必须支持不同的方面:

  • 它包含最终用户如何删除阻止问题的说明或提示(&#34;无法打开文件bibbit.txt,因为它是只读的&#34;

  • 它包含有关错误发生原因的支持和开发信息,允许它们在将来更好地避免/处理错误。 (例如......堆栈跟踪)

  • 它需要允许调用代码来检测已知异常并专门处理它们(例如用户想要打开的文件是只读的,询问用户是否要以只读模式打开,然后再试一次

很少有例外情况完美无缺。但是在某些&#34;层重新包装&#34;这里有很多帮助 - 尽管这会让我们失去一些异常之美。

这有帮助吗?

关于您的修改的修改:

通常没问题,但不要重新构建异常。将您的代码更改为:

 ...
 catch (std::exception& ex) {
  string msg = "While processing " + foo.toString() + ": " + ex.what;
  log << msg;
  throw;  // <--- !!!!!!
}

这会重新抛出原始异常,因此进一步向下捕获处理程序仍然可以区分不同的类型:

  void Foo() {
     class random_exception : public std::exception { ... }
     try {
        ...
        if (rand() % 2) throw random_exception();
        ...
     }
     catch(std::exception const & x)
     {
        log << x.what();
        throw;
     }
   } 

   int main()
   {
      try {
        Foo();
      }
      catch(random_exception const & x)
      {
         cout << "A random exception occurred, you can try again";
      }
      catch(std::exception const & x)
      {
         cout << This didn't work. " << x.what(); 
      }
      catch(...) // catches all exception types, but you don't know what
      {
         cout << "Uh, oh, you are doomed.".
      }
   }

当然,重新打包例外时会出现问题。

我倾向于的一些事情:

要抛出的异常类型是(智能)指针周围的瘦包装器。这允许存储异常(例如,将它们传递给另一个线程,或作为&#34;内部异常&#34;重新包装时)而不丢失类型信息:

我为最终用户和诊断提供单独的消息。需要最终用户消息,通常从(并包含)错误代码和上下文生成诊断。用户将看不到&#34;奇怪的数字和技术胡言乱语&#34;除非他们点击&#34;详细信息&#34;。

而不是在错误消息中包含参数:

msg = "Could not open file bibbit.txt (tried 5 times)"; // :-(

我使用固定消息和(名称,值)参数列表。

msg = "Could not open file";  // :-)
msg.Add("filename", filename).Add("retries", retryCount);

这应该简化本地化,并允许调用者访问已知错误的各个属性,从而允许更具体的处理。

答案 1 :(得分:2)

  

但是,我假设这些天每个人都抛出std :: Exception,因此在catch中是安全的。这是一个公平的假设吗?

这大多是一个安全的假设。有些人坚持投掷随机的东西,但这不是你的事 - 如果你是一个图书馆,你抛出异常,最多你抓住自己的,你不应该关心其他人抛出(你的代码应该使用RAII无论如何要避免资源泄漏 - 这甚至涵盖了例如回调抛出你不知道的东西的情况。

  

Exception.what函数返回一个char *(不是字符串),无法释放它。当然,该字符串应包含有关异常的信息,例如,如果找不到文件,则查找未找到的文件的名称。我假设我们只是在抛出异常时容忍一个小的内存泄漏。正确的吗?

不。 what返回的字符串通常不会导致内存泄漏。要么是静态分配的(我看过许多简单的异常类只返回一个字符串文字),要么在异常本身内进行管理(std::runtime_error通常包含std::stringwhat返回c_str方法的结果。一般情况下,只要异常存在,您可以假设只有what结果存在 - 如果您需要在catch块之外将其复制到std::string或其他内容。

这实际上是必需的标准:

  

返回值保持有效,直到获取获取它的异常对象或调用异常对象的非const成员函数为止。

(C ++ 11,§18.8.1¶10)

  

目前还不清楚如何可靠地以一般方式生成堆栈跟踪并将其发送到日志文件。 (在出现问题的生产环境中,而不是在调试器中。)所以我想在代码中的有趣位置插入其他多余的try / catch块,这些位置可以添加日志记录并将额外信息附加到什么文本然后重新抛出新的例外。但是,该过程当然会破坏任何堆栈跟踪。

C ++中的堆栈跟踪是一个悲伤的故事;没有可移植的方法来生成它们,你必须求助于特定于平台的调用(或者抽象它们的库);我个人过去曾使用过几种方法:

  • 在一个新的,多平台的Qt应用程序上,我编写了自己的“胖”异常类,它保存了堆栈跟踪(我从booster::exception派生,它已经捆绑了堆栈跟踪保存部分)并添加了在异常被捕获和重新抛出时添加额外信息的可能性,类似于Boost.Exception的实现方式(重要:Boost.Exception!= booster :: exception,它们完全不相关);看here,它充满了Qt类型,但你可以得到这个想法;
  • 在遗留项目中抛出最奇怪的类型(并且无法将它们更改为常见的异常类型)我只是挂钩(通过链接器技巧)抛出并且我总是将堆栈跟踪保存在全局循环缓冲区中,如果未捕获异常,则打印出来。

您的方法存在缺陷,因为它不仅会丢失堆栈跟踪的一部分,甚至会丢失有关异常的类型信息。查看Boost.Exception如何做到这一点,看看如何正确地做到这一点。此外,你永远不会抛出一个new(=堆分配)异常,否则释放它会成为想要抓住它的人的负担(也许,没有人会抓住它,因为没有一个抓住指针,你通常通过引用捕获)。