条款19:GotW#21 代码复杂性(Code Complexity) (二)

【问题】

让我们来考虑GotW#20里的那个函数,这个函数是异常安全(再出现异常时仍能正常工作)还是异常中立(能将所有异常都传递给调用者)?

String EvaluateSalaryAndReturnName(Employee e)
{
	if(e.Title() == "CEO" || e.Salary() > 100000)
	{
		cout << e.First() << " " << e.Last()
			<< "is overpaid" << endl;
	}
	return e.First() + " " + e.Last();
}

请对你的回答作出解释。如果它是异常安全的,那么它支持basic guarantee(基本保证)还是strong guarantee(强力保证)?如果它不是异常安全的,那该如何对其修改以使其支持basic guarantee或strong guarantee?

这里我们假设所有被调用的函数都是异常安全的(即可能抛出异常,但是抛出异常时没有任何副作用),并且假设所使用的任何对象(包括临时对象在内)也都是异常安全的(即当这些对象被销毁时,其占用的资源也能被清理)。

【basic guarantee 和Strong guarantee】

简单来说,basic guarantee保证可销毁性且没有泄露;而strong guarantee除此之外还保证了完全的commit-or-rollback(要么执行,要么不执行)语意。

【解答】

 

【关于假设的一点说明】

如题所述,我们假设的所有被调用的函数——包括流函数(stream function)在内——都是异常安全的(即可能抛出异常,但在抛出异常时无副作用),并假设所使用到的所有对象(包括临时对象)——也都是异常安全的(即当这些对象被销毁时,其占用的资源也都能被清理)。

然而流(stream)且偏偏对此使了拌儿——这缘于其可能产生的“un-rollbackable”(不可恢复)副作用。例如,运算符<<可能会在输出了string的一部分之后抛出异常,而此时已经输出的部分是无法被“反输出(un-emitted)”的;同样,流的错误状态也会在此被设置。在大部分情况下,我们都忽略这些情况;本次讨论的重点是考察【当函数具有两个互不相同的副作用时,如何使函数成为异常安全的】

【Basic guarantee vs. Strong guarantee】

由题可知,该函数满足basic guarantee :当出现异常时,函数不会产生资源泄露。

该函数不满足strong guarantee。strong guarantee意:如果函数由异常而造成失败,程序的状态必须仍保持不变。然而这里的函数由两个互不相同的副作用:

  1. 一个“...overpaid...”消息被送到cout;
  2. 一个名称字符串被返回。

如考虑第2点,那函数应该满足strong guarantee了,因为异常产生时,值不会被返回。若考虑第一点,函数仍然不是异常安全的,原因有两个:

  1. 如果在欲输出消息的第一部分被送到了cout后,整个消息被完全送出到cout之前时候可能抛异常(比如代码中的第4个<<抛出异常),那么此时已经有一部分消息被输出了。
  2. 如果消息被成功输出,但在成功输出之后产生异常(),那么消息也确已经(无法挽回)被送到了cout,尽管该函数因为一个异常而宣告失败。

要满足strong guarantee,函数应该满足:要么两件事(即输出到cout和值传回)都圆满成功,要么遇到该函数抛出异常时候,两件事都不做。

我们可以达到这样的要求吗?下面是一种我们可能会尝试的方法(第一次尝试):

String EvaluateSalaryAndReturnName(Employee e)
{
	String result = e.First() + " " + e.Last();
	if(e.Title() == "CEO" || e.Salary() > 100000)
	{
		String message = e.First() + " " + e.Last()
						  + "is overpaid\n";
		cout << message;
	}
	return result;
}

这段代码不算坏。应该注意到,为了让整个string只使用一个<<调用,我们用换行符代替了endl(虽然两者并不完全相同)。(当然,这样做并不保证【底层的流系统本身不会对消息施以写操作的时候失败,从而造成不完整的输出】——但我们在这样的高层次已经做了利索能力的努力)。

【一个稍微有点揪心的问题】

到现在,我们仍然有一个小瑕疵,它如下面的用户代码(client code):

String theName;
theName = EvaluateSalaryAndReturnName(bob);

由于函数的结果采用了return by value方式返回,因此String的拷贝构造函数被唤起;拷贝赋值操作也被唤起,用来将结果拷贝到theName。如果这两个拷贝操作中失败了一个,那么函数的副作用已经发生(因为消息被完全输出,返回值也被完全构造好了),而其结果也就无法挽回的丢失了。

我们可否做的更好一些,可以通过避免拷贝操作来回避这个问题吗?我们让函数接受一个non-const 的String引用参数,并将返回值放在这个参数中:

void EvaluateSalaryAndReturnName(Employee e,String& r)
{
	String result = e.First() + " " + e.Last();
	if(e.Title() == "CEO" || e.Salary() > 100000)
	{
		String message = e.First() + " " + e.Last()
						  + "is overpaid\n";
		cout << message;
	}
	r = result;
}

然而不幸的是,对r赋值仍然可能失败,这将造成其中一个副作用被完全而另一个没被完成。最关键的问题在于,这第二次尝试并没有给我们带来多大好处。

于是我们可能会尝试使用auto_ptr来返回结果(第三次尝试):

auto_ptr<String> 
EvaluateSalaryAndReturnName(Employee e)
{
	auto_ptr<String> result = new(e.First() + " " + e.Last());
	if(e.Title() == "CEO" || e.Salary() > 100000)
	{
		String message = e.First() + " " + e.Last()
						  + "is overpaid\n";
		cout << message;
	}
	return result;//依赖所有权的转移,不能抛出异常
}

这正是解题的诀窍所在——我们有效的隐藏了第二个副作用(返回值)操作,同时也保证了在第一个副作用(打印消息)被完成之后,只使用不抛异常的操作把结果安全的返回给函数的调用者。但这样强安全性的代价就是——我们使用了额外的动态内存分配。

【异常安全性和多重副作用】

从本次讨论可以看出,在第三次尝试中有可能以基本的commit-or-rollback语意来完成那两个副作用(与流有关的那个除外)。究其原因,是因为这两个副作用看起来应该可以通过某种技术而被自动完成的——为两个副作用所做的全部“真正的”工作能够以这样一种方式被完成:即可见的副作用只能够通过不抛异常(nonthrowing)操作来完成。

尽管这次还比较幸运,但情况并不那么简单:要编写强异常安全的函数,且该函数包含两个或多个能被自动完成的、互不相关的副作用(例如,当两个副作用中一个向cout送消息,一个向cerr送消息,那会怎样?)——这是不可能的,因为strong guarantee要求出现异常时“程序的状态保存不变”:换句话说,即只要有异常出现就不能有副作用产生。通常当你遇到两个副作用无法被自动完成的情况下,要实现强异常安全的唯一方法就是把函数分成两个能自动完成副作用的函数。

本期的GotW意在描述3个重点:

  1. 要对强异常安全提供保证,经常(但并不总是)需要你以放弃一部分性能为代价。
  2. 如果一个函数含有多重副作用,那么其总是无法成为异常安全的。此时,唯一的方法就是将函数分为几个函数,以使得每一个分出来的函数之副作用能被自动完成。
  3. 并不是所有函数都需要强异常安全性。本条款中的原始代码和第一次尝试的代码已经能够满足basic guarantee了。在许多情况下,第一次尝试的代码已经足够好了,能够将副作用在异常情况下发生的可能性减到最小,而并不需要像第三次那样非要损失一定性能。

【又及:流和副作用】

正如本条款所示,我们对【被调用的函数没有副作用】之假设并不是完全真实的情况。特别的,我们根本无法保证【流在输出一部分结果之后不会突然失败】。这意味着,我们无法执行流输出的函数中真正的commit-or-rollback语义——至少在这些标准流中是不可能的。另外一点是,如果流输出失败了,流的状态也将会改变。目前我们不去检查这种情况,也不尝试对其予以回复——但我们仍然对其函数进行修改,以使其能够捕捉或由于流引起的异常,并在重新向调用者抛出异常之前重载cout的error flags。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值