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