【Effective C++】尽量写出“异常安全”的代码

异常安全代码的目标

当函数抛出异常时,具有“异常安全”性的代码应该实现以下两点:

  • 不泄露任何资源。
  • 不破坏数据。 也就是说,系统处于前后一致的状态。

比如下面这个代码:

class Image;

//多线程环境下使用
class Menu
{
private:
	std::mutex mtx;		//互斥器
	Image* bgImg;	//当前的背景图案
	int imgChanges;	//图案被改变的次数

public:
	//改变背景图像
	void ChangeBackground(std::istream& imgSrc)
	{
		mtx.lock();
		delete bgImg;
		imgChanges++;
		bgImg = new Image(imgSrc);
		mtx.unlock();
	}
};
  • 泄露了 mutex 资源。一旦 new Image(imgSrc) 导致异常,对 unlock 的调用就绝对不会执行,于是互斥器就永远被把持住了。
  • 破坏了原始数据。如果 new Image(imgSrc) 抛出异常,bgImg 就会指向一个已经删除了的对象,而且 imgChanges 被错误的累加了,然而新的图像并没有成功加载。

异常安全代码的三种实现等级

对于数据被破坏的问题,异常安全的函数通常有三种不同的实现等级:

  • 基本的保证。 函数在抛出异常的时候,程序中的所有东西仍然保持为有效状态,但是却可能无法预测具体的状态。
  • 强烈保证。 函数在抛出异常的时候,程序的状态不会改变。对于该函数的调用,具有原子性。也就是说,一旦抛出异常,就像没有调用过这个函数一样。
  • 不抛异常。 函数永远不会抛出异常,它总是能够完成交付给它的任务。

异常安全的代码必须提供以上三种保证的一种。


异常安全代码的实现方案

  1. 对于资源泄露问题,可以使用 对象来管理资源。比如说引入 Lock 类来确保 mutex 能够被实时释放掉(离开对象作用域时,对象析构函数自动释放资源):
class Lock
{
public:
	explicit Lock(mutex* pm) : pMutex(pm) 
	{
		pMutex->lock();
	}

	~Lock()
	{
		pMutex->unlock();
	}

private:
	std::mutex* pMutex;
};

离开 m 的作用域时,mutex 资源利用 m 的析构函数被自动释放,解决了资源泄露问题。

void ChangeBackground(std::istream& imgSrc)
{
	Lock m(&mtx);	

	delete bgImg;
	imgChanges++;

	bgImg = new Image(imgSrc);
}

利用资源管理类 Lock,也减少了代码量,减小了可能出错的机会。(客户端无需调用 unlock 函数)


  1. ChangeBackground 提供强烈保证也不难。
  • bgImg 的数据类型从普通的 Image* 指针,改为智能指针。
  • 调整语句 imgChanges++ 的顺序。
class Menu
{
private:
	std::mutex mtx;		//互斥器
	std::shared_ptr<Image> bgImg;	//当前的背景图案
	int imgChanges;	//图案被改变的次数

public:
	//改变背景图像
	void ChangeBackground(std::istream& imgSrc)
	{
		Lock m(&mtx);

		bgImg.reset(new Image(imgSrc));

		imgChanges++;
	}
};

改进之后:

  • 不需要再手动 delete 旧的 image,这已经由智能指针的 reset 函数内部处理了。
  • 销毁操作只有在 new Image(imgSrc) 被成功创建之后才会发生。更确切的说,delete 函数只在 reset 函数内被调用。只有 new Image(imgSrc) 被成功创建后,delete 才会被调用。
  • 还有一个问题。关于参数 imgSrc,如果 Image 的构造函数抛出异常,输入流的读标记可能会被移动,这个移动致使状态发生变化并且对程序接下来的运行是可见的。这是一种对整个系统的副作用,类似的副作用还有对数据库的操作。我们也没有一种通用的操作撤销对数据库的操作。这一点暂且可以忽略,我们还是认为 ChangeBackground 提供了强烈的保证。

copy and swap

copy and swap 常用来实现强烈保证。首先将原来的对象复制一份,然后所有的修改动作都是在这个拷贝对象上进行。如果任何修改操作抛出异常,只是这个拷贝对象发生了改变,原来的对象仍然保持原来的状态。一旦修改成功,将源对象和修改后的对象进行不会抛出异常的交换就可以了。(写一个不抛异常的 swap 函数)

为了实现 copy and swap,往往会把类的数据成员放到一个单独的对象之中,然后提供一个指向这个数据对象的指针:

struct MImpl
{
	std::shared_ptr<Image> bgImg;	//当前的背景图案
	int imgChanges;	//图案被改变的次数
}; 

//多线程环境下使用
class Menu
{
private:
	std::mutex mtx;		//互斥器
	std::shared_ptr<MImpl> pImpl;

public:
	//改变背景图像
	void ChangeBackground(std::istream& imgSrc)
	{
		using std::swap;

		Lock m(&mtx);

		std::shared_ptr<MImpl> copy(new MImpl(*pImpl));	//copy original data

		copy->bgImg.reset(new Image(imgSrc));
		copy->imgChanges++;

		
		swap(copy, pImpl);	//swap
	}
};
  • 这里使用 struct 而没有使用 class ,这是因为 Menu 的数据成员的封装性已经由 pImplprivate 属性保证,所以可以不必将 MImpl 定义为 class。如果需要,可以将 struct MImpl 定义在 Menu 类的内部,但是这样又会带来打包问题。
  • copy and swap 是处理异常安全的常用办法,但是一般情况下,它不能保证所有的函数都是强异常安全型的。比如说下面这样:
void ChangeBackground(std::istream& imgSrc)
{
	//...
	func1();
	func2();
	//...
}

如果函数 func1() 和函数 func2() 不是强异常安全的,那么 ChangeBackground 就很难是异常安全的。所以,一个函数的异常安全级别,不会高于它所调用的所有函数中的最低安全级别。

  • copy and swap 还存在一个效率问题。你需要为原有的数据进行拷贝,然后再拷贝对象上进行数据改动,最后还要进行交换动作,这就需要时间和空间。
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值