Effective_C++_条款二十五: 考虑写出一个不抛出异常的swap函数

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

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

#include <iostream>

int main()
{
    int a = 3;
    int b = 4;
    std::swap(a, b);
}

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

template<class T>
void swap(T& a, T& b)
{
    T c = a;
    a = b;
    b = c;
}

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

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

class Element
{
private:
    int a;
    int b;
    vector<double> c;
};

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

class Sample
{
private:
    Element* p;
public:
    ~Sample();
    Sample(const Sample&);
    Sample& operator= (const Sample&);
};

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

Sample& operator= (const Sample& s)
{
    if(this != &s)
    {
        Sample temp(s);
        swap(*this, temp);
    }
    return *this;
}

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

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

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

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

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

void swap(Sample& s)
{
    std::swap(p, s.p);
}
Sample& operator= (const Sample& s)
{
    if(this != &s)
    {
        Sample temp(s);
        this->swap(s);//直接swap里面的指针,而不是整个Sample对象
    }
    return *this;
}

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

namespace std
{
template<>
void swap<Sample>(Sample &s1, Sample &s2)
{
    s1.swap(s2); // 在这里调用类的成员函数
}
}

std namespace是不能随便添加东西的,只允许添加类似于swap这样的全特化版本(注意:这里是直接在std空间中做添加。其实一般不用模板函数的全特化的,直接使用函数重载即可,但是这里涉及到了std空间,不能往里面加任何东西,只能实现一个全特化的版本。模板函数不支持偏特化,用模板偏特化的地方也应该使用函数重载实现:https://blog.csdn.net/songchuwang1868/article/details/91350814

 

重写operator=,像下面这样:

Sample& operator= (const Sample& s)
{
    if(this != &s)
    {
        Sample temp(s);
        swap(*this, s); // 顺眼多了,会先去调用特化版本的swap,而特化版的swap又会去调Sample的成员函数
    }
    return *this;
}

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

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

template <class T>
class Element
{…};

template <class T>
class Sample
{…};

那应该怎么做呢?

在模板下特化std的swap是不合法的(这叫做偏特化(并不是偏特化,而是模板函数重载)编译器不允许在std里面偏特化,实际上不允许任何函数模板进行偏特化,使用模板函数的重载解决),只能将之定义在自定义的空间中(std命名空间中不允许增加函数重载),比如:

namespace mysample
{
    template <class T>
class Element
{…};

template <class T>
class Sample
{…};

template <class T>
void swap(Sample<T> &s1, Sample<T> &s2)
{
    s1.swap(s2);
}
}

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

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

Sample& operator= (const Sample& s)
{
    if(this != &s)
    {
        Sample temp(s);
        swap(*this, s); // 前面的swap不要加std::
    }
    return *this;
}

里面的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而言全新的东西。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值