【问题标题】:How throw, try {} catch {} should be used in the real world?throw, try {} catch {} 在现实世界中应该如何使用?
【发布时间】:2011-11-29 02:33:06
【问题描述】:

我的意思是,我知道所有关于 throw, try {} catch {} 的语言规则,但我不确定我在现实世界中是否正确使用它们。请看下面的例子:

我们有一大段科学代码可以处理各种图像处理工作,最近我们决定对其进行修饰并使其更加健壮。经常使用的例程之一是 void rotate_in_place(float* image, image_size sz);

为了使其更加健壮,我们在代码开头添加了一些完整性检查:

void rotate_in_place(float* image, image_size sz) {
    // rotate_in_place does not support non-square image;
    if (sz.nx != sz.ny)  throw NonSquareImageError;
    // rotate_in_place does not support image too small or too large
    if (sz.nx <= 2 || sz.nx > 1024)  throw WrongImageSizeError;
    // Real rode here
    .....
}

现在的问题是,rotate_in_place() 被用在了 1000 多个地方,我是否应该用 try{} catch {} 包装每个 rotate_in_place() 调用,这在我看来会让代码非常臃肿。另一种可能性是不包装任何 try{} catch{} 并让程序退出,但这与仅使用有何不同

if (sz.nx != sz.ny) {
    cerr << "Error: non-squared image error!\n";
    exit(0);
}

简而言之,我不太确定使用 throw、try、catch 的真正好处,有什么好的建议吗?

【问题讨论】:

  • 图像作为原始指针?很高兴我不在你的代码库上工作。
  • 好吧,我们在科学界工作,代码质量通常不是我们最关心的问题,因为主要目标是快速编写代码并快速计算结果。诸如错误处理、可扩展性、可读性之类的事情通常是事后才想到的。话虽如此,如果您能提出任何更好的解决方案,我将不胜感激。
  • std::vector 用于数据(或者可能是 Boost.MultiArray),某种Image 结构来保存数据和元数据。不关心质量似乎不太科学。
  • 我的意思是,我们确实非常关心代码的正确性和效率,但我认为在其他问题上花费太多时间通常是矫枉过正。毕竟,我们不是软件工程师,我们只是想利用计算机的计算能力。
  • 其实科学界对这个问题有很多争论。科学家应该在多大程度上关心编程?或者他们应该雇佣什么样的人?如果雇用同一科学领域的人,他知道所有的东西,但有时会练习草率的编码。如果聘请专业的程序员,需要付出很多努力向他解释。

标签: c++ exception try-catch throw


【解决方案1】:

例外情况的一般规则是,“直接呼叫站点是否关心这里发生的事情?”如果调用站点确实关心,那么返回状态代码可能是有意义的。否则,投掷更有意义。

这样考虑——当然,你的原地旋转方法有几个无效的参数类型,在这种情况下你可能应该抛出std::invalid_argument。例如,rotate_in_place 的调用者不太可能想要处理或知道如何处理图像不是正方形的情况,因此最好将其表示为异常。

另一种可能性是不要包装任何 try{} catch{} 并让 程序退出,但这与仅使用有什么不同

if (sz.nx != sz.ny) {
    cerr << "Error: non-squared image error!\n";
    exit(0);
}

这是不同的,因为如果以后有人想要获取您的功能并将其放入,例如,GUI 应用程序,他们不必根据错误终止程序。他们可以将该异常变成对用户来说很漂亮的东西或类似的东西。

它现在对您也有好处 - 即您不必为了编写错误而将 &lt;iostream&gt; 拉入该翻译单元。

我通常使用这样的模式:

int realEntryPoint()
{
    //Program goes here
}

int main()
{
    //Allow the debugger to get the exception if this is a debug binary
    #ifdef NDEBUG
    try
    #endif
    {
      return realEntryPoint();
    }
    #ifdef NDEBUG
    catch (std::exception& ex)
    {
      std::cerr << "An exception was thrown: " << ex.what() << std::endl;
    }
    #endif
}

【讨论】:

    【解决方案2】:

    每个处理错误的站点都需要try-catch 块。这完全取决于您的设计,但我怀疑您是否需要在每个 rotate_in_place 呼叫站点处理错误,您可能大部分时间都不会向上传播。

    打印错误并使用exit 不好,原因有三个:

    1. 您无法处理该错误。 exit 没有处理(除非它在错误非常严重时完成,但你的函数不知道——调用者可能有办法恢复)。
    2. 您正在通过写入硬编码流来扩展函数的职责,这甚至可能不可用(这是rotate_in_place,而不是rotate_in_place_and_print_errors_and_kill_the_program_if_something_is_wrong)——这会损害可重用性。
    3. 使用这种方法会丢失所有调试信息(您可以从未处理的异常生成堆栈跟踪,您无法对每次都退出的函数执行任何操作 - 未处理的异常是一个错误,但您可以遵循它来源)。

    【讨论】:

      【解决方案3】:

      使用异常处理的要点在于以下简单规则:

      • 一旦由于错误的用户输入(内部逻辑应通过断言/日志处理)而发生任何不良情况,立即抛出异常。尽可能快地抛出异常:与 .Net 异常相比,C++ 异常通常非常便宜。
      • 如果您无法处理错误,请让异常传播。这意味着几乎总是这样。

      要记住的是:异常应该冒泡到可以处理的程度。这可能意味着一个带有某种错误格式的对话框,或者这可能意味着一些不重要的逻辑将不会被执行,等等。

      【讨论】:

      • As soon as anything bad c... terminate called after throwing an instance of 'std::runtime_error'
      【解决方案4】:

      好吧,我同意真正使用异常会导致代码臃肿。这是我不喜欢它们的主要原因。

      无论如何,就您的示例而言:抛出异常与仅使用 exit() 之间的主要区别在于,由于异常的处理发生(或应该发生)在生成错误/异常的程序片段之外,您没有指定函数/类的用户必须如何处理错误。通过使用异常,您可以进行不同的处理,例如中止程序、报告错误甚至从某些错误中恢复。

      TLDNR:如果您使用异常,则代码的异常生成部分不需要指定如何处理异常情况。这发生在外部程序中,并且可以根据代码的使用方式进行更改。

      【讨论】:

      【解决方案5】:

      如果函数调用成功,您可以做的是让 rotate_in_place 返回一个布尔值。并通过函数参数返回旋转后的图像。

      bool rotate_in_place(float* image, image_size sz, float** rotated_image) {
          // rotate_in_place does not support non-square image;
          if (sz.nx != sz.ny)  return false;
          // rotate_in_place does not support image too small or too large
          if (sz.nx <= 2 || sz.nx > 1024)  return false;
          // Real rode here
          .....
          return true;
      }
      

      【讨论】:

      • 由于问题指出“现在的问题是 rotate_in_place() 用于 1000 多个地方,...”,将返回类型从 void 更改为 bool 可能不可行。
      【解决方案6】:

      现在的问题是,rotate_in_place() 被用在了 1000 多个地方,我是否应该用 try{} catch {} 包装每个 rotate_in_place() 的调用,这在我看来会让代码非常臃肿。

      它会的,而且它首先打破了使用异常的目的。

      另一种可能性是不包装任何 try{} catch{} 并让程序退出,但这与仅使用 [...] 有何不同

      您以后可以随时更改异常处理的位置。如果在某个时候你找到了一个更好的地方来明智地处理错误(也许从中恢复),那么这就是你放置catch 的地方。有时那是在你抛出异常的函数中;有时它在调用链中很靠前。

      请在main 中添加catch-all,以防万一。从标准异常(例如std::runtime_error)派生异常可以更轻松地执行此操作。

      【讨论】:

      • +1 表示“超越了首先使用异常的目的”。完全同意。
      【解决方案7】:

      视情况而定。

      异常通常意味着被捕获/处理。在您的情况下,是否可以处理异常(例如,用户提供了非方形图像,因此您要求他们再试一次)。但是,如果您对此无能为力,那么cerr 就是您要走的路。

      【讨论】:

        【解决方案8】:

        使用异常允许调用者决定如何处理错误。如果您直接在函数内调用exit,则程序将退出,而调用者无法决定如何处理错误。此外,使用exit,堆栈对象不会被展开。 :-(

        【讨论】:

        • ... 但是,如果没有捕获到异常,则无法保证堆栈展开,因此最好有一个顶级 try 块(仅打印异常并退出) ,至少保证堆栈展开。
        猜你喜欢
        • 1970-01-01
        • 2015-07-07
        • 2010-12-14
        • 2011-05-31
        • 1970-01-01
        • 2020-02-11
        • 2015-09-08
        • 2011-01-24
        • 1970-01-01
        相关资源
        最近更新 更多