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

6
我是一名有用的助手,可以为您翻译文本。

我的意思是,我知道所有关于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{} 并让程序退出,但这与仅仅使用 exit() 有什么不同吗?

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

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

3
以裸指针形式获取图像?很高兴我不用处理你的代码库。 - Puppy
嗯,我们在科学界工作,代码的质量通常不是我们的主要关注点,因为主要目标是快速编写代码和快速计算结果。像错误处理、可扩展性、可读性等方面经常被忽视。话虽如此,如果您有任何更好的解决方案,我会非常感激。 - John Yang
1
使用std::vector来存储数据(或者也可以使用Boost.MultiArray),使用某种类型的Image结构体来同时保存数据和元数据。不关心质量似乎不太科学。 - Cat Plus Plus
我是说,我们非常关心代码的正确性和效率,但我认为在其他问题上花费太多时间通常是过度的。毕竟,我们不是软件工程师,我们只是想利用计算机的计算能力。 - John Yang
2
实际上,在科学界存在很多关于这个问题的争议。科学家应该在编程方面关注多少?或者他们应该雇用什么样的人?如果雇用一个在同一科学领域的人,他知道所有的东西,但有时会编写不规范的代码。如果雇用一名专业程序员,则需要花费大量精力向他解释事情。 - John Yang
8个回答

6
每个处理错误的网站都需要使用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. 使用此方法会丢失所有调试信息(可以从未处理的异常生成堆栈跟踪,但您无法处理每次退出的函数-未处理的异常是错误,但是这是一个错误,您可以追踪到源头)。

5
例外的一般规则是,“立即调用站点是否关心这里正在发生的事情?” 如果调用站点确实关心,则返回状态码可能是有意义的。否则,抛出异常更合适。
考虑一下-当然,您的原地旋转方法有几种无效的参数类型,在这种情况下,您应该抛出std :: invalid_argument。调用rotate_in_place的调用者不太可能想要处理或知道如何处理图像不是正方形的情况,因此最好将其表示为异常。

Another possibility is do not wrap any try{} catch{} and let the program exit, but how is this different from just using

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

这是因为如果有人想要将您的函数放入GUI应用程序中,他们不必基于错误终止程序。他们可以将异常转化成用户友好的形式或其他类型。

现在它也对您有好处 - 您不必仅仅为了进行错误写入而将引入该翻译单元。

通常我会使用类似于以下的模式:

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
}

4
现在的问题是,rotate_in_place() 在1000多个地方被使用,我是否应该将每个 rotate_in_place() 的调用都包装在 try{} catch {} 中?这看起来会使代码变得非常臃肿。
这样做确实会如此,并且违背了首次使用异常的初衷。
另一种可能性是不包装任何 try{} catch{} 并让程序退出,但这与仅使用 [...] 有何不同?
您始终可以稍后更改异常处理的位置。如果在某个时刻,您发现一个更好的地方来合理地处理错误(例如恢复错误),那么这就是您放置 catch 的时候。有时它在抛出异常的函数中;有时它在调用链的顶部。
请确保在 main 中放置一个 catch-all,以防万一。从标准异常(例如 std::runtime_error)派生异常可以使这样做变得更加容易。

1
完全同意。这违背了首次使用异常的目的。 - C. K. Young

3
在使用异常处理的过程中,遵循以下简单规则:
  • 只要由于用户输入错误可能会发生任何问题(内部逻辑应通过断言/日志记录来处理),就抛出异常。尽早抛出,并尽可能多地抛出:与 .Net 异常相比,C++ 异常通常是非常便宜的。
  • 如果无法处理错误,就让异常传播。这基本上意味着总是让其传播。
需要记住的是:异常应该冒泡到可以处理的地方。这可能意味着弹出一个带有一些错误格式化的对话框,或者暗示某个不重要的逻辑最终将不会被执行等等。

1
`As soon as anything bad c...terminate called after throwing an instance of 'std::runtime_error'` - Matteo Italia

1
你可以让 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可能不可行。 - DavidRR

1

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


1
虽然,如果异常没有被捕获,堆栈展开就不能保证,因此最好有一个顶层的 try 块(仅打印异常并退出),至少可以保证堆栈展开。 - Matteo Italia

0

这要看情况。

通常情况下,异常是需要被捕获和处理的。在您的情况下,是否有可能处理异常(例如,用户提供了一个非正方形的图像,那么您可以要求他们再试一次)。但是如果您无法对其进行任何处理,则使用 cerr 是可行的方法。


0

嗯,我同意使用异常确实会导致代码膨胀。这是我不喜欢它们的主要原因。

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

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


嗯,C++ FAQ认为不使用异常会导致代码膨胀 - DavidRR

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接