Effective C++条款25:设计与声明——考虑写出一个不抛异常的swap函数

一、标准模板库中的swap函数

  • 标准模板库中的swap函数,其是STL中的一部分,后来成为异常安全性编程(见条款29)以及用来处理自我赋值可能性(见条款11)的一个常见机制
  • 下面是STL中swap的源码:只要类型T支持拷贝(拷贝构造函数或者拷贝赋值运算符),缺省的swap函数就可能帮你对两个类型为T的对象进行兑换

  • 为什么不使用std::swap()函数:
    • 从源码可以看出,std::swap函数,其涉及三个对象的复制操作:a复制到temp,b复制到a,temp复制到b
    • 但是对于我们自己设计的类来说,可能有时用不到这些复制操作,复制太多,效率太低
  • 因此对于自己设计的类(非模板类,或模板类),都不想使用std提供的默认swap()版本。本文下面将会介绍针对于自己设计的“非模板类,或模板类”设计自己的swap()函数

二、针对于非模板类,设计全特化的std::swap()

①有些情况下我们不希望使用std::swap()

  • 例如我们有下面的一个类WidgetImpl,其中保存Widget的数据:
//针对Widget数据设计的class
class WidgetImpl
{
public:
    //...
private:
    int a, b, c;
    std::vector<double> v;
    //...其他数据
};
  • 此时又有一个类Widget,其采用pimpl手法(参阅条款31),里面保存一个指针指向于一个对象(此处为WidgetImpl),该对象内含真正的数据:
//这个class使用pimpl手法
class Widget
{
public:
    Widget(const Widget& rhs);
    Widget& operator=(const Widget& rhs)//拷贝赋值运算符
    {
        //...
        *pImpl = *(rhs.pImpl);
        //...
    }
private:
    WidgetImpl* pImpl; //此处是指针
};

为什么我们不希望使用std::swap():

  • 我们使用时真正用到的是Widget对象,如果我们使用std::swap函数,那么其会进行3次Widget对象的复制,并且每次复制的时候调用的是上面Widget类中的operator=运算符
  • 因此std::swap函数会调用三次operator=函数,这种效率是非常低的
  • 可以看出Widget中只用一个pImpl指针保存数据,因此我们想要交换两个Widget对象,只要交换它们两个的pImpl指针就行了,这样就达到数据互换的目的了(实现见下面的②)

②使用全特化的std::swap()函数

  • 我们希望让std::swap()知道,当Widget类进行替换的时候只需要替换其内部的pImpl指针就可以了,此时我们可以针对std::swap()做一个全特化的版本
  • 代码如下:
//此swap用于表示这个函数针对“T是Widget”而设计的
namespace std {
    //template<>用于表示这是一个std::Swap的全特化版本
    template<>
    void swap<Widget>(Widget& a, Widget& b)
    {
        //错误的,pImpl是private的,无法编译通过
        swap(a.pImpl, b.pImpl);
    }
}
  • 但是上面的代码是无法编译通过的,因为pImpl是private的,因此函数无法编译通过

③为类设计一个swap成员函数,并设计一个全局swap函数

  • 通过②的设计我们知道,由于pImpl是private的,因此全特化的swap无法编译通过。我们有两种解决办法:
    • 1.将上面的特化版本定义为class的friend,但是我们不希望这么做
    • 2.(此处要介绍的)为class设计一个swap()函数,在全特化的版本中调用我们class的swap()成员函数
  • 此时我们可以修改Widget class,使全特化的版本可以应用于我们的Widgte class:
class Widget
{
public:
    void swap(Widget& rhs)
    {
        using std::swap; //为什么要有这个声明,见下
        swap(pImpl, rhs.pImpl); //调用std::swap()函数,只交换两个对象的指针
    }
private:
    WidgetImpl* pImpl;
};


namespace std {
template<>
    void swap<Widget>(Widget& a, Widget& b)
    {
        a.swap(b); //调用Widget::swap()成员函数
    }
}
  • 现在的代码可以编译通过了,并且可以使用我们的std::swap()全特化版本了
  • STL容器中的swap()成员函数就是按照这种原理实现的:为class提供public swap()成员函数和std::swap()的全特化版本(然后以后者调用前者)
  • 顺便提一下:上面我们是在std中偏特化了std::swap()函数,但是我们不建议这样做,因为这样可能会对std命名空间造成污染,一种解决解决方法是在std命名空间之外定义全特化swap()函数(下面的“三”中有演示案例)

三、如果是模板类,那么该如何解决?

  • 紧接着“二”,如果Widget和WidgetImpl都不是普通的类,而是模板类。如下所示:
//此时这两个类都变成模板类

template<typename T>
class WidgetImpl { };

template<typename T>
class Widget { };

偏特化是错误的

  • 对于“二”中的解法,我们可能会设计下面的代码(但是是错误的,无法编译通过):
    • 我们的Widget和WidgetImpl都是类模板,因此我们尝试偏特化std::swap()函数
    • 但是由于C++只允许对类模板偏特化,不允许对函数模板偏特化,所以下面的代码是无法编译通过的
template<typename T>
class WidgetImpl{ //同上 };

template<typename T>
class Widget{ //同上 };

namespace std {
    //此处是错误的,C++只允许对类模板偏特化,不允许对函数模板偏特化
    template<typename T>
    void swap<Widget<T>>(Widget<T>& a, Widget<T>& b)
    {
        a.swap(b);
    }
}

 

使用重载版本代替偏特化

  • 上面我们尝试偏特化函数模板,但是C++编译器不允许。因此为了解决这种问题,我们可以为其添加一个重载版本
  • 代码如下:
template<typename T>
class WidgetImpl{ //同上 };

template<typename T>
class Widget{ //同上 };

namespace std{
    //这里std::swap的一个重载版本,而不是特化版本
    template<typename T>
    void swap(Widget<T>& a, Widget<T>& b)
    {
        a.swap(b);
    }
}

 

在自己的命名空间中定义swap()函数

  • 上面我们重载了std::swap函数,但是是在std命名空间中进行重载的,std是个特殊的命名空间,其管理规则比较特殊,因此我们不建议在std中重载任何内容
  • 一种解决方法就是在自己的命名空间中定义swap()函数。代码如下:
//假设这是自己设计的命名空间
namespace WidgetStuff{
template<typename T>
class WidgetImpl{ //同上 };
template<typename T>
class Widget{ //同上 };

template<typename T>
    void swap(Widget<T>& a, Widget<T>& b)
    {
        a.swap(b);
    }
}
  • 注意事项:
    • 我们在自己的命名空间中定义的swap()函数不属于std::swap()的重载版本,因为它们作用域不一致
    • 此处我们以命名空间为例,其实不使用命名空间也可以,我们主要是为了指出不要与std::swap()产生冲突。但是为了让全局数据空间太过杂乱,我们建议使用命名空间
    • 在“二”中最后我们提到过,不要在std中进行全特化。因此此处的方法不仅适用于模板类,同样也适用于非模板类

四、使用using声明

  • 假设此时我们编写了一个函数模板,其接受两个参数,并在模板内调换两个元素。代码如下:
template<typename T>
void doSomething(T& obj1,T& obj2)
{
    //...
    swap(obj1,obj2); //调用哪一个版本的swap()函数哪?
    //...
}
  • 根据C++的名称查找规则,对于swap()的调用会根据以下顺序查找:
    • 先在doSomething()函数所在的命名空间中,查找是否有针对于类型T所特化的swap()函数。如果有就调用,如果没有进行下一步
    • 调用std::swap()函数调换两个元素
  • 通过上面我们知道:
    • 在调用swap()时,先在自己的命名空间中查找是否有适当的特化版本的swap()函数,如果有就调用,如果没有就调用std::swap()
    • 因此,为了使std::swap()函数在函数内可用,我们通常使用using声明,导入std::swap()函数。让其在没有特化版本的swap()时去调用std::swap()
template<typename T>
void doSomething(T& obj1,T& obj2)
{
    //使std::swap在次函数内可用
    //如果没有针对于T的特化swap版本,那么就调用std::swap
    using std::swap;

    //...
    swap(obj1,obj2);
    //...
}
  • 一个注意事项:
    • 不要在调用swap()函数的时候指定std::限定符,否则将永远无法调用自己的特化版本
    • 例如下面的代码:强制调用std::swap(),那么自己定义的特化swap()将永远不会被调用
template<typename T>
void doSomething(T& obj1,T& obj2)
{
    //...
    std::swap(obj1,obj2); //错误的swap调用方式
    //...
}

五、swap()函数的总结

1)如果std::swap()函数对你的类或类模板使用,并且不会影响效率,那么就优先使用std::swap()

2)如果std::swap()针对于你的类或类模板不太使用,那么就自己设计swap()函数。设计如下:

  • 在类中提供一个public swap()函数,在其中置换两者的数据(代码自己设计,但是不允许抛出异常)
  • 然后在你的类或类模板所在的命名空间中提供一个非成员函数swap(),然后在其中调用成员函数swap()
  • 如果你的类(而不是类模板),那么可以为你的类特化std::swap(),并在特化版本的swap()中调用成员函数swap()

六、swap成员函数不能抛出异常

  • 成员版的swap函数绝不可能抛出异常。因为swap的一个最好的应用是帮助类(和类模板)提供强烈的异常安全性保障(条款29介绍)
  • 但此技术基于一个假设:成员版的swap绝不抛出异常。这一约束只施加于swap成员版本,不可施加于非成员版本,因为swap缺省版本是以拷贝构造函数和拷贝赋值运算符为基础,而一般情况下两者都允许抛出异常
  • 因此当你写下一个自定义的swap,往往提供的不只是高效置换对象的版本,而且还不抛出异常

七、本文总结

  • 当std::swap对你的类型效率不高时,提供一个swap成员函数,并确定这个函数不抛出异常
  • 如果你提供一个member swap,也该提供一个non-member swap用来调用前者。对于class(而非template class),也请特化std::swap
  • 调用swap时应针对std::swap使用using声明式,然后调用swap并且不带任何“命名空间资格修饰”
  • 用“用户定义类型”进行std template全特化时最好的,但千万不要尝试在std内加入某些对std而言全新的东西
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值