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中添加任何东西。

相关文章
|
30天前
|
程序员 C++ 容器
在 C++中,realloc 函数返回 NULL 时,需要手动释放原来的内存吗?
在 C++ 中,当 realloc 函数返回 NULL 时,表示内存重新分配失败,但原内存块仍然有效,因此需要手动释放原来的内存,以避免内存泄漏。
|
1月前
|
存储 前端开发 C++
C++ 多线程之带返回值的线程处理函数
这篇文章介绍了在C++中使用`async`函数、`packaged_task`和`promise`三种方法来创建带返回值的线程处理函数。
46 6
|
1月前
|
C++
C++ 多线程之线程管理函数
这篇文章介绍了C++中多线程编程的几个关键函数,包括获取线程ID的`get_id()`,延时函数`sleep_for()`,线程让步函数`yield()`,以及阻塞线程直到指定时间的`sleep_until()`。
25 0
C++ 多线程之线程管理函数
|
1月前
|
编译器 C语言 C++
C++入门3——类与对象2-2(类的6个默认成员函数)
C++入门3——类与对象2-2(类的6个默认成员函数)
23 3
|
1月前
|
编译器 C语言 C++
详解C/C++动态内存函数(malloc、free、calloc、realloc)
详解C/C++动态内存函数(malloc、free、calloc、realloc)
171 1
|
1月前
|
存储 编译器 C++
C++入门3——类与对象2-1(类的6个默认成员函数)
C++入门3——类与对象2-1(类的6个默认成员函数)
31 1
|
1月前
|
编译器 C语言 C++
C++入门6——模板(泛型编程、函数模板、类模板)
C++入门6——模板(泛型编程、函数模板、类模板)
42 0
C++入门6——模板(泛型编程、函数模板、类模板)
|
2月前
|
编译器 C++
【C++核心】函数的应用和提高详解
这篇文章详细讲解了C++函数的定义、调用、值传递、常见样式、声明、分文件编写以及函数提高的内容,包括函数默认参数、占位参数、重载等高级用法。
23 3
|
3月前
|
编译器 C++ 容器
【C++】String常见函数用法
【C++】String常见函数用法
|
3月前
|
C++
c++常见函数及技巧
C++编程中的一些常见函数和技巧,包括生成随机数的方法、制表技巧、获取数字的个位、十位、百位数的方法、字符串命名技巧、避免代码修改错误的技巧、暂停和等待用户信号的技巧、清屏命令、以及避免编译错误和逻辑错误的建议。
35 6