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{} 并让程序退出,但这与仅使用有何不同

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

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

4

8 回答 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. 使用这种方法您会丢失所有调试信息(您可以从未处理的异常生成堆栈跟踪,您无法对每次都退出的函数执行任何操作 - 未处理的异常是一个错误,但它是一个您可以跟踪源代码的错误) .
于 2011-10-01T16:34:13.323 回答
5

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

以这种方式考虑 - 当然,您的原地旋转方法有几个无效的参数类型,在这种情况下您可能应该抛出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
}
于 2011-10-01T16:32:44.077 回答
4

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

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

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

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

一定要放一个catch-all in main,以防万一。从标准异常中派生异常,例如std::runtime_error使得这样做更容易。

于 2011-10-01T16:32:32.597 回答
3

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

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

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

于 2011-10-01T16:33:05.537 回答
1

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

于 2011-10-01T16:31:45.297 回答
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;
}
于 2011-10-01T16:32:48.510 回答
0

这取决于。

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

于 2011-10-01T16:32:29.500 回答
0

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

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

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

于 2011-10-01T16:36:51.173 回答