读书笔记 effective c++ Item 25 实现一个不抛出异常的swap
1. swap如此重要
Swap是一个非常有趣的函数,最初作为STL的一部分来介绍,它已然变成了异常安全编程的中流砥柱(Item 29),也是在拷贝中应对自我赋值的一种普通机制(Item 11)。Swap非常有用,恰当的实现swap是非常重要的,与重要性伴随而来的是一些并发症。在这个条款中,我们将探索这些并发症以及如何处理它们。
2. swap的傻瓜实现方式及缺陷
2.1 swap函数的默认实现
Swap函数就是将两个对象的值进行交换,可以通过使用标准的swap算法来实现:
namespace std { template<typename T> // typical implementation of std::swap; void swap(T& a, T& b) // swaps a’s and b’s values { T temp(a); a = b; b = temp; } }
只要你的类型支持拷贝(拷贝构造函数和拷贝赋值运算符),默认的swap实现不需要你做一些特别的工作来支持它。
2.2 swap函数默认实现的缺陷——有可能效率低
然而,默认的swap实现也许并没有让你激动,它包括三次拷贝:a 拷贝到temp,b拷贝到a, temp拷贝到b。对于一些类型来说,这些拷贝不是必须的,默认的swap将你从快车道拉到了慢车道。
这些不需要拷贝的类型内部通常包含了指针,指针指向包含真实数据的其他类型。使用这种设计方法的一个普通的例子就是“pimpl idiom”(指向实现的指针 Item 31).举个例子:
class WidgetImpl { // class for Widget data; public: // details are unimportant ... private: int a, b, c; // possibly lots of data — std::vector<double> v; // expensive to copy! ... }; class Widget { // class using the pimpl idiom public: Widget(const Widget& rhs); Widget& operator=(const Widget& rhs) // to copy a Widget, copy its { // WidgetImpl object. For ... // details on implementing *pImpl = *(rhs.pImpl); // operator= in general, ... // see Items 10, Item 11, and Item 12. } ... private: WidgetImpl *pImpl; // ptr to object with this }; // Widget’s data
为了交换两个Widget对象的值,我们实际上唯一需要做的是交换两个pImpl指针,但是默认的swap算法没有办法能够获知这些。它不仅拷贝了三个Widget对象,还拷贝了三个WidgetImpl对象。非常没有效率,也不令人鸡冻。
3. 如何实现一个高效的swap
3.1 为普通类定义全特化版本swap
我们需要做的就是告诉std::swap当Widget对象被swap的时候,执行swap的方式是swap内部的pImpl指针。也就是为Widget定制一个std::swap。这是最基本的想法,看下面的代码,但是不能通过编译。。
namespace std { template<> // this is a specialized version void swap<Widget>(Widget& a, // of std::swap for when T is Widget& b) // Widget { swap(a.pImpl, b.pImpl); // to swap Widgets, swap their } // pImpl pointers; this won’t compile }
开始的”templpate<>”说明这是对std::swap的模板全特化(total template specializaiton),名字后面的”<Widget>”是说明这个特化只针对T为Widget类型。换句话说,当泛化的swap模板被应用到Widget类型时,应该使用上面的实现方法。一般来说,我们不允许修改std命名空间的内容,但是却允许使用我们自己创建的类型对标准模板进行全特化。
但是这个函数不能编译通过。这是因为它尝试访问a和b中的pImpl指针,它们是private的。我们可以将我们的特化函数声明成friend,但是传统做法却是这样:在Widget中声明一个真正执行swap的public成员函数swap,让std::swap调用成员函数:
class Widget { // same as above, except for the public: // addition of the swap mem func ... void swap(Widget& other) { using std::swap; // the need for this declaration // is explained later in this Item swap(pImpl, other.pImpl); // to swap Widgets, swap their } // pImpl pointers ... }; namespace std { template<> // revised specialization of void swap<Widget>(Widget& a, // std::swap Widget& b) { a.swap(b); // to swap Widgets, call their } // swap member function }
这种做法不仅编译能通过,同STL容器一致,它们都同时为swap提供了public成员函数版本和调用成员函数的std::swap版本。
3.2 为模板类定义偏特化版本swap
然而假设Widget和WidgetImpl换成了类模版,我们就将存储在WidgetImpl中的数据类型替换成一个模板参数:
template<typename T> class WidgetImpl { ... }; template<typename T> class Widget { ... };
在Widget中实现一个swap成员函数和原来一样简单,但是std::swap的特化遇到了麻烦。下面是我们想写出来的:
namespace std { template<typename T> void swap<Widget<T> >(Widget<T>& a, // error! illegal code! Widget<T>& b) { a.swap(b); } }
上面的代码看上去完全合理,但却是不合法的。我们尝试偏特化(partially specialize)一个模板(std::swap),虽然允许对类模版进行偏特化,却不允许对函数模板进行偏特化。因此这段代码不能通过编译(虽然有些编译器错误的通过了编译)。
当你想“偏特化”一个函数模板的时候,常见的方法是添加一个重载函数。像下面这样:
namespace std { template<typename T> // an overloading of std::swap void swap(Widget<T>& a, // (note the lack of “<...>” after Widget<T>& b) // “swap”), but see below for { a.swap(b); } // why this isn’t valid code }
一般来说,对函数模板进行重载是可以的,但是std是一个特殊的命名空间,使用它的规则也很特殊。在std中进行全特化是可以的,但是添加新的模板(类,函数或其他任何东西)不可以。Std的内容完全由C++标准委员会来决定。越过这条线的程序肯定可以通过编译并且能运行,但是行为未定义。如果你想你的软件有可预测的行为,不要向std中添加新东西。
那该怎么做呢?我们仍然需要一种方式来让其他人调用我们的高效的模板特化版本的swap。答案很简单。我们仍然声明一个调用成员函数swap的非成员swap,但我们不将非成员函数声明为std::swap的特化或者重载。举个例子,和 Widget相关的功能被定义在命名空间WidgetStuff中,像下面这样:
namespace WidgetStuff { ... // templatized WidgetImpl, etc. template<typename T> // as before, including the swap class Widget { ... }; // member function ... template<typename T> // non-member swap function; void swap(Widget<T>& a, // not part of the std namespace Widget<T>& b) { a.swap(b); } }
现在,如果在任何地方调用swap,C++ 中的名字搜寻策略(name lookup rules)将会在WidgetStuff中搜寻Widget的指定版本。这正是我们需要的。
4. 普通类中swap的特化版本和非成员函数版本都需要提供
这种方法对类同样有效,所以看上去我们应该在任何情况下都使用它。不幸的是,你还需要为类提供特化的std::swap(稍后解释)版本,所以如果你想在尽可能多的上下文环境中调用swap的类特定版本,你需要同时在类命名空间中定义swap的非成员函数版本和std::swap的特化版本。
5. 调用swap时的搜寻策略
至今为止我已经实现的都要从属于swap的作者,但从客户角度来看有一种情况值得注意。假设你正在实现一个函数模板,函数中需要对两个对象的值进行swap:
template<typename T> void doSomething(T& obj1, T& obj2) { ... swap(obj1, obj2); ... }
他会调用swap的哪个版本?已存的std中的版本?可能存在也可能不存在的std中的特化版本?还是可能存在也可能不存在的,可能在一个命名空间内也可能不在一个命名空间内(肯定不应该在std中)T特定版本?你所需要的是如果有的话就调用一个T特定版本,没有的话就调用std中的普通版本。下面来实现你的需求:
template<typename T> void doSomething(T& obj1, T& obj2) { using std::swap; // make std::swap available in this function ... swap(obj1, obj2); // call the best swap for objects of type T ... }
当编译器看到了对swap的调用,它们会寻找swap的正确版本。C++名字搜寻策略先在全局范围内或者同一个命名空间内搜寻swap的T特定版本。(例如,如果T是命名空间WidgetStuff中的Widget,编译器会用参数依赖搜寻(argument-dependent lookup)在WidgetStuff中寻找swap).如果没有T特定的swap版本存在,编译器会使用std中的swap版本,多亏了using std::swap使得std::swap在函数中可见。但是编译器更喜欢普通模板std::swap上的T指定特化版本,因此如果std::swap已经为T特化过了,特化版本将会调用。
6. 调用swap时不要加std限定符
因此调用正确的swap版本很容易。一件你需要注意的事情是不要对调用进行限定,因为这会影响c++决定调用哪个函数。举个例子,如果你像下面这样调用swap:
std::swap(obj1, obj2); // the wrong way to call swap
你强制编译器只考虑std中的swap版本(包含所有模板特化版本),这样就调不到在其他地方定义的更加合适的T特定版本了(如果有的话)。一些被误导的程序员确实就对swap的调用进行了这种限定,因此为你的类对std::swap进行全特化很重要:它使得被误导的程序员即使使用错误的调用方式(加std限定)也能够调用特定类型的swap版本。
7. 实现swap步骤小结
到现在我们已经讨论了默认swap,成员函数swap,非成员函数swap以及std::swap的特化版本,并且讨论了对swap的调用,让我们总结一下:
首先,如果为你的类或者类模版提供的swap默认实现版本在效率上可以满足你,你就什么都不需要做。任何人尝试对你定义类型的对象进行swap,只要调用默认版本就可以了,这会工作的很好。
其次,如果swap的默认实现在效率上达不到你的要求(通常就意味着你的类或者类模板在使用同指向实现的指针(pimpl idiom)类似的变量),那么按照下面的去做:
- 提供一个public 的swap成员函数,对你的类型的两个对象值可以高效的swap。原因一会解释,这个函数永远不应该抛出异常。
- 在与你的类或模板相同的命名空间中提供一个非成员swap。让它调用你的swap成员函数版本。
- 如果你正在实现一个类(不是一个类模版),为你的类特化std::swap。让他也调用你的swap成员函数版本。
最后,如果你正在调用swap,确保在你的函数中include一个using声明来使得std::swap是可见的,然后调用swap时不要加std命名空间对其进行限定。
8. 最后的警告——不要让成员函数swap抛出异常
我最后的警告是永远不要让swap成员函数版本抛出异常。因为swap的一个最有用的地方就是帮助类(或类模版)提供强有力的异常安全保证。Item 29中有详细解释,其中的技术也是建立在swap成员函数版本不会抛出异常的假设之上的。这个约束只针对成员函数版本!而不针对非成员函数版本,因为swap的默认版本是基于拷贝构造函数和拷贝赋值运算符的,而一般情况下,这两个函数都允许抛出异常。当你实现一个swap的个性化版本,你就不单单提供了对值进行swap的高效方法;你同时提供了一个不会抛出异常的函数。作为通用规则,swap的这两个特性总是会在一起的,因为高效的swap通常是建立在对内建类型进行操作的基础之上的(像底层的指向实现的指针),而内建类型永远不会抛出异常。
9. 总结
- 当使用std::swap对你的自定义类型进行swap时,如果效率不够高,那么提供一个成员函数版本,并确保这个函数不会抛出异常。
- 如果你提供了一个成员函数swap,同时提供了一个非成员swap来调用成员swap。在类(不是模板)上对std::swap进行特化。
- 当调用swap时,使用using std::swap声明,对调用的swap不使用命名空间限定。
- 为用户定义类型全特化std模板没有问题,但永远不要尝试像std中添加全新的东西。
读书笔记 effective c++ Item 25 实现一个不抛出异常的swap的更多相关文章
- 读书笔记 effective c++ Item 54 让你自己熟悉包括TR1在内的标准库
1. C++0x的历史渊源 C++标准——也就是定义语言的文档和程序库——在1998被批准.在2003年,一个小的“修复bug”版本被发布.然而标准委员会仍然在继续他们的工作,一个“2.0版本”的C+ ...
- 读书笔记 effective c++ Item 29 为异常安全的代码而努力
异常安全在某种意义上来说就像怀孕...但是稍微想一想.在没有求婚之前我们不能真正的讨论生殖问题. 假设我们有一个表示GUI菜单的类,这个GUI菜单有背景图片.这个类将被使用在多线程环境中,所以需要mu ...
- 读书笔记 effective c++ Item 41 理解隐式接口和编译期多态
1. 显示接口和运行时多态 面向对象编程的世界围绕着显式接口和运行时多态.举个例子,考虑下面的类(无意义的类), class Widget { public: Widget(); virtual ~W ...
- 读书笔记 effective c++ Item 1 将c++视为一个语言联邦
Item 1 将c++视为一个语言联邦 如今的c++已经是一个多重泛型变成语言.支持过程化,面向对象,函数式,泛型和元编程的组合.这种强大使得c++无可匹敌,却也带来了一些问题.所有“合适的”规则看上 ...
- 读书笔记 effective c++ Item 5 了解c++默认生成并调用的函数
1 编译器会默认生成哪些函数 什么时候空类不再是一个空类?答案是用c++处理的空类.如果你自己不声明,编译器会为你声明它们自己版本的拷贝构造函数,拷贝赋值运算符和析构函数,如果你一个构造函数都没有声 ...
- 读书笔记 effective c++ Item 9 绝不要在构造函数或者析构函数中调用虚函数
关于构造函数的一个违反直觉的行为 我会以重复标题开始:你不应该在构造或者析构的过程中调用虚函数,因为这些调用的结果会和你想的不一样.如果你同时是一个java或者c#程序员,那么请着重注意这个条款,因为 ...
- 读书笔记 effective c++ Item 11 在operator=中处理自我赋值
1.自我赋值是如何发生的 当一个对象委派给自己的时候,自我赋值就会发生: class Widget { ... }; Widget w; ... w = w; // assignment to sel ...
- 读书笔记 effective c++ Item 12 拷贝对象的所有部分
1.默认构造函数介绍 在设计良好的面向对象系统中,会将对象的内部进行封装,只有两个函数可以拷贝对象:这两个函数分别叫做拷贝构造函数和拷贝赋值运算符.我们把这两个函数统一叫做拷贝函数.从Item5中,我 ...
- 读书笔记 effective c++ Item 13 用对象来管理资源
1.不要手动释放从函数返回的堆资源 假设你正在处理一个模拟Investment的程序库,不同的Investmetn类型从Investment基类继承而来, class Investment { ... ...
随机推荐
- poi jar包介绍
来自官网: Component Application type Maven artifactId Notes POIFS OLE2 Filesystem poi Required to work w ...
- iOS 开源库 之 AFNetWorking 2.x
1. 网络请求的基本知识 2. Get/Post 请求的使用 3. 文件(图片)上传 4. 断点下载 5. 其它使用细节 6. 设计优良的地方
- 简单的shared_ptr实现
RT,代码参考了STL中shard_ptr的实现,基本原理是引用计数,利用Ref_cnt类来管理内存,在shared_ptr创建时创建,此后shared_ptr仅是在拷贝复制析构的过程中对引用进行修改 ...
- sqlite3编译
1.sqlite3编译: 1.PC版: 1.解压: tar xvf sqlite-autoconf-3140100.tar.gz cd sqlite-autoconf-3140100/ 2.检查配置 ...
- js原生之scrollTop、offsetHeight和offsetTop等属性用法详解
scrollTop.offsetHeight和offsetTop等属性用法详解:标题中的几个相关相关属性在网页中有这大量的应用,尤其是在运动框架中,但是由于有些属性相互之间的概念比较混杂或者浏览器兼容 ...
- glyph
glyph[英][glɪf][美][ɡlɪf]n.象形文字; 纵的沟纹; 竖沟; 浮雕;
- React Router基础使用
React是个技术栈,单单使用React很难构建复杂的Web应用程序,很多情况下我们需要引入其他相关的技术 React Router是React的路由库,保持相关页面部件与URL间的同步 下面就来简单 ...
- HDU5842
Lweb and String Time Limit: 2000/1000 MS (Java/Others) Memory Limit: 65536/65536 K (Java/Others)T ...
- 在C语言中以编程的方式获取函数名
仅仅为了获取函数名,就在函数体中嵌入硬编码的字符串,这种方法单调乏味还易导致错误,不如看一下怎样使用新的C99特性,在程序运行时获取函数名吧. 对象反射库.调试工具及代码分析器,经常会需要在运行时访问 ...
- canvas动态小球重叠效果
前面的话 在javascript运动系列中,详细介绍了各种运动,其中就包括碰壁运动.但是,如果用canvas去实现,却是另一种思路.本文将详细介绍canvas动态小球重叠效果 效果展示 静态小球 首先 ...