拷贝赋值运算符处理自我赋值和异常安全检查

    拷贝赋值运算符通常应该返回一个指向其左侧运算对象的引用。目的是为了实现连锁赋值操作;比如 x = y = z。其一般形式如下:
class Foo
{
public:
	Foo& operator=(const Foo &rhs)    //返回类型是个reference
	{
		...
		return *this;                 //返回左侧对象           
	}
};
在operator中主要有两个问题:(1)处理operator=中自我赋值情况。(2)处理异常问题。比如:
class Foo{...};
Foo f;
f = f;
这段代码看起来有点愚蠢,但是它是合法,有的用户会那么做,此外自我赋值运算可能存在比较隐蔽的地方,比如
b[i] = b[j]   //潜在的自我赋值
如果i和j有相同的值,这便是个自我赋值。
下面是一段operator=实现代码,表面上看起来合理,但自我赋值出现时并不安全。
class Foo
{
private:
	string *str;
	int val;
public:
	...
	Foo & operator=(const Foo&rhs)  //一份不安全的operator=实现版本
	{
		delete str;
		str = new string(*(rhs.str));
		val = rhs.val;
		return *this;
	}
}
这里的自我赋值问题是,operator=函数内的*this和rhs有可能是同一个对象,如果他们是同一个对象,delete就不仅销毁是(*this).str中所指向的对象,他也销毁rhs.str中所指向的对象。当执行第二行时,构造的是一个空的str,因为当前对象已经被销毁了。我们可以加上一个验证条件处理自我赋值,比如如下:
class Foo
{
private:
	string *str;
	int val;
public:
	...
	Foo & operator=(const Foo&rhs)  //一份安全的operator=实现版本,但没有处理异常问题
	{
		if(this == &rhs) return *this; //加入验证条件,处理自我赋值的情况
		delete str;
		str = new string(*(rhs.str));
		val = rhs.val;
		return *this;
	}
}
此版本虽然解决了自我赋值问题,但不具备异常安全性检查。更具体的说,如果new string(*(rhs.str))导致异常(不论是因为分配内存不足或是string的构造函数抛出异常),Foo最终会持有一个指针指向一块被删除的string,你无法安全地删除它们,甚至无法读取他们,许多时候我们可以精心安排一下语句的结构,就可以导出异常安全以及自我赋值,我们进行一下改进:
class Foo
{
private:
	string *str;
	int val;
public:
	...
	Foo & operator=(const Foo&rhs)  //一份安全的operator=实现版本
	{
		string *tmp = new string(*(rhs.str)); //拷贝一个副本
		delete str;  //释放原先str指向的内存空间
		str = tmp;  //从右侧运算对象拷贝到左侧
		val = rhs.val;
		return *this;
	}
}
现在,如果new string抛出异常,str会保持原状。即使没有等同验证条件,这段代码还是可以能够处理自我赋值情况,因为我们对原string做了一份副本、删除原来的string、然后指向新构造的那个副本,虽然不是自我赋值的最高效的办法,但是它行得通。如果你关系效率,可以把等同验证条件再次放回函数起始处。
我们也可以使用copy and swap技术来实现赋值运算符,这种技术将左侧运算对象与右侧运算对象的一个副本进行交换,在这个版本的赋值运算符中,参数并不是一个引用,我们将右侧运算对象以传值方式传递给赋值运算符,因此,rhs是右侧运算对象的一个副本,参数传递时拷贝Foo的操作会分配该对象的string的一个副本。在赋值运算符的函数体中,我们调用swap函数交换rhs和*this中的数据成员。这个调用将左侧运算对象原来保存的指针存入rhs中,并将rhs中原来的指针存入*this中,因此,在swap调用之后,*this中的指针成员将指向新分配的右侧运算对象中string的一个副本。当赋值运算符结束时,rhs被销毁,Foo的析构函数将被执行。此析构函数delete rhs现在指向的内存,即释放掉左侧运算对象中原来的内存。这个技术的有趣之处是它自动处理了自我赋值情况且天然就是异常安全的。它通过在改变左侧运算对象之前拷贝右侧运算对象保证了自我赋值的正确,这与我们在原来的赋值运算符中使用的方法是一样的。它保证异常安全的方法也是与原来的赋值运算符实现一样,代码中唯一可能抛出异常的是拷贝构造函数中的new表达式。如果真发生了异常,它会在我们改变左侧运算对象之前发生。
class Foo
{
private:
	string *str;
	int val;
public:
	friend void swap(Foo &rhs1,Foo &rhs2);
	Foo & operator=(Foo rhs)  //注意rhs是按值传递的,意味着Foo的
	{                         //拷贝构造函数将右侧运算对象中string拷贝到rhs
		swap(*this,rhs);     //rhs指向本对象曾经使用的内存
		return *this;        //rhs被销毁
	}
}

inline void swap(Foo &lhs, Foo &rhs)
{
	using std::swap;
	swap(lhs.str,rhs.str);
	swap(lhs.val,rhs.val);
}

你可能感兴趣的:(拷贝赋值运算符处理自我赋值和异常安全检查)