swap函数几乎是所有初学者都写过的一个最基本的函数之一,通常是用它来了解函数概念、形参与实参、引用和指针。然而,这个极为基础的函数却有着非常重要的作用。正因为它的重要性、普遍性,意味着swap函数必须有着极高的安全性,因此如何写一个不抛出异常的swap函数是本章讨论的主题。
普通的Swap函数
让我们先看看一个例子:
void swap(int a, int b)
{
int swap = a;
a = b;
b = swap;
}
这是最常见的一种错误,a
和b
是一个int
类型的值,它们只是实参的一个拷贝,作用域只在swap函数内,所以无法起到交换的作用。
这是一个最普遍的swap函数实现:
void swap(int& a, int& b)
{
int swap = a;
a = b;
b = swap;
}
通过使用引用来保证swap函数中对参数做出的改变传递到swap的作用域外。
还有一个取巧的办法:
void swap(int& a, int& b)
{
a = a + b; // a + b
b = a - b; // a + b - b = a
a = a - b; // a + b - a = b
}
这个函数通过对a
和b
的一系列运算从而避免了申请新的变量作为中间容器,节省了空间。然而这种做法实在是得不偿失,因为它的毛病实在太多了:
-
只适用于内置类型 由于使用了加减运算,这中做法对于类类型的变量来说并不能使用(除非类重载了
+
、-
运算符)。 - 节省空间可以说是毫无卵用 因为这种方式一般只使用于内置类型,而现在的计算机大多都是64位,内置类型最多占用8个字节,这点内存对于现在的机器来说不算什么,通过3个加减运算来替换它,得不偿失。
-
可能出现数据溢出或损失精度现象 举个栗子:
int
类型变量的范围是-2^31~2^31-1
,当参数为int类型时,如果a
和b
都为2^31-1时,第一步a = a + b
就会造成数据溢出。如果参数的类型为float
或double
时,对其进行基本运算会造成精度的损失。
上述swap函数的另一个版本:
void swap(int* a, int* b)
{
*a = *a + *b;
*b = *a - *b;
*a = *a - *b;
}
这是一种更恶劣的做法,这个函数除了上述3个缺点之外,还有一个缺点:当指针a
和b
指向同一个地址,也就是使用了别名的时候,产生的结果就是*a
为原来的两倍,而*b
等于0。
这是一个通过位运算而且不需要额外的存储空间来实现swap函数的方法:
void swap(int a, int b)
{
a = a ^ b; // a ^ b
b = a ^ b; // a ^ b ^ b = a
a = a ^ b; // a ^ b ^ a = b
}
类和容器的swap函数
标准程序库中提供了缺省的swap函数,其实现与我们所预料的大致相同:
namespace std {
template <class T>
void swap ( T& a, T& b )
{
T c(a);
a = b;
b = c;
}
}
只要T拥有copy构造函数,这个缺省的swap函数就能工作。然而它只能够助你淌过小溪,但是要跨过太平洋,它就有点不够看了。如果对象中含有大量数据,这种方法就要进行三次长时间的赋值运算,效率极其低下,尤其是当置换的类是以pimpl
手法实现的时候。
注:pimpl手法(pointer to implementation)是一种将对象实现的细节通过一个私有指针成员进行隐藏的常用手法,可以使接口与实现分离,解除它们之间的耦合关系,来降低文件间的编译依赖性。
一个pimpl手法的栗子:
class BoxImpl
{
public: // 细节不重要
...
private:
string name; // 包含大量数据
string id;
std::vector<int> vec;
...
};
class Box
{
public:
Box(const Box& x); // copy构造函数
Box& operator=(const Box& x)
{
...
*pImpl = *(x.pImpl); // 重载'='操作符,复制Box的实现
...
}
...
private:
BoxImpl* pImpl; // 指向Box的实现
};
正如我们所想的,一旦我们想要置换两个Box
对象,除了复制三个Box
外,还要复制三个BoxImpl
,所以我们需要另一种方法来实现swap函数。现在有一个好主意,如果我们生成一个特化版本的std::swap
函数只调换pImpl
指针(我们不能改变std命名空间里的任何东西,但是我们可以为标准模板添加特化版本),那么我们在调用swap函数的时候会对Box
对象使用我们定制的方法,而对其他对象使用缺省的方法:
namespace std {
template<>
void swap<Box>(Box& a, Box& b)
{
swap(a.pImpl, b.pImpl);
}
}
然而尴尬的是这个函数并不能编译,因为pImpl
指针是Box
的私有成员,所以我们需要在Box
类中添加一个public
的接口来隐藏对私有成员的调用:
class Box {
public:
...
void swap(Box& x)
{
using std::swap; // 稍后解释为什么这么做
swap(pImpl, x.pImpl); // 置换pImpl指针
}
...
};
namespace std {
template<>
void swap<Box>(Box& a, Box& b)
{
a.swap(b);
}
}
这种做法与STL容器对swap的实现有着一致性,STL容器通过std::swap
的特化版本来调用容器的public swap
成员函数,而这个成员函数只是置换数据的指针。
我们完成了目标,但是又发现如果我们要置换的不是class
而是class templates
,那么我们swap函数又不够看了。读者可能觉得很烦,我们仅仅只是实现一个小小的swap函数而已,为什么要兼容这么多东西啊!!!你变了,你不再是从前那个纯洁质朴的swap函数了( TДT)然而为了效率这样做还是值得的。。。
所以我们又对std::swap
偏特化:
namespace std {
template<typename T>
void swap< Box<T> >(Box<T>& a, Box<T>& b)
{
a.swap(b); // 错误!这样做并不能通过编译。
}
}
这样做看起来很合理,但是C++只允许对类模板进行偏特化,而不支持对函数模板的偏特化操作Orz。于是聪明的你又想出了一个好主意,重载swap函数:
namespace std {
template<typename T>
void swap(Box<T>& a, Box<T>& b)
{
a.swap(b); // 依然错误!
}
}
这里出现的问题我之前已经提到过来,我们可以向std命名空间中添加特化的std模板,而不能添加新的模板。
注:如果我们这也做程序可能还是会通过编译并运行,但是它们的行为并没有明确的定义,所以请不要在std中添加任何新东西。
我们所想到的两个实现方法都被否决了,不过天无绝人之路,我们可以声明一个非成员swap函数来调用类模板的swap成员函数,但并不将它声明为std::swap函数的特化或重载版本。我们将所有与Box
相关的机能都置于命名空间BoxTool
中(我们也可以置于global
命名空间中,但是大量的类、模板、方法等充斥在你的global
命名空间中会让你的代码很不优雅):
namespace BoxTool {
... // 模板化的BoxImpl和Box等的实现
template<typename T>
void swap(Box<T>& a, Box<T>& b)
{
a.swap(b); // 该函数没有在std命名空间中
} // 不用担心std::swap的特化或重载问题
}
于是我们可以放心地置换Box对象了。但这时你会发现这种方法好像对类也样适用,所以一直用这种方法岂不是更方便?然而并不是这样。如果你想让你的对某个类特化的swap函数在尽可能多的语境下调用,就需要在该类所在的命名空间中添加一个非成员的swap函数以及一个std::swap函数。
好了,终于写好了swap函数,我们可以来使用它了\(>0<)/:
template<typename T>
void doSomething(T& a, T& b)
{
...
swap(a, b);
...
}
然而swap会调用哪一个版本呢?std中的一般化版本?特化的std::swap版本?类所在命名空间中的那个swap版本?懵逼了(◎_◎;)我们所希望的是调通T的专属版本,如果不存在则调用一般的std::swap函数:
template<typename T>
void doSomething(T& a, T& b)
{
using std::swap; // 令std::swap在函数中可用
...
swap(a, b); // 不能这样:std::swap(a, b);
...
}
通过C++的名称查找规则,确保我们所使用的swap函数是最合适的那个。不过编译器还是会比较喜欢自家人,所以偏向于使用std中的特化版本而不是T所在的命名空间中的模板函数。
我们讨论了这么多swap函数的实现,为自己找到一个合适的swap函数吧(●'◡'●)ノ♥
本文参考自《Effective C++》第25条