C++中定制化你的swap函数

简介: C++中定制化你的swap函数

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> (Widget& a, Widget& b){  // swap<Widget>表示这个特化版本是针对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> (Widget& a, Widget& b){  // swap<Widget>表示这个特化版本是针对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<T>>(Widget<T>& a, Widget<T>& b){  // 错误!
        a.swap(b);
    }
}


出现乱流的原因在于我们企图部分特化一个函数模板,但C++只允许对类模板进行部分特化,在函数模板上部分特化是不行的解决方法:当你打算对一个函数模板进行部分特化时,需要为它添加一个重载版本。


namespace std{
    template<typename T>
    void swap(Widget<T>& a, Widget<T>& 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<T>& a, Widget<T>& 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中添加任何东西。

相关文章
|
1月前
|
C++
C++ 数学函数、头文件及布尔类型详解
C++ 支持数学操作,如`max`和`min`函数找最大值和最小值,以及`&lt;cmath&gt;`库中的`sqrt`、`round`等数学函数。`bool`类型用于布尔逻辑,取值`true`(1)或`false`(0)。布尔表达式结合比较运算符常用于条件判断,例如在`if`语句中检查年龄是否达到投票年龄。在代码示例中,`isCodingFun`和`isFishTasty`变量分别输出1和0。
123 1
|
1月前
|
算法 C++ 容器
C++中模板函数以及类模板的示例(template)
C++中模板函数以及类模板的示例(template)
|
2月前
|
存储 设计模式 安全
【C++ 软件设计思路】多角度探索C++事件处理:以‘handlePowerEvent’函数为例
【C++ 软件设计思路】多角度探索C++事件处理:以‘handlePowerEvent’函数为例
85 2
|
5天前
|
存储 C++
c/c++宏定义(函数)
c/c++宏定义(函数)
|
7天前
|
编译器 C++
【C++进阶】引用 & 函数提高
【C++进阶】引用 & 函数提高
|
11天前
|
C++
C++从入门到精通:2.1.2函数和类——深入学习面向对象的编程基础
C++从入门到精通:2.1.2函数和类——深入学习面向对象的编程基础
|
11天前
|
存储 C++
C++从入门到精通:2.1.1函数和类
C++从入门到精通:2.1.1函数和类
|
19天前
|
机器学习/深度学习 定位技术 C++
c++中常用库函数
c++中常用库函数
39 0
|
20天前
|
算法 搜索推荐 C++
浅谈sort函数底层(一道c++面试的天坑题)
浅谈sort函数底层(一道c++面试的天坑题)
|
23天前
|
编译器 C++
C++ 解引用与函数基础:内存地址、调用方法及声明
C++ 中的解引用允许通过指针访问变量值。使用 `*` 运算符可解引用指针并修改原始变量。注意确保指针有效且不为空,以防止程序崩溃。函数是封装代码的单元,用于执行特定任务。理解函数的声明、定义、参数和返回值是关键。函数重载允许同一名称但不同参数列表的函数存在。关注公众号 `Let us Coding` 获取更多内容。
136 1