swap是个有趣的函数。原本它只是STL的一部分,而后成为异常安全性编程(exception-safe programing,见条款29)的脊柱,以及用来处理自我赋值可能性(见条款11)的一个常见机制。由于swap如此有用,适当的实现很重要。
所谓swap(置换)两对象值,意思是将两对象的值彼此赋予对方。缺省情况下swap动作可由标准程序库提供的swap算法完成。其典型实现:
namespace std {
template<typename T> // std::swapd的典型实现;
void swap(T& a, T& b) // 置换a和b的值
{
T temp(a);
a = b;
b = temp(b);
}
}
只要类型T支持copying(通过copy构造函数和copy assignment操作符完成),缺省的swap实现代码就会帮你置换类型为T的对象,你不需要为此另外再做任何工作。
这个缺省的swap实现涉及三个对象复制:a复制到temp,b复制到a,以及temp复制到b。但是对某些类型而言,这些复制动作无一必要;对它们而言swap缺省行为等于是把高速铁路铺设在慢速小巷弄内。
其中最主要的就是“以指针指向一个对象,内含真正数据”的那种类型。这种设计常见表现形式是所谓“pimpl手法”(pimpl是“pointer to implementation”的缩写,见条款31)。如果以这种手法设计Widget class,看起来会像这样:
class WidgetImpl { // 针对Widget数据而设计的class
public:
... // 细节不重要
private:
int a, b, c; // 可能有许多数据
std::vector<double> v; // 意味复制时间很长
...
};
class Widget { // 这个class使用pimpl手法
public:
Widget(const Widget& rhs);
Widget& operator=(const Widget& rhs) // 复制Widget时,令它复制其WidgetImpl对象。
{
... // 关于operator=的一般性实现
*pImpl = *(rhs.pImpl); // 细节,见条款10,11和12
...
}
...
private:
WidgetImpl* pImpl; // 指针,所指对象内含Widget数据
};
一旦要置换两个Widget对象值,我们唯一需要做的就是置换其pImpl指针,但缺省的swap算法不知道这一点。它不只复制是三个Widgets,还复制三个WidgetImpl对象。非常缺乏效率!
我们希望能够告诉std::swap:当Widgets被置换时真正该做的是置换其内部的pImpl指针。确切实践这个思路的一个做法是:将std::swap针对Widget特化。下面是基本构想,但目前这个形式无法通过编译:
namespace std {
template<> // 这是std::swapd针对“T是Widget”的特化版本;
void swap<Widget>(Widget& a, Widget& b) // 目前无法通过编译
{
swap(a.pImpl, b.pImpl); // 置换Widgets时只置换它们的pImpl指针就好
}
}
这个函数一开始的“template<>”表示它是std::swap的一个全特化(total template specialization)版本,函数名称之后的“<Widget>”表示这一特化版本针对“T是Widget”而设计。换句话说当一般性的swap template施行于Widgets身上便会启用这个版本。通常我们不被允许改变std命名空间内的任何东西,但可以为标准templates(如swap)制造特化版本,使它专属于我们自己的classes(例如Widget)。以上作为正是如此。
但是一如稍早我说,这个函数无法通过编译。因为它企图访问a和b内的pImpl指针,而那却是private。我们可以将这个特化版本声明为friend,但和以往的规矩不太一样:我们令Widget声明为swap的public成员函数做真正的置换工作,然后将std::swap特化,令它调用该成员函数:
class Widget { // 与前相同,唯一差别是增加swap函数
public:
...
void swap(Widget& other)
{
using std::swap; // 这个声明之所以必要,稍后解释。
swap(pImpl, other.pImpl); // 若要置换Widgets就置换其pImpl指针。
}
...
};
namespace std {
template<> // 修订后的std::swap特化版本
void swap<Widget>(Widget& a, Widget& b)
{
swap(a.pImpl, b.pImpl); // 若要置换Widgets,调用其swap成员函数
}
}
这种做法不只能通过编译,还与STL容器有一致性,因为所有STL容器也都提供有public swap成员函数和std::swap特化版本(用以调用前者)。
然而假设Widget和WidgetImpl都是class templates而非classes,也许我们可以试试将WidgetImpl内的数据类型加以参数化:
template<typename T>
class WidgetImpl { ... };
template<typename T>
class Widget { ... };
在Widget内(以及WidgetImpl内,如果需要的话)放个swap成员函数就像以往一样简单,但我们却在特化std::swap时遇上乱流。我们想写成这样:
namespace std {
template<typename T>
void swap< Widget<T> >(Widget<T>& a, Widget<T>& b) { // 错误!不合法!
a.swap(b);
}
}
看起来合情合理,却不合法。我们企图偏特化(partially specialize)一个function template(std::swap),但C++只允许对class templates偏特化,在function templates身上偏特化是行不通的。
namespace std {
template<typename T> // std::swap的一个重载版本
void swap(Widget<T>& a, Widget<T>& b) { // 注意“swap”之后没有“<...>”, 但即使这样也不合法。
a.swap(b);
}
}
一般而言,重载function templates没有问题,但std是个特殊的命名空间,其管理规则也比较特殊。客户可用全特化std内的templates,但不可以添加新的templates(或classes或functions或其它任何东西)到std里头。
那该如何是好?毕竟我们总是需要一个办法让其他人调用swap时能够取得我们提供的较高效的template特定版本。答案很简单,我们还是声明一个non-member swap让它调用member swap,但不再将那个non-member声明为std::swap的特化版本或重载版本。假设Widget的所有相关机能都被置于命名空间WidgetStuff内,整个结果看起来便像这样:
namespace WidgetStuff {
... // 模板化的WidgetImpl等等。
template<typename T> // 同前,内含swap成员函数。
class WidgetImpl { ... };
...
template<typename T> // non-member swap函数;
void swap(Widget<T>& a, Widget<T>& b) { // 这里并不属于std命名空间。
a.swap(b);
}
}
这个做法对classes和class templates都行得通,所以似乎我们应该任何时候都使用它。
此刻,我们已经讨论过default swap、member swaps、non-member swaps、std::swap特化版本、以及对swap的调用,现在让我把整个形势做个总结。
首先,如果swap的缺省实现码对你的class或class template提供可接受的效率,你不需要额外做任何事。任何尝试置换(swap)那种对象的人都会取得缺省版本,而那将有良好的运作。
其次,如果swap缺省实现版的效率不足(那几乎总是意味你的class或template使用了某种pimpl手法),试着做以下事情:
1. 提供一个public swap成员函数,让它高效地置换你的类型的两个对象值。稍后我将解释,这个函数绝不该抛出异常。
2. 在你的class或template所在的命名空间内提供一个non-member swap,并令它调用上述swap成员函数。
3. 如果你正编写一个class(而非class template),为你的class 特化std::swap。并令它调用你的swap成员函数。
最后,如果你调用swap,请确定包含一个using声明式,以便让std::swap在你的函数内曝光可见,然后不加任何namespace,赤裸裸地调用swap。
唯一还未明确的是我的劝告:成员版swap绝不可能抛出异常。那是因为swap的一个最好的应用是帮助classes(和class templates)提供强烈的异常安全性(exception-safety)保障。条款29对此主题提供了所有细节,但此技术基于一个假设:成员版的swap绝不抛出异常。这一约束只施行于成员版!因为swap缺省版本是以copy构造函数和copy assignment操作符为基础,而一般情况下两者都允许抛出异常。因此当你写下一个自定版本的swap,往往提供的不只是高效置换对象值的办法,而且不抛出异常。一般而言这两个swap特性是连在一起的,因为高效率的swaps几乎总是基于对内置类型的操作(例如pimpl手法的底层指针),而内置类型上的操作绝不会抛出异常。
请记住
- 当std::swap对你的类型效率不高时,提供一个swap成员函数,并确定这个函数不抛出异常。
- 如果你提供一个member swap,也该提供一个non-member swap用来调用前者。对于classes(而非templates),也请特化std::swap。
- 调用swap时应针对std::swap使用using声明式,然后调用swap并且不带任何“命名空间资格修饰”。
- 为“用户定义类型”进行std templates全特化是好的,但千万不要尝试在std内假如某些对std而言全新的东西。
个人心得:
从该条款中可以看到,swap函数调用时,我们传递的对象进行交换时是采用深复制的手段,为了提高效率,改成用浅复制的方式,即交换指针对象。
如果对象非常大,采用深复制就很慢,要重新申请内存,然后赋值等一系列操作,如果只复制指针对象就快得多,因为跟对象本身大小没什么关系,不过浅复制容易出现悬垂指针的问题。所以实际项目开发过程中需要视情况而定。