如何在C ++中记录异常情况?

时间:2009-11-25 14:41:25

标签: c++ exception logging exception-handling

编写函数时,我的实现经常看起来像这样:

  • 调用子功能
  • 如果此子功能无法执行(由于异常情况):记录此故障并中止当前功能
  • 否则继续调用其他子功能,这反过来可能会失败

关键部分是伐木。每个失败的函数都应该在日志中添加简短描述。这样,在处理异常的级别,可以向用户显示详细的错误消息。

例如,考虑一个可以创建新用户帐户的应用程序,并且数据库连接存在问题。以下反向堆栈跟踪结果:

  • SQLDriverConnect() - > “SQLDriverConnect错误:未找到数据源且未指定默认驱动程序”
  • OpenDatabaseConnection() - > “无法打开数据库连接”
  • CreateUser() - > “无法创建新用户”
  • ValidateAndSaveNewUserAccount() - > “无法保存用户个人资料”
  • 捕获异常并向用户显示记录的消息

使用例外功能,我将按如下方式实现:

void CreateUser()
{
    try {
        OpenDatabaseConnection();
    }
    catch(std::exception& e) {
        e.AddLog("Failed to create the new user");
        throw;
    }
    //...
}

使用简单的返回值,我会写下以下内容:

bool CreateUser(Log& log)
{
    if (!OpenDatabaseConnection(log))
    {
        log.Add("Failed to create the new user");
        return false;
    }
    //...
    return true;
}

我发现两种实现都同样好。因此,我没有看到使用异常的优势。我很清楚异常处理通常被认为是一个有用的功能,但我真的不明白为什么。很久以前,我广泛使用异常处理,但我没有看到它的大优势,所以现在我再也不用它了。因此我的问题是:

  • 使用异常,如何更简洁地在C ++中实现?
  • 如果没有,那么抛出异常而不是返回“成功”布尔值会有什么好处?

注意:我使用术语记录作为“收集对错误的解释,以便以后可以将其呈现给用户”。我宁愿不将该解释存储在日志消息的全局集合中(在内存中,在文件中或在数据库中),因为它直接描述了特定的异常。

更新:感谢您的回复。我知道只有当用户不需要有关错误的详细反馈时,例外才有用。 (如果我误解了这个,请纠正我。)

7 个答案:

答案 0 :(得分:6)

您的策略似乎避免了异常的最有用方面,您可以抛出一个异常类,其中已经包含日志信息的文本 - 这是在异常抛出时不生成日志的文本捕获异常的时间。然后你不必抓住每个级别上升到堆栈,但只是在顶层。

因此只有一个try块和一个log.add - 一般来说代码少得多。 像这样的东西似乎删除了你的所有复制。

void OpenDatabaseConnection()
{
   if (Error) throw MyException("Failed opening database");
}

void CreateUser()
{
    try {
        OpenDatabaseConnection();
        //...... do everything here
    }
    catch(MyException& E) { //only one copy of this code
        E.AddLog(E.getMessage());
        throw;
    }
}

答案 1 :(得分:2)

我认为在这里使用异常的一个重要案例是,您现在已经将日志记录作为方法签名的一部分。总的来说,我认为不应该这样,因为这是一个贯穿各领域的问题。例如,想象一下尝试用用户权限做类似的事情。你打算到处写这个吗?

bool CreateUser(Log& log, UserSecurityContext& u) {
    if (!HasPermissionsFor(u, SecurityRoles::AddUser)) {
        log.Add("Insufficient permissions");
        return false;
    }
    //...
    return true;
}

还有其他原因想要使用异常(请参阅Elemental's answer),但只要不使用语言功能会影响软件的设计,就值得考虑这是否是正确的方法它

答案 2 :(得分:2)

如果您总是希望在通话结束后立即处理您的异常情况,那么没有真正的优势。

当您想要处理调用链上几层的条件时,优势就出现了。要用你的成功标志做到这一点,你必须将标志冒出几层子程序调用。每一层都必须用知识来编写,它必须在代码的内部跟踪特殊标志。这只是PITA的主要推广。

例如,对于实时工作,我们通常围绕迭代循环构建应用程序。循环期间的任何错误通常只是中止循环的迭代(除了“致命”错误,这会中止整个应用程序)。处理此问题的最简单方法是从发生的任何地方抛出异常,并在应用程序最外层的自己的catch块中处理它们。

答案 3 :(得分:1)

异常处理从正常控制流中删除错误处理。这样,代码结构更加干净。异常处理也会自动展开堆栈。这样,您就不必在出错的方法中调用每个方法中包含错误处理代码。如果您需要其中一项功能,请使用例外情况。如果不这样做,请使用错误代码或任何其他方法,因为异常会产生成本(计算时间),即使它们没有被抛出。


您的评论的其他答案。想象一下代码,它会调用几个可能失败的函数。

procedure exceptionTest(...)
{
  try
  {
    call1();
    call2();
    call3();
    call4();
  } 
  catch (...)
  {
    //errorhandling outside the normal control flow
  }
}

无一例外:

procedure normalTest(...)
{
   if (!call1())
   {
     //errorHandling1
   } 
   else if (!call2())
   {
     //errorHandling2
   }
   else if ....
   ...
}

您可以很容易地看到,正常的控制流程因错误处理而中断。与此代码相比,使用异常的代码更易于阅读。

如果您需要在每个调用的方法中添加错误处理,则异常可能无法带来好处。但是如果你有嵌套调用,每个调用都可能产生错误,那么在顶层捕获异常可能更容易。我正是这个意思。在你的例子中并非如此,知道从异常中受益的地方仍然是好的。

答案 4 :(得分:1)

例外情况仅在极端情况下使用。执行异常太慢了。对于日志不是很大的错误,请尝试使用返回值。

示例:

int someMethod{
    if(erorr_file_not_found){
        logger.add("File not found");
        return 1;
    }

    if(error_permission){
        logger.add("You have not permissons to write this file");
        return 2;
    }

    return 0;
}

在这种情况下,您可以打印错误并在更高级别处理此错误。

或(更复杂):

int someMethod{
    int retval=0;
    if(someshit){
        retval=1;
        goto _return;
    }
    //...
    _return:
    switch(retval){
        case 1:logger.add("Situation 1");break;
        case 2:logger.add("Situation 2");break;
        //...
    }
    /*
    close files, sockets, etc.
    */
    return retval;
}

这种方式更难但最快。

答案 5 :(得分:0)

根据您的具体情况,您可以从异常的构造函数(可能是异步的)进行日志记录,这样您的代码就像:

void CreateUser()
{
      OpenDatabaseConnection();
}

当然,您需要从OpenDatabaseConnection()抛出自定义异常。

当这个策略成功使用时,我参与了两个项目。

答案 6 :(得分:0)

我建议将错误处理与日志记录和用户交互分开。

  1. 每个方法都可以为自己写入日志文件。使用小型日志消息框架,方法可以输出调试,信息和错误消息。根据配置文件定义的应用程序运行的上下文,例如,实际上只写入严重错误消息。

  2. 特别是在网络应用程序中,连接失败总是会发生,并且不是例外。对不应发生或很少发生的意外错误使用异常。如果您需要,例如,在内部使用例外也是有意义的。堆栈展开功能:

    void CreateUser() {
        try {
           CDatabaseConnection db = ConnectToDatabase(); 
           InsertIntoDB(db, "INSERT INTO ... "); 
           SetPermission(...);
        } catch(...) {}
    }
    

    如果InsertIntoDB因为再次丢失网络连接而抛出异常,则将销毁对象CDatabaseConnection,并且永远不会运行SetPermission。使用它可以带来更好的代码。

  3. 您要做的第三件事是在交互式应用程序中提供用户反馈。这是完全不同的事情。让您的内部方法返回可能的错误代码的枚举eerrorCONNECTIONLOST, eerrorUSERNAMEINVALID, etc不要从核心方法返回错误字符串。用户界面层应该打扰要显示的字符串(可能将它们国际化)。在内部,错误代码将更有用。你可以,例如如果您的登录方法返回eerrorCONNECTIONLOST,则重试五次。