读书笔记effectivec++Item25实现一个不抛出异常的swap

1. swap如此重要

Swap是一个非常有趣的函数,最初作为STL的一部分来介绍,它已然变成了异常安全编程的中流砥柱(Item 29),也是在拷贝中应对自我赋值的一种普通机制(Item 11)。Swap非常有用,恰当的实现swap是非常重要的,与重要性伴随而来的是一些并发症。在这个条款中,我们将探索这些并发症以及如何处理它们。

2. swap的傻瓜实现方式及缺陷

2.1 swap函数的默认实现

Swap函数就是将两个对象的值进行交换,可以通过使用标准的swap算法来实现:

  
 template<typename T>  
  
  
  
 a = 
 b = 
  
 }

只要你的类型支持拷贝(拷贝构造函数和拷贝赋值运算符),默认的swap实现不需要你做一些特别的工作来支持它。

2.2 swap函数默认实现的缺陷——效率低

然而,默认的swap实现也许并没有让你激动,它包括三次拷贝:a 拷贝到temp,b拷贝到a, temp拷贝到b。对于一些类型来说,这些拷贝不是必须的,默认的swap将你从快车道拉到了慢车道。

这些不需要拷贝的类型内部通常包含了指针,指针指向包含真实数据的其他类型。使用这种设计方法的一个普通的例子就是“pimpl idiom”(指向实现的指针 Item 31).举个例子:

  
  
  
  
  
 std::vector< 
  
  
  
  
 Widget( 
 Widget&  
 {  
 ...  
 *pImpl = *(rhs.pImpl);  
 ...  
  
  
  
 WidgetImpl *pImpl;  
 };   
 template<>  
  
 Widget& b)  
  
 swap(a.pImpl, b.pImpl);  
 }  
  
 }

开始的”templpate<>”说明这是对std::swap的模板全特化(total template specializaiton),名字后面的”<Widget>”是说明这个特化只针对T为Widget类型。换句话说,当泛化的swap模板被应用到Widget类型时,应该使用上面的实现方法。一般来说,我们不允许修改std命名空间的内容,但是却允许使用我们自己创建的类型对标准模板进行全特化

但是这个函数不能编译通过。这是因为它尝试访问a和b中的pImpl指针,它们是private的。我们可以将我们的特化函数声明成friend,但是传统做法却是这样:在Widget中声明一个真正执行swap的public成员函数swap,让std::swap调用成员函数:

  
  
  
  
  
  
  
 swap(pImpl, other.pImpl);  
 }  
  
  
  
 template<>  
  
 Widget& 
  
 a.swap(b);  
 }  
 }

这种做法不仅编译能通过,同STL容器一致,它们都同时为swap提供了public成员函数版本和调用成员函数的std::swap版本。

3.2 为模板类定义偏特化版本swap

然而假设Widget和WidgetImpl换成了类模版,我们就将存储在WidgetImpl中的数据类型替换成一个模板参数:

 template<typename T>
 
  
 template<typename T>
 
   
 template<typename T>
 
  
 Widget<T>& 
  
 }

上面的代码看上去完全合理,但却是不合法的。我们尝试偏特化(partially specialize)一个模板(std::swap),虽然允许对类模版进行偏特化,却不允许对函数模板进行偏特化。因此这段代码不能通过编译(虽然有些编译器错误的通过了编译)。

当你想“偏特化”一个函数模板的时候,常见的方法是添加一个重载函数。像下面这样:

  
 template<typename T>  
  
 Widget<T>& b)  
 { a.swap(b); }  
 }

一般来说,对函数模板进行重载是可以的,但是std是一个特殊的命名空间,使用它的规则也很特殊。std中进行全特化是可以的,但是添加新的模板(类,函数或其他任何东西)不可以。Std的内容完全由C++标准委员会来决定。越过这条线的程序肯定可以通过编译并且能运行,但是行为未定义。如果你想你的软件有可预测的行为,不要向std中添加新东西。

那该怎么做呢?我们仍然需要一种方式来让其他人调用我们的高效的模板特化版本的swap。答案很简单。我们仍然声明一个调用成员函数swap的非成员swap,但我们不将非成员函数声明为std::swap的特化或者重载。举个例子,和 Widget相关的功能被定义在命名空间WidgetStuff中,像下面这样:

  
 ...  
 template<typename T>  
  
  
 template<typename T>  
  
 Widget<T>& 
  
  
  
 }

现在,如果在任何地方调用swap,C++ 中的名字搜寻策略(name lookup rules)将会在WidgetStuff中搜寻Widget的指定版本。这正是我们需要的。

4. 普通类中swap的特化版本和非成员函数版本都需要提供

这种方法对类同样有效,所以看上去我们应该在任何情况下都使用它。不幸的是,你还需要为类提供特化的std::swap(稍后解释)版本,所以如果你想在尽可能多的上下文环境中调用swap的类特定版本,你需要同时在类命名空间中定义swap的非成员函数版本和std::swap的特化版本。

5. 调用swap时的搜寻策略

至今为止我已经实现的都要从属于swap的作者,但从客户角度来看有一种情况值得注意。假设你正在实现一个函数模板,函数中需要对两个对象的值进行swap:

 template<typename T>
 
  
  
  
  
  
 }

他会调用swap的哪个版本?已存的std中的版本?可能存在也可能不存在的std中的特化版本?还是可能存在也可能不存在的,可能在一个命名空间内也可能不在一个命名空间内(肯定不应该在std中)T特定版本?你所需要的是如果有的话就调用一个T特定版本,没有的话就调用std中的普通版本。下面来实现你的需求:

 template<typename T>
 
  
  
  
  
 swap(obj1, obj2);  
  
 }

当编译器看到了对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)类似的变量),那么按照下面的去做:

  1. 提供一个public 的swap成员函数,对你的类型的两个对象值可以高效的swap。原因一会解释,这个函数永远不应该抛出异常。
  2. 在与你的类或模板相同的命名空间中提供一个非成员swap。让它调用你的swap成员函数版本。
  3. 如果你正在实现一个类(不是一个类模版),为你的类特化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中添加全新的东西。
更多相关文章
  • 书中提到,如果swap的缺省实现对你的class或class template 效率不足,试着做一下事情 提供一个public swap成员函数,让它高效的置换你的类型的两个对象. 在你的class 或template 所在的命名空间内提供一个non-member swap, 并另它调用上述swap ...
  • 读书笔记C#学习笔记八:StringBuilder与String详解及参数传递问题剖析
    前言 addnum = addnum + } Console.WriteLine( addnum = addnum + } 运行结果是: 从结果中可以看出 RefClass refClass = addnumRef.addnum += } 运行结果为: oldStr = } 运行结果为: Conso ...
  • JavaScript、jQuery、HTML5、Node.js实例大全-读书笔记3
    技术很多,例子很多,只好慢慢学,慢慢实践!!现在学的这本书是 JavaScript.jQuery.HTML5.Node.js实例大全-读书笔记2 3.3 响应鼠标动作 图3-2的效果已经有了,需要鼠标来操作展示想看的照片,这就需要在相应的地方加上事件. 3.3.1 响应小照片单击动作 在3.2.3的 ...
  • 存储器的保护三——x86汇编语言:从实模式到保护模式读书笔记20
    存储器的保护(三) 修改本章代码清单,使之可以检测1MB以上的内存空间(从地址0x0010_0000开始,不考虑高速缓存的影响).要求:对内存的读写按双字的长度进行,并在检测的同时显示已检测的内存数量.建议对每个双字单元用两个花码0x55AA55AA和0xAA55AA55进行检测. 上面的文字选自原 ...
  • 从林开始--C++primer读书笔记--Part1:TheBasics
    从「林」开始--C++ primer 读书笔记 -- Part1: The Basics ##################################### //声明:1: 本文根据自己阅读的重点记录而已 //          2:笔记基本都是从<C++ Primer第四版中英文对照 ...
  • 2016读书3月读书笔记
    1.<梵高传> 欧文·斯通       The Starry Night. 1889. 阿尔     很久都没有看过小说了,将近20天前,偶然在图书馆的推荐书籍面前看到了这本书,一时没有在意,后来看见还在那,于是拿来翻了一翻,然后就果断拿去看了,没想到一看就停不下来..本书讲述了梵高从2 ...
  • Docker笔记--镜像中部署一个tomcat
    前篇笔记中搞定了一个镜像并安装了jdk 本想着这tomcat会更简单,后来发现我错了. 且看下面过程: 我这个镜像原始的系统就有openssh,只需要进到镜像里 passwd一个新的密码.退出后commit一下就添加了一个ssh服务. 之后通过以下命令在后台启动镜像,执行ssh服务,开放22端口 d ...
  • QB程序语言设计读书笔记
    今天从网上找了个<QB程序语言设计>的PDF,看完之后总结了几条笔记,也亲自在自己的计算机上验证了下大部分的程序和用法,现在我把今天的工作记下来,以备日后用到. <QB程序语言设计>,周察金主编,高等教育出版社,ISBN号为7–04–015152–9 这里说的QB即QBasi ...
  • JavaScript 基础优化JavaScript 高级程序设计读书笔记
    1.带有 src 属性的<script>元素不应该在其<script>和</script>标签之间再包含额外的 JavaScript 代码.如果包含了嵌入的代码,则只会下载并执行外部脚本文件,嵌入的代码会被忽略.一般都把全部 JavaScript 引用放在< ...
一周排行