我已经编写了很长时间的C和C ++,到目前为止我从未使用异常和try / catch。使用它而不仅仅是让函数返回错误代码有什么好处?
答案 0 :(得分:28)
可能是一个显而易见的观点 - 开发人员可以忽略(或不知道)您的返回状态,并继续幸福地发现某些事情失败。
需要以某种方式承认异常 - 如果没有积极地放置某些内容,就不能无声地忽略它。
答案 1 :(得分:22)
例外的优点有两个:
它们不容忽视。您必须在某个级别处理它们,否则它们将终止您的程序。使用错误代码时,您必须明确检查它们,否则它们将丢失。
可以忽略它们。如果无法在一个级别处理错误,它将自动冒泡到下一级别,即可。必须明确传递错误代码,直到它们达到可以处理的级别。
答案 2 :(得分:17)
优点是您不必在每次可能失败的呼叫后检查错误代码。为了实现这一点,您需要将它与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 ++中使用异常,因为您会泄漏资源和内存。所有清理工作都需要在堆栈分配对象的析构函数中完成。
我意识到还有其他方法可以进行错误代码处理,但它们最终看起来有点相同。如果你放弃了这些,你最终会重复清理代码。
错误代码的一个要点是,它们可以明显地说明事情可能会失败,以及它们如何失败。在上面的代码中,您假设事情不会失败(但如果它们确实存在,您将受到RAII包装器的保护)。但是你最不应该注意事情可能出错的地方。
答案 3 :(得分:10)
异常处理很有用,因为它可以很容易地将错误处理代码与为处理程序功能而编写的代码分开。这使得阅读和编写代码更容易。
答案 4 :(得分:8)
除了提到的其他内容之外,您无法从构造函数返回错误代码。也可以是析构函数,但是你也应该避免从析构函数中抛出异常。
答案 5 :(得分:7)
在前一种情况下,函数的调用者必须检查错误代码是否存在预期的失败;在后一种情况下,异常可以由堆栈(或默认处理程序)上的任何调用者处理(如果适当)
答案 6 :(得分:4)
我写了一篇关于此的博客文章(Exceptions make for Elegant Code),后来发表在Overload。我实际上是为了回应Joel在StackOverflow播客上所说的内容而写的这个!
无论如何,我坚信在大多数情况下,异常比错误代码更可取。我发现使用返回错误代码的函数真的很痛苦:你必须在每次调用后检查错误代码,这可能会破坏调用代码的流程。这也意味着您无法使用重载运算符,因为无法发出错误信号。
检查错误代码的痛苦意味着人们经常忽略这样做,从而使它们完全没有意义:至少你必须用catch
语句明确地忽略异常。
在C ++中使用析构函数和在.NET中使用配置器来确保在存在异常时正确释放资源也可以大大简化代码。为了获得与错误代码相同的保护级别,您需要大量的if
语句,大量重复的清理代码,或者在函数末尾对公共清理块进行goto
调用。这些选择都不令人愉快。
答案 7 :(得分:2)
Here's对EAFP的一个很好的解释(“更容易要求宽恕而不是权限。”),我认为即使它是维基百科中的Python页面也适用于此。使用例外导致更自然的编码风格,IMO - 以及许多其他人也认为。
答案 8 :(得分:2)
当我以前教授C ++时,我们的标准解释是它们可以让你避免纠结阳光灿烂的日子和阴雨天。换句话说,你可以编写一个函数,好像一切都能正常工作,并最终捕获异常。
没有例外,您必须从每次调用中获取返回值并确保它仍然是合法的。
当然,一个相关的好处是你不会“浪费”你的异常返回值(从而允许无效的方法为void),也可以从构造函数和析构函数返回错误。
答案 9 :(得分:2)
Google's C++ Style Guide对C ++代码中异常使用的优缺点进行了深入细致的分析。它还表明了你应该问的一些更大的问题;即我是否打算将我的代码分发给其他人(他们可能难以与启用异常的代码库集成)?
答案 10 :(得分:1)
有时你真的必须使用异常来标记异常情况。例如,如果构造函数出现问题并且您发现通知调用者有意义,那么您别无选择,只能抛出异常。
另一个例子:有时你的函数没有值可以返回来表示错误;函数可能返回的任何值表示成功。
int divide(int a, int b)
{
if( b == 0 )
// then what? no integer can be used for an error flag!
else
return a / b;
}
答案 11 :(得分:1)
您必须确认异常的事实是正确的,但这也可以使用错误结构来实现。 您可以创建一个基本错误类,在其dtor中检查是否已调用某个方法(例如IsOk)。如果没有,你可以记录一些东西,然后退出,抛出异常,或者提出一个断言等......
只是在错误对象上调用IsOk而不对它做出反应,那么就等同于编写catch(...){} 这两个陈述都表现出同样缺乏程序员的善意。
将错误代码传输到正确的级别是一个更大的问题。基本上,您必须使几乎所有方法都返回错误代码,这是传播的唯一原因。 但话说回来,一个函数或方法应该总是用它可以生成的异常来注释。所以基本上你有同样的问题,没有支持它的接口。
答案 12 :(得分:0)
正如@Martin指出抛出异常会迫使程序员处理错误。例如,不检查返回代码是C程序中安全漏洞的最大来源之一。例外确保您处理错误(希望如此)并为您的程序提供某种恢复路径。如果您选择忽略异常而不是引入安全漏洞,程序将崩溃。