最近开始看《Effective C++》,为了方便以后回顾,特意做了笔记。若本人对书中的知识点理解有误的话,望请指正!!!

swap函数是一个非常经典又有用的函数,除了它本身用来交换两个对象数值的功能,还可以用来实现异常安全的赋值,避免自赋值(见条款11)等等用途。在std标准库里,swap函数就是这样实现的:

namespace std{template<typename T>void swap(T& a, T& b){T temp(a);a = b;b = temp;}
}

只要类型T支持拷贝函数(通过拷贝构造函数和拷贝赋值运算符完成),默认的swap实现代码就会帮你置换类型为T的对象,你无需再做任何工作。

这个默认的swap实现版本十分普通。它涉及了三个对象的复制:a 复制到 temp,b 复制到 a,以及 temp 复制到 b。如果类型T的大小很大,那么要消耗的内存也很大。

要解决这样的问题,有一个常用的方法叫pimpl(“pointer to implementation”)。它的概念是要把类的实现细节从中移除,放在另一个类中,并通过一个指针进行访问。使用pimpl设计手法的类大概长这样:

 //这个类包含Widget类的数据
class WidgetImpl{public:...
private:int a,b,c;              //可能有许多数据,意味复制时间长std::vector<double> v;    //高成本拷贝警告!
};//这个类使用pimpl手法
class Widget{
public:Widget(const Widget& rhs);//复制Widget时,令它复制WidgetImpl对象Widget& operator=(const Widget& rhs)//赋值运算符的实现见条款10,11,12{...*pImpl = *(rhs.pImpl);...}...
private:WidgetImpl* pImpl; //使用pimpl指针来指向Widget数据
};

一旦交换两个对象,直接交换指针就行了。可是默认的swap并不知道这些,它不止复制3个 Widget,还复制3个 WidgetImpl 对象。非常缺乏效率。我们能否告知std::swap:当交换Widget时,实际是交换其内存的 pImpl 指针?

这当然可以的:将std::swap针对 Widget特殊化(specialization),下面是基本构想,但目前无法通过编译:

namespace std{//特殊化的std::swap,当T是Widget类型时使用如下实现template<>void swap<Widget>(Widget& a, Widget& b){swap(a.pImpl,b.pImpl); //交换Widget时只要交换它们的pImpl指针}
}

template<> 代表了下面的代码是对 std::swap 的完全特殊化(total specialization)实现,函数名称后的 <Widget> 则代表了当T是 Widget 类型时使用这个特殊实现。也就是对于其它类型依然使用默认的 std::swap,仅仅对于Widget类型才使用特殊化。

请记住,通常我们不能够(不被允许)改变 std 命名空间内的任何东西,但可以(被允许)为标准 template(如swap)设计特殊化版本,使它专属于我们自己的类(如Widget )。

但这个函数是无法通过编译的,因为Widget 内的 pImpl 指针在 private 域内,我们无法访问。我们可以在Widget 内声明一个名为 swap 的 public 成员函数做真正的交换工作,然后将 std::swap 特殊化,令它调用该成员函数:

class Widget{//与前面一样,唯一差别时增加swap函数
public:...void swap(Widget& other){using std::swap;             //这句稍后解释swap(pImpl, other.pImpl); //执行真正的swap,只交换指针}...
};namespace std{template<> //完全特殊化的std::swapvoid swap<Widget>(Widget& a, Widget& b){a.swap(b);  //若要交换Widget,调用其swap成员函数}
}

这种做法不止能通过编译,还与 STL 容器有一致性。因为 STL 容器也使用了 public swap成员函数和一个特殊化的std::swap来调用这个成员函数实现高效交换功能。

类的交换的讨论结束了,那么对于类模板的交换呢?

template<typename T>
class WidgetImpl{...};
template<typename T>
class Widget{...};

如果我们还和上面一样,在 Widget 内增加个 swap 成员函数,这样是无法通过编译的:

namespace std{template<typename T>void swap<Widget<T>>(Widget<T>& a, Widget<T>& b){ //非法代码a.swap(b);}
}

这种做法叫部分特殊化(partial specialization),即 template<...> 参数表里面还有一个模板参数而不是完全特殊化的 template<>。C++只允许对类模板进行部分特殊化,但不允许对函数模板进行部分特殊化,因此这个方法是无法通过编译的。

当你打算部分特殊化一个函数模板时,通常的做法是写一个std::swap模板的重载:

namespace std{//定义一个重载函数模板template<typename T>void swap(Widget<T>& a, Widget<T>& b){ //请注意与上面的swap的区别,函数名后面没有了<...>就不是特殊化了a.swap(b);}
}

一般而言,重载函数模板没有问题,但 std 是个特殊的命名空间,我们可以对其中的模板进行特殊化,但不允许添加新的模板。因为只有C++委员会才可以对std的内容进行修改。

那现在怎么办呢?我们可以声明一个非成员函数 swap,让它调用成员函数 swap,但不再将那个非成员函数 swap 声明为 std::swap 的特殊化版本或重载版本。假设 Widget 的所有相关机能都在命名空间 WidgetStuff (不能放在 std 命名空间)内:

//我们自己的名空间
namespace WidgetStuff{//我们的类模板template<typename T>class Widget{...};...//swap函数和类模板在同一命名空间template<typename T>void swap(Widget<T>& a, Widget<T>& b){a.swap(b);}...
}

这样做还有一个好处就是能把我们自定义的类相关的所有功能全部整合在一起,在逻辑上和代码上都更加整洁。而且这也符合C++的函数搜索规则,会自动在函数每个实参的命名空间和全局作用域内查找函数,即实参依赖查找(argument dependent lookup)。

这个方法既适用于类也适用于类模板,所以我们应该在任何时候都使用它。但是我们必须对 std::swap 进行特殊化,如果想让我们的 swap 函数适用于更多情况,那么除了在我们自己的命名空间里写一个 swap,在 std 里面依然要特殊化一个 swap,下面就会讲到。

目前为止,我们所写的每一样东西都和 swap 编写者有关。换位思考,从用户观点看事情也有必要。假如我们正在写一个函数模板,其内需要交换两个对象值:

template<typename T>
void doSomething(T& obj1, T& obj2){...swap(obj1,obj2);..
}

应该调用哪个 swap?是 std 默认的那个版本,还是某个可能存在的特殊化版本,抑或是一个可能存在的 T 专属版本而且可能栖身于某个命名空间(但当然不可以是 std)内。 最理想的情况是,调用 T 专属版本,并在它不存在的情况下调用 std 内的一般化版本:

template<typename T>
void doSomething(T1& obj1, T2& obj2){..using std::swap; //让std::swap对编译器可见swap(obj1,obj2); //为T类型对象调用最佳swap版本...
}

当编译器看到要调用 swap 的时候,实参依赖查找会让编译器在全局作用域和实参所在的命名空间里搜索适当的 swap 调用。例如,如果T是 Widget 类型,那么编译器就会使用实参依赖查找找到 Widget 的命名空间里的 swap。如果没有的话,编译器使用 std 内的 swap,这归功于 using std::swap 让 std::swap 在函数内曝光。然而编辑器还是比较喜欢 std::swap 的 T 专属特殊化版本,而非默认的 swap,所以如果你已针对 T 将 std::swap 特殊化,特殊化版本会被编译器挑中。

因此,令适当的 swap 被调用是很容易的,但是需要注意的是,别为这一调用添加额外的修饰符,因为这会影响C++挑选适当的函数,如下:

std::swap(obj1,obj2); //这是错误的调用方式

这强迫了编译器只认 std 内的swap(包括其任何特殊化的模板),因此不可能再调用一个适合它的 swap 函数。

我们讨论了 swap 的默认版本、成员函数版本、非成员函数版本 、std 特殊化版本以及对 swap 的调用,现在我们来总结一下:

  • 首先,如果 swap 的默认版本对你的类或类模板提供可接受的效率,你不需要额外做任何事
  • 其次,如果 swap 的默认版本的效率不足(意味着你的类和模板使用了某种 pimpl 手法),试着做以下事情:
    • 提供一个 public swap 成员函数,让它高效的交换你的类型的两个对象值,这个函数绝不能抛出异常
    • 在你的类或模板所在的命名空间内提供一个非成员函数 swap ,并令它调用上面的 swap 成员函数
    • 如果你正编写一个类(而非类模板),为你的类特殊化 std::swap ,并令它调用你的 swap 成员函数
  • 最后,如果你调用 swap ,请加上 using std::swap,以便让 std::swap 在你的函数内曝光,然后不加任何命名空间名字调用 swap

为什么成员函数 swap 不能抛出异常? 因为 swap 这个功能本身经常会被用来实现异常安全。但是非成员函数的 swap 则可能会抛出异常,因为它还包括了拷贝构造等功能,而这些功能则是允许抛出异常的。当你写一个高效的 swap 实现时,要记住不仅仅是为了高效,更要保证异常安全,但总体来讲,高效和异常安全是相辅相成的。

Note:

  • 当 std::swap 对你的类型效率不高时,提供一个 swap 成员函数,并确定这个函数不抛出异常
  • 如果你提供一个成员函数 swap,也该提供一个非成员函数 swap 用来调用前者,对于 类(而非模板),也请特殊化 std::swap
  • 调用 swap 时应针对 std::swap 使用 using 声明式,然后调用 swap 并且不带任何命名空间资格修饰符
  • 为用户定义类型进行 std 模板全特殊化时好的,但千万不能在 std 内加入任何新模板

条款26:尽可能推迟变量定义

《Effective C++》学习笔记(条款25:考虑写出一个不抛异常的swap函数)相关推荐

  1. 《Effective C++》item25:考虑写出一个不抛异常的swap函数

    std::swap()是个很有用的函数,它可以用来交换两个变量的值,包括用户自定义的类型,只要类型支持copying操作,尤其是在STL中使用的很多,例如: int main(int argc, _T ...

  2. 25 考虑写出一个不抛异常的swap

    一.std::swap std::swap面对指针(如pImpl写法)时效率低,因此需要重写swap 1.1 如何重写swap 为标准库提供特化版本(全特化) //pImpl写法的Widget cla ...

  3. 用js写出数据结构中的自定义队列,利用队列思想写出一个击鼓传花的游戏函数,优先级队列

    队列的核心是先进先出 1.用js写出数据结构中的自定义队列 class Queue{constructor(){this.item = [];}// 1.入队enqueue(ele){this.ite ...

  4. Effective C++ 学习笔记 条款05 了解C++默默编写并调用了哪些函数

    当写下一个空类时,编译器会为你合成一个拷贝构造函数.一个拷贝赋值运算符.一个析构函数,如没有声明其他的构造函数,编译器会合成一个默认构造函数.这些都是inline的public成员. 当类有一个引用成 ...

  5. Effective c++学习笔记条款20:宁以 pass-by-reference-to-const替换pass-by-value

    Prefer pass-by-reference-to-const to pass-by-value         这个问题在在C++是非常常见的.传值和传引用巨大的差别在于你所使用的参数是其本身还 ...

  6. onvif学习笔记8:最近写的一个ONVIF客户端的心得小结

    最近,利用业余时间编写一个简单的ONVIF客户端MFC程序,这里记录一下过程. 语言选择 在之前调查过,有用QT实现的,有用python实现的,虽然正在学python,无奈才看了3天的书,没自信去搞. ...

  7. 把一个字符串13579先变成Array——[1, 3, 5, 7, 9],再利用reduce(),就可以写出一个把字符串转换为Number的函数。

    function string2int(s) {let arr=s.split('');let numArr=arr.map(function pow(x) {return x * 1}); //将字 ...

  8. Effective C++ 学习笔记 第七章:模板与泛型编程

    第一章见 Effective C++ 学习笔记 第一章:让自己习惯 C++ 第二章见 Effective C++ 学习笔记 第二章:构造.析构.赋值运算 第三章见 Effective C++ 学习笔记 ...

  9. 深度学习笔记:Tensorflow手写mnist数字识别

    文章出处:深度学习笔记11:利用numpy搭建一个卷积神经网络 免费视频课程:Hellobi Live | 从数据分析师到机器学习(深度学习)工程师的进阶之路 上一讲笔者和大家一起学习了如何使用 Te ...

最新文章

  1. java 加密服务器_Javascript端加密java服务端解密
  2. “晶振”拍了拍你,“你知道我是如何工作的吗?”
  3. 4.边缘光照的描边shader
  4. java nio epoll_Java NIO 选择器(Selector)的内部实现(poll epoll)
  5. 《C++编程惯用法——高级程序员常用方法和技巧》——2.7 Const
  6. python如何并发上千个get_Python拓展21(python3X之百万并发借鉴)
  7. python 数组去重复_numpy数组去掉重复的行,保留唯一的行数据
  8. 对《谈谈培训机构的骗局给新人一些建议》这篇博文的个人评论
  9. C++(八)— 死锁原因及解决方法
  10. 使用virtualenv和pip构建项目所需的独立Python环境
  11. 阿里总裁马云对于第5个经济体技术有着独特的见解
  12. 02)高淇java300集学习笔记
  13. PHP网页输入年份查找生肖,vb十二生肖程序 根据年份的输入便可以查询到你是什么生肖...
  14. 【深度学习】 Designing Network Design Spaces
  15. 计算机中的成绩排名相同怎么弄,Excel:重复名次也可以查姓名成绩EXCEL基本教程 -电脑资料...
  16. OpenGL 与显卡
  17. 腾讯副总裁邱岳鹏:云的发展要迈过三道关
  18. 影像传感器尺寸换算(英寸-毫米)
  19. 无线网服务器mac是什么原因,苹果笔记本上网很慢怎么回事?macbook无线上网慢的解决方法...
  20. 小程序报错errMsg:hideLoading:fail:toast can't be found和hideToast:fail:toast can't be found?解决方案

热门文章

  1. Linux无线网络架构
  2. Android百度地图地理围栏定位间隔
  3. 前百度首席科学家张栋:36岁以前做到这8点再谈梦想
  4. NanoPi的网络配置
  5. 伦敦旅游攻略及注意事项(201909)
  6. linux终端如何连接wifi,如何在 Linux 终端中连接使用 WiFi?
  7. 用Chrome浏览器调试钉钉应用--Web页面调试
  8. python中import re_Python3中正则模块re.compile、re.match及re.search函数用法详解
  9. CleanMyMac4.11.2清理苹果电脑硬盘、删除垃圾文件软件
  10. 第三方服务之Bmob——答题系统小项目