我已经编写 C 和 C++ 很长时间了,到目前为止,我从未使用过异常和 try/catch。使用它而不是让函数返回错误代码有什么好处?
13 回答
可能很明显 - 开发人员可以忽略(或不知道)您的退货状态并继续幸福地不知道某些事情失败了。
需要以某种方式确认异常 - 如果不积极采取措施,就不能默默地忽略它。
例外的好处有两个:
他们不能被忽视。 您必须在某种程度上处理它们,否则它们将终止您的程序。使用错误代码,您必须明确检查它们,否则它们会丢失。
它们可以被忽略。 如果一个错误不能在一个级别处理,它会自动冒泡到下一个级别,它可以在那里。错误代码必须明确传递,直到它们达到可以处理的级别。
优点是您不必在每次可能失败的调用后检查错误代码。但是,为了使其工作,您需要将它与 RAII 类结合起来,以便在堆栈展开时自动清理所有内容。
带有错误消息:
int DoSomeThings()
{
int error = 0;
HandleA hA;
error = CreateAObject(&ha);
if (error)
goto cleanUpFailedA;
HandleB hB;
error = CreateBObjectWithA(hA, &hB);
if (error)
goto cleanUpFailedB;
HandleC hC;
error = CreateCObjectWithA(hB, &hC);
if (error)
goto cleanUpFailedC;
...
cleanUpFailedC:
DeleteCObject(hC);
cleanUpFailedB:
DeleteBObject(hB);
cleanUpFailedA:
DeleteAObject(hA);
return error;
}
有例外和 RAII
void DoSomeThings()
{
RAIIHandleA hA = CreateAObject();
RAIIHandleB hB = CreateBObjectWithA(hA);
RAIIHandleC hC = CreateCObjectWithB(hB);
...
}
struct RAIIHandleA
{
HandleA Handle;
RAIIHandleA(HandleA handle) : Handle(handle) {}
~RAIIHandleA() { DeleteAObject(Handle); }
}
...
乍一看,RAII/Exceptions 版本似乎更长,直到您意识到清理代码只需要编写一次(并且有一些方法可以简化它)。但是 DoSomeThings 的第二个版本更加清晰和可维护。
不要尝试在没有 RAII 习语的情况下在 C++ 中使用异常,因为您会泄漏资源和内存。您的所有清理工作都需要在堆栈分配对象的析构函数中完成。
我意识到还有其他方法可以处理错误代码,但它们最终看起来都差不多。如果你放弃了 goto,你最终会重复清理代码。
错误代码的一点是,它们清楚地表明了事情可能在哪里失败,以及它们如何失败。在上面的代码中,您假设事情不会失败(但如果失败,您将受到 RAII 包装器的保护)。但你最终会更少关注可能出错的地方。
异常处理很有用,因为它可以很容易地将错误处理代码与为处理程序功能而编写的代码分开。这使得阅读和编写代码更容易。
除了提到的其他事情之外,您不能从构造函数返回错误代码。析构函数也可以,但你也应该避免从析构函数中抛出异常。
- 在某些情况下预期会出现错误情况时返回错误代码
- 在任何情况下都不会出现错误情况时抛出异常
在前一种情况下,函数的调用者必须检查预期失败的错误代码;在后一种情况下,异常可以由堆栈上的任何调用者(或默认处理程序)酌情处理
我为此写了一篇博文(Exceptions make for Elegant Code),随后发表在Overload上。实际上,我写这篇文章是为了回应 Joel 在 StackOverflow 播客上所说的话!
无论如何,我坚信在大多数情况下异常比错误代码更可取。我发现使用返回错误代码的函数真的很痛苦:您必须在每次调用后检查错误代码,这可能会破坏调用代码的流程。这也意味着您不能使用重载的运算符,因为无法发出错误信号。
检查错误代码的痛苦意味着人们经常忽略这样做,从而使它们变得毫无意义:至少您必须使用catch
语句明确忽略异常。
使用 C++ 中的析构函数和 .NET 中的处理器来确保在出现异常时正确释放资源也可以大大简化代码。为了使用错误代码获得相同级别的保护,您要么需要大量if
语句、大量重复的清理代码,要么需要goto
在函数末尾调用公共清理块。这些选择都不是令人愉快的。
这是对 EAFP(“请求宽恕比许可更容易”)的一个很好的解释,我认为即使它是 Wikipedia 中的 Python 页面也适用于此。使用异常会导致更自然的编码风格,IMO——在许多其他人看来也是如此。
当我以前教 C++ 时,我们的标准解释是它们让你避免纠结晴天和雨天的场景。换句话说,您可以编写一个函数,就好像一切正常,并最终捕获异常。
如果没有例外,您必须从每个调用中获取一个返回值并确保它仍然是合法的。
当然,一个相关的好处是您不会在异常上“浪费”您的返回值(因此允许应该为 void 的方法为 void),并且还可以从构造函数和析构函数返回错误。
Google 的 C++ Style Guide对 C++ 代码中异常使用的优缺点进行了详尽的分析。它还指出了您应该问的一些更大的问题;即我是否打算将我的代码分发给其他人(他们可能难以与启用异常的代码库集成)?
有时您确实必须使用异常来标记异常情况。例如,如果构造函数中出现问题,并且您发现通知调用者这是有意义的,那么您别无选择,只能抛出异常。
另一个例子:有时你的函数没有返回值来表示错误;函数可能返回的任何值都表示成功。
int divide(int a, int b)
{
if( b == 0 )
// then what? no integer can be used for an error flag!
else
return a / b;
}
您必须承认异常这一事实是正确的,但这也可以使用错误结构来实现。您可以创建一个基本错误类,在其 dtor 中检查是否调用了某个方法(例如 IsOk )。如果没有,你可以记录一些东西然后退出,或者抛出一个异常,或者引发一个断言,等等......
只在错误对象上调用 IsOk 而不对其做出反应,就相当于编写 catch( ... ) {} 这两个语句都会显示出同样缺乏程序员的善意。
将错误代码传输到正确级别是一个更大的问题。出于传播的唯一原因,您基本上必须使几乎所有方法都返回错误代码。但话又说回来,一个函数或方法应该总是用它可能产生的异常进行注释。所以基本上你有同样的问题,没有接口来支持它。
正如@Martin 指出的那样,抛出异常会迫使程序员处理错误。例如,不检查返回码是 C 程序中最大的安全漏洞来源之一。异常确保您处理错误(希望如此)并为您的程序提供某种恢复路径。如果您选择忽略异常而不是引入安全漏洞,您的程序就会崩溃。