C ++似乎更喜欢使用异常。
在某些方面,我建议实际上比Objective-C少,因为C ++标准库通常不会引发程序员错误,例如以最常见的案例设计形式(operator[]
即,即)或尝试取消引用无效的迭代器。该语言不会抛出对数组的越界访问,取消引用空指针或任何此类的东西。
实际上,从异常处理方程式中消除程序员的错误实际上消除了其他语言经常通过响应的很大种类的错误throwing
。assert
在这种情况下,C ++倾向于(不会在发布/生产版本中进行编译,而仅在调试版本中进行编译)或只是小故障(经常崩溃),部分原因是该语言不想强加这种运行时检查的成本除非程序员明确希望通过编写自己执行此类检查的代码来支付费用,否则将无法发现此类错误。
Sutter甚至鼓励避免在C ++编码标准中的此类情况下出现异常:
使用异常报告编程错误的主要缺点是,当您希望调试器在检测到违规的确切行上启动且该行的状态完好无损时,您实际上不希望发生堆栈退绕。总结:有些错误可能会发生(请参阅第69到75条)。对于其他所有不应该的事情,如果这样做,则是程序员的错,这是assert
。
该规则不一定是一成不变的。在某些更关键任务的情况下,最好使用包装器和编码标准,它们统一记录发生程序员错误的位置以及throw
存在程序员错误的情况,例如试图对无效的东西进行引用或超出范围,因为如果有机会,在这些情况下无法恢复可能会付出太大的代价。但是总的来说,语言的更普遍使用倾向于倾向于不面对程序员的错误。
外部异常
我看到C ++中最常鼓励异常的地方(例如,根据标准委员会)是“外部异常”的原因,这是程序外部某些外部源的意外结果。一个例子是分配内存失败。另一个原因是无法打开运行该软件所需的关键文件。另一个无法连接到所需的服务器。另一个是用户在没有外部中断的情况下,用户按下中止按钮以取消其普通情况下执行路径希望成功的操作。所有这些都不受即时软件和编写该软件的程序员的控制。它们是来自外部来源的意外结果,这些外部结果阻止了操作(在我的书中应该真正视为不可分割的交易*)成功。
交易次数
我经常鼓励将try
区块视为“交易”,因为交易应该整体上成功或整体失败。如果我们尝试执行某项操作,但操作中途失败,则通常需要回退对程序状态所做的任何副作用/更改,以使系统恢复为有效状态,就像从未执行过该事务一样,就像RDBMS无法中途处理查询一样,它也不会损害数据库的完整性。如果您直接在上述事务中更改程序状态,则必须在遇到错误时对其进行“取消更改”(在这里,范围保护对于RAII很有用)。
更简单的选择是不更改原始程序状态。您可以变异它的副本,然后,如果成功,则将其与原始副本交换(确保交换不会抛出)。如果失败,则丢弃副本。即使您通常不将异常用于错误处理,这也适用。如果在遇到错误之前发生了程序状态突变,则“事务性”思维方式是正确恢复的关键。它要么整体成功,要么整体失败。它不能成功地进行突变。
当我看到程序员询问如何正确执行错误或异常处理时,这是最不经常讨论的主题之一,但是在所有想要直接改变程序状态的软件中,这都是最困难的事情。它的运作。纯度和不变性在这里可以帮助实现异常安全,就像它们在线程安全方面的帮助一样,因为不需要回退不会发生的突变/外部副作用。
性能
关于是否使用异常的另一个指导性因素是性能,我并不是用某种强迫性,费力的,适得其反的方式表示。许多C ++编译器都实现了所谓的“零成本异常处理”。
它为零错误执行提供了零运行时开销,甚至超过了C返回值错误处理的开销。作为权衡,异常的传播具有较大的开销。
根据我所读到的内容,它使您的普通情况下执行路径不需要任何开销(甚至不需要通常伴随C样式错误代码处理和传播的开销),以换取将成本大幅度地推向例外路径(这意味着throwing
现在比以往任何时候都昂贵)。
“昂贵”有点难以量化,但是,对于初学者来说,您可能不想在某个紧密循环中投入一百万次。这种设计假设异常情况并非一直都在发生。
无错误
而且该性能点使我陷入了无错误的境地,如果我们查看各种其他语言,这将是令人惊讶的模糊。但是我要说的是,鉴于上述的零成本EH设计,您几乎可以肯定不想throw
响应在集合中找不到密钥。因为这不仅可以说是非错误的(搜索密钥的人可能已经建立了集合,并期望搜索并不总是存在的密钥),但是在这种情况下,这将是非常昂贵的。
例如,一个集合交集函数可能想要遍历两个集合并搜索它们共有的关键点。如果找不到键threw
,则会遍历整个循环,并且可能在一半或更多的迭代中遇到异常:
Set<int> set_intersection(const Set<int>& a, const Set<int>& b)
{
Set<int> intersection;
for (int key: a)
{
try
{
b.find(key);
intersection.insert(other_key);
}
catch (const KeyNotFoundException&)
{
// Do nothing.
}
}
return intersection;
}
上面的示例绝对是荒谬和夸张的,但是我已经看到,在生产代码中,一些来自其他语言的人在C ++中使用异常,有些像这样,并且我认为这是合理可行的说法,即无论如何都不适用于异常在C ++中。上面的另一个提示是,您会注意到该catch
块绝对不做任何事情,而只是为了强行忽略任何此类异常而编写,这通常暗示(尽管不是保证人)在C ++中异常可能没有被适当地使用。
对于那些类型的情况,指示失败的某种返回值类型(从返回false
无效的迭代器或nullptr
在上下文中有意义的任何东西)通常更合适,并且由于非错误类型的返回值通常也更实用,更具生产力case通常不需要进行一些堆栈展开过程即可到达类比catch
站点。
问题
如果我选择避免异常,则必须使用内部错误标志。它会处理起来太麻烦,还是会比异常情况做得更好?两种情况的比较将是最佳答案。
在我看来,完全避免使用C ++产生异常对我产生了反作用,除非您在某些嵌入式系统中工作或禁止使用此类情况(在这种情况下,您还必须全力以赴避免所有情况)库和语言功能,否则throw
,例如严格使用nothrow
new
)。
如果出于某种原因(例如:在您导出的C API的模块的C API边界内工作)绝对必须避免异常,许多人可能与我不同意,但我实际上建议您使用OpenGL等全局错误处理程序/状态glGetError()
。您可以使其使用线程本地存储来使每个线程具有唯一的错误状态。
我这样做的理由是,我不习惯看到生产环境中的团队彻底检查所有可能的错误,不幸的是,当返回错误代码时。如果周全,则某些C API几乎可以在每个C API调用中遇到错误,而周全检查将需要以下内容:
if ((err = ApiCall(...)) != success)
{
// Handle error
}
...几乎所有调用API的代码行都需要进行此类检查。但是我还没有与如此彻底的团队合作的幸运。他们经常有一半甚至有时大部分时间都忽略这种错误。这是我最大的例外。如果我们包装此API并throw
在遇到错误时统一使用它,那么异常可能就不会被忽略,并且在我看来,根据经验,这就是异常的优势所在。
但是,如果无法使用异常,那么全局的,每个线程的错误状态至少具有优势(与将错误代码返回给我相比,这是一个巨大的优势),它有可能比迟到的时候稍早捕获以前的错误发生在一些草率的代码库中,而不是完全遗漏它,使我们完全不了解发生了什么。该错误可能在之前的几行或之前的函数调用中发生过,但是只要软件尚未崩溃,我们就可以开始逐步研究并确定发生错误的原因。
在我看来,由于指针很少使用,因此如果我选择避免异常,则必须使用内部错误标志。
我不一定会说指针很少。现在,在C ++ 11及更高版本中甚至有一些方法可以获取容器的基础数据指针和一个新nullptr
关键字。如果可以使用类似的方法代替使用原始指针来拥有/管理内存,通常被认为是不明智的,unique_ptr
因为在存在异常的情况下遵循RAII的要求至关重要。但是,不拥有/管理内存的原始指针不一定被认为是很糟糕的(即使是来自Sutter和Stroustrup之类的人),有时也非常实用(作为指向事物的索引)。
它们可以说比标准容器迭代器(至少在发行版中,没有检查的迭代器)安全得多,后者不会检测到在无效之后是否尝试取消引用它们。我要说的是,C ++仍然毫无疑问是一种危险的语言,除非您对它的特定使用想要包装所有内容并隐藏甚至不拥有原始指针。几乎至关重要的是,例外情况是资源符合RAII(通常不收取任何运行时费用),但除此之外,它并不一定要成为最安全的语言来避免开发人员不需要的成本。换其他东西。推荐的用法并不是要保护您免受悬空指针和无效的迭代器之类的影响,可以这么说(否则,建议您使用shared_ptr
在整个地方,而Stroustrup强烈反对)。它试图防止您在某些情况下无法正确地释放/释放/销毁/解锁/清理资源throws
。