throw, try {} catch{}应该如何在现实世界中使用

How throw, try {} catch {} should be used in the real world?

本文关键字:世界 try catch throw      更新时间:2023-10-16

我的意思是,我知道关于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的真正好处,有什么好的建议吗?

每个处理错误的站点都需要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. 使用这种方法会丢失所有调试信息(您可以从未处理的异常生成堆栈跟踪,您不能对每次都跳出的函数做任何事情-未处理的异常是一个错误,但这是一个您可以跟踪到源代码的错误)。

例外的一般规则是,"立即调用站点关心这里发生了什么吗?"如果调用站点确实关心,那么返回状态码可能是有意义的。否则,throw更有意义。

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

另一种可能是不包装任何try{} catch{},让程序退出,但这与仅仅使用

有什么不同?
if (sz.nx != sz.ny) {
    cerr << "Error: non-squared image error!n";
    exit(0);
}

这是不同的,因为如果有人以后想要使用你的函数并将其放入,比如说,GUI应用程序中,他们不必基于错误终止程序。他们可以把那个异常变成对用户来说很漂亮的东西或者类似的东西。

它现在对您也有好处——也就是说,您不必将<iostream>拉到翻译单元中只是为了进行错误写入。

我通常使用这样的模式:

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
}

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

会的,而且它违背了使用异常的初衷。

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

有什么不同呢?

以后总是可以更改异常处理的位置。如果在某个时候您找到了一个更好的地方来合理地处理错误(可能从中恢复),那么您可以将catch放在这里。有时它就在你抛出异常的函数中;有时它在调用链的顶端。

main中加入catch -all,以防万一。从标准异常(如std::runtime_error)中派生异常使此操作容易得多。

使用异常处理的意义在于以下几个简单的规则:

  • 一旦由于错误的用户输入(内部逻辑应该通过断言/日志处理)而发生任何不好的事情,抛出异常。尽可能快地抛出异常:与。net异常相比,c++异常通常相当便宜。
  • 如果你不能处理错误,让异常传播。这意味着几乎总是。

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

使用异常允许调用者决定如何处理错误。如果在函数中直接调用exit,那么在调用者无法决定如何处理错误的情况下,程序将退出。另外,在exit中,堆栈对象不会被展开。:-(

你能做的是使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;
}

看情况

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

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

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

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