zoukankan      html  css  js  c++  java
  • 读书笔记_Effective_C++_条款二十五: 考虑写出一个不抛出异常的swap函数

    我也不知道为什么作者给这个条款起这样的名字,因为这样看上去重点是在“不抛出异常”,但事实上作者只是在全文最后一段说了一下不抛异常的原因,大部分段落是在介绍怎样写一个节省资源的swap函数。

    你可以试一下,只要包含了头文件iostream,就可以使用swap函数,比如:

    复制代码
    1 #include <iostream>
    2 
    3 int main()
    4 {
    5     int a = 3;
    6     int b = 4;
    7     std::swap(a, b);
    8 }
    复制代码

    结果就是a为4,b为3了,也就是说,在std的命名空间内,已经有现成的swap的函数了,这个swap函数很简单,它看起来像这样:

    复制代码
    1 template<class T>
    2 void swap(T& a, T& b)
    3 {
    4     T c = a;
    5     a = b;
    6     b = c;
    7 }
    复制代码

    这是最常见形式的两数交换了(特别地,当T是整数的时候,还可以使用异或的操作,这不是本条款讨论的重点,所以略过了,但面试题里面很喜欢问这个)。

    假设存在一个类Element,类中的元素比较占空间:

    复制代码
    1 class Element
    2 {
    3 private:
    4     int a;
    5     int b;
    6     vector<double> c;
    7 };
    复制代码

    Sample类中的私有成员是Element的指针,有原生指针,大多数情况下都需要自定义析构函数、拷贝构造函数和赋值运算符,像下面一样。

    复制代码
    1 class Sample
    2 {
    3 private:
    4     Element* p;
    5 public:
    6     ~Sample();
    7     Sample(const Sample&);
    8     Sample& operator= (const Sample&);
    9 };
    复制代码

    在实现operator=的时候,有一个很好的实现方法,参见条款十一。大概像这样:

    复制代码
    1 Sample& operator= (const Sample& s)
    2 {
    3     if(this != &s)
    4     {
    5         Sample temp(s);
    6         swap(*this, temp);
    7     }
    8     return *this;
    9 }
    复制代码

    当判断不是自我赋值后,是通过调用拷贝构造函数来创建一个临时的对象(这里可能会有异常,比如不能分配空间等等),如果这个对象因异常没有创建成功,那么下面的swap就不执行,这样不会破坏this的原始值,如果这个对象创建成功了,那么swap一下之后,把临时对象的值换成*this的值,达到了赋值的效果。

    上面的解释是条款九的内容,如果不记得了,可以回头翻翻看,本条款的重点在这个swap函数上。这里调用的是默认的std里面的swap函数,它会创建一个临时的Sample对象(拷贝构造函数),然后调用两次赋值运算,这就会调回来了,即在swap函数里面调用operator=,而之前又是在operator=中调用swap函数,这可不行,会造成无穷递归,堆栈会溢出。

    因此,我们要写一份自己的swap函数,这个函数是将Sample里面的成员进行交换。

    问题又来了,Sample里面存放的是指向Element的指针,那是交换指针好呢,还是逐一交换指针所指向的对象里面的内容好呢?Element里面的东西挺多的,所以显然还是直接交换指针比较好(本质是交换了Element对象存放的地址)。

    因此,可以定义一个swap的成员函数。像这样:

    复制代码
     1 void swap(Sample& s)
     2 {
     3     std::swap(p, s.p);
     4 }
     5 Sample& operator= (const Sample& s)
     6 {
     7     if(this != &s)
     8     {
     9         Sample temp(s);
    10         this->swap(s);
    11     }
    12     return *this;
    13 }
    复制代码

    但这样看上去有点别扭,我们习惯的是像swap(a, b)这种形式的swap,如果交给其他程序员使用,他们也希望在类外能够像swap(SampleObj1, SampleObj2)那样使用,而不是SampleObj1.swap(SampleObj2)。为此我们可以在std空间里面定义一个全特化的版本(std namespace是不能随便添加东西的,只允许添加类似于swap这样的全特化版本),像这样:

    复制代码
    1 namespace std
    2 {
    3 template<>
    4 void swap<Sample>(Sample &s1, Sample &s2)
    5 {
    6     s1.swap(s2); // 在这里调用类的成员函数
    7 }
    8 }
    复制代码

    重写operator=,像下面这样:

    复制代码
    1 Sample& operator= (const Sample& s)
    2 {
    3     if(this != &s)
    4     {
    5         Sample temp(s);
    6         swap(*this, s); // 顺眼多了,会先去调用特化版本的swap
    7     }
    8     return *this;
    9 }
    复制代码

    这样,就可以在使用namespace std的地方用swap()函数交换两个Sample对象了。

    下面书上的内容就变难了,因为假设Sample现在是一个模板类,Element也是模板类,即:

    复制代码
    1 template <class T>
    2 class Element
    3 {…};
    4 
    5 template <class T>
    6 class Sample
    7 {…};
    复制代码

    那应该怎么做呢?

    在模板下特化std的swap是不合法的(这叫做偏特化,编译器不允许在std里面偏特化),只能将之定义在自定义的空间中,比如:

    复制代码
     1 namespace mysample
     2 {
     3     template <class T>
     4 class Element
     5 {…};
     6 
     7 template <class T>
     8 class Sample
     9 {…};
    10 
    11 template <class T>
    12 void swap(Sample<T> &s1, Sample<T> &s2)
    13 {
    14     s1.swap(s2);
    15 }
    16 }
    复制代码

    总结一下,当是普通类时,可以将swap的特化版本放在std的namespace中,swap指定函数时会优先调用这个特化版本;当是模板类时,只能将swap的偏特化版本放在自定义的namespace中。好了,问题来了,这时候用swap(SampleObj1, SampleObj2)时,调用的是std版本的swap,还是自定义namespace的swap?

    事实上,编译器还是会优先考虑用户定义的特化版本,只有当这个版本不符合调用类型时,才会去调用std的swap。但注意此时:

    复制代码
    1 Sample& operator= (const Sample& s)
    2 {
    3     if(this != &s)
    4     {
    5         Sample temp(s);
    6         swap(*this, s); // 前面的swap不要加std::
    7     }
    8     return *this;
    9 }
    复制代码

    里面的swap不要用std::swap,因为这样做,编译器就会认为你故意不去调用位于samplespace里面的偏特化版本了,而去强制调用std命名空间里的。

    为了防止出这个错,书上还是建议当Sample是普通类时,在std命名空间里定义一个全特化版本。

    这个条款有些难度,我们总结一下:

    1. 在类中提供了一个public swap成员函数,这个函数直接交换指针本身(因为指针本身是int类型的,所以会调用std的普通swap函数),像下面这样:

    1 void Sample::swap(Sample &s)
    2 {
    3     swap(p, s.p); // 也可以写成std::swap(this->p, s.p);
    4 }

    2. 在与Sample在同一个namespace的空间里提供一个non-member swap,并令他调用成员函数里的swap,像下面这样:

    1 template <>
    2 void swap<Sample>(Sample& s1, Sample& s2){s1.swap(s2);} // 如果Sample是普通类,则定义swap位于mysample空间中,同时多定义一个位于std空间中(这个多定义不是必须的,只是防御式编程)

    或者

    1 template <class T>
    2 void swap(Sample<T>& s1, Sample<T>& s2){s1.swap(s2);} // 如果Sample是模板类时,只能定义在mysample空间中

    好了,最后一段终于说到了不抛异常的问题,书上提到的是不要在成员函数的那个swap里抛出异常,因为成员函数的swap往往都是简单私有成员(包括指针)的置换,比如交换两个int值之类,都是交换基本类型的,不需要抛出异常,把抛出异常的任务交给non-member的swap吧。

    最后总结一下:

    1. 当std::swap对你的类型效率不高时,提供一个swap成员函数,这个成员函数不抛出异常,只对内置类型进行操作

    2. 如果提供一个member swap,也该提供一个non-member swap来调用前者,对于普通类,也请特化std::swap

    3. 调用swap时,区分是调用自身命名空间的swap还是std的swap,不能乱加std::符号

    4. 为“用户自定义类型”进行std template全特化是好的,但千万不要尝试在std内加入某些对std而言全新的东西。

  • 相关阅读:
    Flutter form 的表单 input
    FloatingActionButton 实现类似 闲鱼 App 底部导航凸起按钮
    Flutter 中的常见的按钮组件 以及自 定义按钮组件
    Drawer 侧边栏、以及侧边栏内 容布局
    AppBar 自定义顶部导航按钮 图标、颜色 以及 TabBar 定义顶部 Tab 切换 通过TabController 定义TabBar
    清空路由 路由替换 返回到根路由
    应对ubuntu linux图形界面卡住的方法
    [转] 一块赚零花钱
    [转]在树莓派上搭建LAMP服务
    ssh保持连接
  • 原文地址:https://www.cnblogs.com/wuchanming/p/3735410.html
Copyright © 2011-2022 走看看