读书笔记 effective c++ Item 11 在operator=中处理自我赋值

简介: 1.自我赋值是如何发生的 当一个对象委派给自己的时候,自我赋值就会发生: 1 class Widget { ... }; 2 3 Widget w; 4 5 ... 6 7 w = w; // assignment to self、 这看上去是愚蠢的,但这是合法的,所以请放心,客户端是可以这么做的。

1.自我赋值是如何发生的

当一个对象委派给自己的时候,自我赋值就会发生:

1 class Widget { ... };
2 
3 Widget w;
4 
5 ...
6 
7 w = w; // assignment to self、

这看上去是愚蠢的,但这是合法的,所以请放心,客户端是可以这么做的。此外,自身赋值也并不总是很容易的能够被辨别出来。举个例子:

1 a[i] = a[j]; // potential assignment to self

上面的代码在i和j相等的情况下就是自我赋值,同样的,看下面的例子:

*px = *py; // potential assignment to self

如果px和py恰巧指向同一个东西,那么上面的语句就是自身赋值。这些并不怎么明显的自我赋值是使用别名的结果:也就是使用不止一种方法来指向同一个对象。一般情况下,当我们操作指向不同同类型对象的引用和指针时,需要考虑这些不同的对象是否是同一个对象。事实上,如果两个对象来自同一个继承体系,这两个对象甚至不必声明为同类型的,因为基类的指针或者引用可以指向派生类对象:

1 class Base { ... };
2 
3 class Derived: public Base { ... };
4 
5 void doSomething(const Base& rb, // rb and *pd might actually be
6 
7 Derived* pd); // the same object

 

2.处理不好自我赋值会使你掉入陷阱

如果你遵循Item13和Item14的建议,你就会使用对象来管理资源,并且你也能够确信对资源进行管理的对象在进行拷贝时会运行的很好。在这种情况下,你的赋值运算符有可能就是自我赋值安全的,而不用去特定的考虑这件事情。如果你尝试自己来管理资源(如果你自己写一个资源管理类这是必须做的),你可能会掉入一个陷阱:在用完某个资源之前,资源突然被释放掉了。举个例子,假设你创建了一个类来管理一个原生指针,这个指针指向动态分配的bitmap对象:

 1 class Bitmap { ... };
 2 
 3 class Widget {
 4 
 5 ...
 6 
 7 private:
 8 
 9 Bitmap *pb; // ptr to a heap-allocated object
10 
11 };

 

下面是operator=的一个实现,从表面上看是合理的,但因为自我赋值的存在,实际上它是不安全的。(它也不是异常安全的,我们稍会会处理)

 1 Widget&
 2 
 3 Widget::operator=(const Widget& rhs) // unsafe impl. of operator=
 4 
 5 {
 6 
 7 delete pb; // stop using current bitmap
 8 
 9 pb = new Bitmap(*rhs.pb); // start using a copy of rhs’s bitmap
10 
11 return *this; // see Item 10
12 
13 }

 

自我赋值的问题出现在operator=内部,*this(赋值目标)和rhs可能是同一个对象。如果这是真的,delete不仅会为当前对象销毁bitmap,也同样会为ths销毁bitmap。在函数的结尾,Widget对象本不应该通过自我赋值有所改变,但你会发现现在它拥有的是一个指向被删除对象的指针!

3.处理自我赋值的方法一:鉴定测试,防止自我赋值

3.1 实现代码

防止这个错误的传统方法是operator=函数的开始进行一个鉴定测试,看是否是一个自我赋值:

 1 Widget& Widget::operator=(const Widget& rhs)
 2 
 3 {
 4 
 5 if (this == &rhs) return *this; // identity test: if a self-assignment,
 6 
 7 // do nothing
 8 
 9 delete pb;
10 
11 pb = new Bitmap(*rhs.pb);
12 
13 return *this;
14 
15 }

 

3.2这个方法的缺陷

这个方法是可以工作的,但是上面已经提到operator=的早先版本不仅是自我赋值不安全的,同样也是异常不安全的(exception-unsafe),在当前版本中关于异常的麻烦会继续存在。特别的,如果”new Bitmap”语句产生一个异常(因为没有足够的内存可以分配或者因为Bitmap的拷贝构造函数抛出一个异常),Widget将会拥有一个指向被删除Bitmap对象的指针。这样的指针是有毒的,因为你不能够安全的释放它们。你甚至不能够安全的读取它们。你唯一能够做的安全的事情就是花费大量的调试的精力来找出问题出在哪里。

4.处理自我赋值的方法二:对语句进行排序

 让人高兴的是,使operator=变得异常安全的方法也往往能使其变得自我赋值安全。所以,我们将自我赋值 的问题忽略掉,集中精力去达到异常安全。Item29比较深入的探索了异常安全,在这个条款中,我们只需要观察:对一些语句进行仔细的排序就可以生成exception安全(同样能够达到自我赋值安全)的代码,这就足够了。举个例子,我们只需要注意在对pb指向对象的拷贝完成之前不要将pb释放:

 1 Widget& Widget::operator=(const Widget& rhs)
 2 
 3 {
 4 
 5 Bitmap *pOrig = pb; // remember original pb
 6 
 7 pb = new Bitmap(*rhs.pb); // point pb to a copy of rhs’s bitmap
 8 
 9 delete pOrig; // delete the original pb
10 
11 return *this;
12 
13 }

现在,如果”new BItmap”抛出异常,pb仍然不会发生变化。在没有鉴别测试的情况下,这段代码进行了自我赋值,因为我们将源bitmap做了一份拷贝,让pb去指向拷贝的数据,然后删除源bitmap。这也许不是处理自我赋值的最有效率的方法,但这确实是可行的方法。

如果你关系效率,你可以将鉴别测试的代码重新放回到函数的开始处。但是在这么做之前,问问你自己,自我赋值发生的频率会有多高,因为鉴别测试不是免费的。它会增加一些代码(obj文件也会增大),同时引入了一个流程控制的分支,两者都会使得程序运行速度变慢。Prefetching,caching和pipelining指令的效率都会降低。

 

5.处理自我赋值的方法三:copy and swap

5.1 实现方法一 

我们换一种方法来对operator=中的语句进行手动排序,来同时保证自我赋值和异常安全,这种技术叫做拷贝和交换(copy  and swap)。这种技术与异常安全是紧密相关的,所以会在Item29中描述。然而,它也是实现operator=的一个非常普通的方法,因此值得我们来看看这种实现方法究竟是什么样子:

 1 class Widget {
 2 
 3 ...
 4 
 5 void swap(Widget& rhs); // exchange *this’s and rhs’s data;
 6 
 7 ... // see Item 29 for details
 8 
 9 };
10 
11 Widget& Widget::operator=(const Widget& rhs)
12 
13 {
14 
15 Widget temp(rhs); // make a copy of rhs’s data
16 
17 swap(temp); // swap *this’s data with the copy’s
18 
19 return *this;
20 
21 }

 

5.2 实现方法二

利用下面的两个事实我们可以将上面的实现换一种写法,这两个事实是:(1)一个类的拷贝赋值运算符可以被声明为按值传递。(2)按值传递会对值进行拷贝。下面是另外一种写法:

 1 Widget& Widget::operator=(Widget rhs) // rhs is a copy of the object
 2 
 3 { // passed in — note pass by val
 4 
 5 swap(rhs); // swap *this’s data with
 6 
 7 // the copy’s
 8 
 9 return *this;
10 
11 }

从个人观点来说,我担心这种方法为了聪明的实现而牺牲了代码的清晰度,但是通过将拷贝操作从函数体内移动到函数的参数中,编译器有时候能够产生更高效的代码,这是事实。


作者: HarlanC

博客地址: http://www.cnblogs.com/harlanc/
个人博客: http://www.harlancn.me/
本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出, 原文链接

如果觉的博主写的可以,收到您的赞会是很大的动力,如果您觉的不好,您可以投反对票,但麻烦您留言写下问题在哪里,这样才能共同进步。谢谢!

目录
相关文章
|
5月前
|
C++ 容器
C++之deque容器(构造、赋值、大小、插入与删除、存取、排序)
C++之deque容器(构造、赋值、大小、插入与删除、存取、排序)
|
5月前
|
C++ 容器
C++字符串string容器(构造、赋值、拼接、查找、替换、比较、存取、插入、删除、子串)
C++字符串string容器(构造、赋值、拼接、查找、替换、比较、存取、插入、删除、子串)
|
5月前
|
存储 编译器 C++
【C++】:拷贝构造函数和赋值运算符重载
【C++】:拷贝构造函数和赋值运算符重载
27 1
|
4月前
|
编译器 C++
【C++】详解运算符重载,赋值运算符重载,++运算符重载
【C++】详解运算符重载,赋值运算符重载,++运算符重载
|
5月前
|
编译器 C++
【C++】类和对象③(类的默认成员函数:赋值运算符重载)
在C++中,运算符重载允许为用户定义的类型扩展运算符功能,但不能创建新运算符如`operator@`。重载的运算符必须至少有一个类类型参数,且不能改变内置类型运算符的含义。`.*::sizeof?`不可重载。赋值运算符`=`通常作为成员函数重载,确保封装性,如`Date`类的`operator==`。赋值运算符应返回引用并检查自我赋值。当未显式重载时,编译器提供默认实现,但这可能不足以处理资源管理。拷贝构造和赋值运算符在对象复制中有不同用途,需根据类需求定制实现。正确实现它们对避免数据错误和内存问题至关重要。接下来将探讨更多操作符重载和默认成员函数。
|
5月前
|
算法 C++ 容器
C++之vector容器操作(构造、赋值、扩容、插入、删除、交换、预留空间、遍历)
C++之vector容器操作(构造、赋值、扩容、插入、删除、交换、预留空间、遍历)
202 0
|
5月前
|
编译器 C++
《Effective C++ 改善程序与设计的55个具体做法》 第二章 构造/析构/赋值运算 笔记
《Effective C++ 改善程序与设计的55个具体做法》 第二章 构造/析构/赋值运算 笔记
|
13天前
|
存储 编译器 对象存储
【C++打怪之路Lv5】-- 类和对象(下)
【C++打怪之路Lv5】-- 类和对象(下)
19 4
|
13天前
|
编译器 C语言 C++
【C++打怪之路Lv4】-- 类和对象(中)
【C++打怪之路Lv4】-- 类和对象(中)
16 4
|
13天前
|
存储 安全 C++
【C++打怪之路Lv8】-- string类
【C++打怪之路Lv8】-- string类
14 1