c++ swap函数_C++中定制化你的swap函数

272e2e3792539883e391fce0640da0ab.gif点蓝色字关注“CurryCoder的程序”

微信公众号:CurryCoder的程序人生
欢迎关注我,一起学习,一起进步!

8266a7b746a15a1abbb7c068805f22cd.png


1.默认swap函数所带来的效率问题

swap函数用于将两个对象的值相互交换,默认情况下,交换动作可由STL提供的swap算法完成。其典型的实现如下所示:
namespace std{
    template<typename T>
    void swap(T& a, T& b){
        T temp(a);
        a = b;
        b = temp;
    }
}
只要类型T支持拷贝(即通过拷贝构造函数和赋值运算符重载完成),默认的swap函数就会帮你交换类型为T的对象。 但对某些类型来说,三个对象之间的拷贝没有必要,因为它会影响程序执行的效率 。 其中,最主要的就是“以指针指向一个对象,内含真正数据”的那种类型。这种设计的常见形式是所谓的pimpl手法。如下例所示:
class WidgetImpl{  // 针对Widget数据而设计的类
public:
    // ...
private:
    int a, b, c;  
    std::vector<double> v; // 可能有很多数据,意味着拷贝时间很久
    // ...
};

class Widget{  // 该类使用pimpl手法
public:
    Widget(const Widget& rhs);
    Widget& operator=(const Widget& rhs){
        // ...
        *pImpl = *(rhs.pImpl);   // 复制Widget时,令其拷贝其WidgetImpl对象
        // ...
    }
    // ...
private:
    WidgetImpl* pImpl;
};
一旦需要交换两个Widget对象值,唯一需要的就是交换其pImpl指针,但默认的swap函数不知道这点。它不仅仅拷贝三个Widget对象,还拷贝了三个WidgetImpl对象。因此,默认swap函数非常缺乏效率。

2.对std::swap进行特化定制

我们希望告诉std::swap,当Widget对象被交换时,真正该交换的是其内部的pImpl指针。因此,我们需要将std::swap进行定制,得到特化版本的swap函数。如下面代码段所示,但下面的形式仍无法通过编译。
// 这是std::swap针对T是Widget的特化版本,目前还无法通过编译。
namespace std{
    template<>    // 表示它是std::swap的一个特化版本
    void swap (Widget& a, Widget& b){  // swap表示这个特化版本是针对T为Widget设计的
        swap(a.pImpl, b.pImpl);  // 交换Widget对象时,只有交换它们的pImpl指针即可
    }
}
通常情况下,我们不可以改变std命名空间中的任何东西,但是可以为标准模板(如swap函数)制定特化版本,让它成为专属于我们自己的类。 上面代码段中的特化版本swap函数是无法通过编译的,这是因为它企图访问a和b内的pImpl指针,而该指针是私有的。 解决方法令Widget声明一个名为swap的公有成员函数,让其完成真正的交换工作,然后将std::swap特化,令它调用该成员函数。改进后的代码如下所示:
class WidgetImpl{  // 针对Widget数据而设计的类
public:
    // ...
private:
    int a, b, c;  
    std::vector<double> v; // 可能有很多数据,意味着拷贝时间很久
    // ...
};

class Widget{  // 该类使用pimpl手法
public:
    Widget(const Widget& rhs);
    Widget& operator=(const Widget& rhs){
        // ...
        *pImpl = *(rhs.pImpl);   // 复制Widget时,令其拷贝其WidgetImpl对象
        // ...
    }
    // 增加公有的swap成员函数
    void swap(Widget& other){
        using std::swap;  // 这个声明很重要!
        swap(pImpl, other.pImpl);
    }
    // ...
private:
    WidgetImpl* pImpl;
};

// 这是std::swap针对T是Widget的特化版本,通过编译。
namespace std{
    template<>    // 表示它是std::swap的一个特化版本
    void swap (Widget& a, Widget& b){  // swap表示这个特化版本是针对T为Widget设计的
       a.swap(b);  // 交换Widget对象时,只要调用swap成员函数即可
    }
}
3.新的问题 但是,假设Widget和WidgetImpl都是类模板而不是普通的类呢?两个类模板如下所示:
template<typename T>
class WidgetImpl{
    // ...
};

template<typename T>
class Widget{
    // ...
};
照葫芦画瓢,在Widget类模板中放入一个公有成员函数swap。不幸的是,仍然像2中的做法那样就不行啦,此时在特化std::swap时会出现乱流。
namespace std{
    template<typename T>
    void swap>(Widget& a, Widget& b){  // 错误!
        a.swap(b);
    }
}
出现乱流的原因在于我们企图部分特化一个函数模板,但C++只允许对类模板进行部分特化,在函数模板上部分特化是不行的解决方法 :当你打算对一个函数模板进行部分特化时,需要为它添加一个重载版本。
namespace std{
    template<typename T>
    void swap(Widget& a, Widget& b){  // std::swap的一个重载版本
        a.swap(b);
    }
}
一般来说,重载函数模板是没有问题的。但是,std是一个特别的命名空间。客户可以全部特化std中的模板,但是不可以添加新的类或函数到std中。 解决方法声明一个non-member函数swap,让它来调用成员函数swap。但是,不再将那个non-member函数swap声明为std::swap的特化版本或重载版本。如下所示:
namespace WidgetStuff{
    // 类模板WidgetImpl
    template<typename T>
    class WidgetImpl{
        // ...
    };

    // 类模板Widget
    template<typename T>
    class Widget{
        // ...
        // 内含swap成员函数
    };
    // ...

    template<typename T>
    void swap(Widget& a, Widget& b){   // non-member函数swap,不属于std命名空间
        a.swap(b);
    }
}
上面的做法对类和类模板都行得通,似乎我们任何时候都可以使用此方法。不幸的是,如果你想让你的类专属版swap函数在尽可能多的地方被调用,你需要同时在该类所在的命名空间内写一个non-member版本的swap函数和一个std::swap特化版本。 4.究竟调用哪个swap函数呢? 前面三部分的内容,我们都是从程序编写者的角度去看问题。下面,我们从程序调用者即客户的角度出发,假设你正在写一个函数模板,它的内部需要交换两个对象值。如下所示:
template<typename T>
void doSomething(T& obj1, T& obj2){
    // ...
    swap(obj1, obj2);
    // ...
}
上面程序段中应该调用哪个swap呢?是默认的swap?还是特化版的std::swap?还是一个可能存在的T专属版本但却放置在某个命名空间内?其实,你的本意是调用T专属版本,并在该版本不存在的情况下调用std::swap。因此,你可以写成如下的代码:
template<typename T>
void doSomething(T& obj1, T& obj2){
    using std::swap;  // std::swap在此函数内可用
    // ...
    swap(obj1, obj2);  // T专属版本
    // ...
}
其中, 需要注意的一点是别在调用的时候额外添加修饰符,因为那会影响C++选择适当的函数 。如下面的错误调用方式,只会强迫编译器只认识std命名空间中的swap函数,因此不会再调用一个定义在其他命名空间中的T专属版本swap函数。
std::swap(obj1, obj2);  // 错误的调用swap方式
最后注意的地方成员函数版本的swap函数绝不可抛出异常。这是因为默认的swap函数是以拷贝构造函数和赋值运算符重载为基础实现的,而在一般情况下两者都允许抛出异常。因此,当你写了一个定制化版本的swap函数,往往提供的不只是高效交换对象值的方法,而且也并不抛出异常。一般而言,这两个swap特性是连在一起的,因为高效率的swap几乎总是基于对内置类型的操作(如pimpl手法的底层指针),而内置类型上的操作绝不会抛出异常。 5.总结 (1) 当std::swap对你的类型效率不高时,提供一个swap成员函数,并确保这个函数不抛出异常。 (2) 如果你提供一个成员函数版本的swap,也应该提供一个non-member版本的swap用来调用前者。对于类而言,也请使用特化版本的std::swap。 (3) 调用swap时应针对std::swap使用using进行声明,然后调用swap并且不带任何命名空间修饰。 (4) 为自定义的类而全部特化std模板是没问题的,但千万不要往std中添加任何东西。

觉得好看,请点这里↓

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值