最近开始看《Effective C++》,为了方便以后回顾,特意做了笔记。若本人对书中的知识点理解有误的话,望请指正!!!
swap函数是一个非常经典又有用的函数,除了它本身用来交换两个对象数值的功能,还可以用来实现异常安全的赋值,避免自赋值(见条款11)等等用途。在std标准库里,swap函数就是这样实现的:
namespace std{
template<typename T>
void swap(T& a, T& b)
{
T temp(a);
a = b;
b = temp;
}
}
只要类型T支持拷贝函数(通过拷贝构造函数和拷贝赋值运算符完成),默认的swap实现代码就会帮你置换类型为T的对象,你无需再做任何工作。
这个默认的swap实现版本十分普通。它涉及了三个对象的复制:a 复制到 temp,b 复制到 a,以及 temp 复制到 b。如果类型T的大小很大,那么要消耗的内存也很大。
要解决这样的问题,有一个常用的方法叫pimpl(“pointer to implementation”)。它的概念是要把类的实现细节从中移除,放在另一个类中,并通过一个指针进行访问。使用pimpl设计手法的类大概长这样:
//这个类包含Widget类的数据
class WidgetImpl{
public:
...
private:
int a,b,c; //可能有许多数据,意味复制时间长
std::vector<double> v; //高成本拷贝警告!
};
//这个类使用pimpl手法
class Widget{
public:
Widget(const Widget& rhs);
//复制Widget时,令它复制WidgetImpl对象
Widget& operator=(const Widget& rhs)//赋值运算符的实现见条款10,11,12
{
...
*pImpl = *(rhs.pImpl);
...
}
...
private:
WidgetImpl* pImpl; //使用pimpl指针来指向Widget数据
};
一旦交换两个对象,直接交换指针就行了。可是默认的swap并不知道这些,它不止复制3个 Widget
,还复制3个 WidgetImpl
对象。非常缺乏效率。我们能否告知std::swap:当交换Widget
时,实际是交换其内存的 pImpl
指针?
这当然可以的:将std::swap针对 Widget
特殊化(specialization),下面是基本构想,但目前无法通过编译:
namespace std{
//特殊化的std::swap,当T是Widget类型时使用如下实现
template<>
void swap<Widget>(Widget& a, Widget& b)
{
swap(a.pImpl,b.pImpl); //交换Widget时只要交换它们的pImpl指针
}
}
template<>
代表了下面的代码是对 std::swap 的完全特殊化(total specialization)实现,函数名称后的 <Widget>
则代表了当T是 Widget
类型时使用这个特殊实现。也就是对于其它类型依然使用默认的 std::swap,仅仅对于Widget类型才使用特殊化。
请记住,通常我们不能够(不被允许)改变 std 命名空间内的任何东西,但可以(被允许)为标准 template(如swap)设计特殊化版本,使它专属于我们自己的类(如Widget
)。
但这个函数是无法通过编译的,因为Widget
内的 pImpl
指针在 private 域内,我们无法访问。我们可以在Widget
内声明一个名为 swap 的 public 成员函数做真正的交换工作,然后将 std::swap 特殊化,令它调用该成员函数:
class Widget{//与前面一样,唯一差别时增加swap函数
public:
...
void swap(Widget& other){
using std::swap; //这句稍后解释
swap(pImpl, other.pImpl); //执行真正的swap,只交换指针
}
...
};
namespace std{
template<> //完全特殊化的std::swap
void swap<Widget>(Widget& a, Widget& b){
a.swap(b); //若要交换Widget,调用其swap成员函数
}
}
这种做法不止能通过编译,还与 STL 容器有一致性。因为 STL 容器也使用了 public swap成员函数和一个特殊化的std::swap来调用这个成员函数实现高效交换功能。
类的交换的讨论结束了,那么对于类模板的交换呢?
template<typename T>
class WidgetImpl{...};
template<typename T>
class Widget{...};
如果我们还和上面一样,在 Widget
内增加个 swap 成员函数,这样是无法通过编译的:
namespace std{
template<typename T>
void swap<Widget<T>>(Widget<T>& a, Widget<T>& b){ //非法代码
a.swap(b);
}
}
这种做法叫部分特殊化(partial specialization),即 template<...>
参数表里面还有一个模板参数而不是完全特殊化的 template<>
。C++只允许对类模板进行部分特殊化,但不允许对函数模板进行部分特殊化,因此这个方法是无法通过编译的。
当你打算部分特殊化一个函数模板时,通常的做法是写一个std::swap模板的重载:
namespace std{
//定义一个重载函数模板
template<typename T>
void swap(Widget<T>& a, Widget<T>& b){ //请注意与上面的swap的区别,函数名后面没有了<...>就不是特殊化了
a.swap(b);
}
}
一般而言,重载函数模板没有问题,但 std 是个特殊的命名空间,我们可以对其中的模板进行特殊化,但不允许添加新的模板。因为只有C++委员会才可以对std的内容进行修改。
那现在怎么办呢?我们可以声明一个非成员函数 swap,让它调用成员函数 swap,但不再将那个非成员函数 swap 声明为 std::swap 的特殊化版本或重载版本。假设 Widget
的所有相关机能都在命名空间 WidgetStuff
(不能放在 std 命名空间)内:
//我们自己的名空间
namespace WidgetStuff{
//我们的类模板
template<typename T>
class Widget{...};
...
//swap函数和类模板在同一命名空间
template<typename T>
void swap(Widget<T>& a, Widget<T>& b){
a.swap(b);
}
...
}
这样做还有一个好处就是能把我们自定义的类相关的所有功能全部整合在一起,在逻辑上和代码上都更加整洁。而且这也符合C++的函数搜索规则,会自动在函数每个实参的命名空间和全局作用域内查找函数,即实参依赖查找(argument dependent lookup)。
这个方法既适用于类也适用于类模板,所以我们应该在任何时候都使用它。但是我们必须对 std::swap 进行特殊化,如果想让我们的 swap 函数适用于更多情况,那么除了在我们自己的命名空间里写一个 swap,在 std 里面依然要特殊化一个 swap,下面就会讲到。
目前为止,我们所写的每一样东西都和 swap 编写者有关。换位思考,从用户观点看事情也有必要。假如我们正在写一个函数模板,其内需要交换两个对象值:
template<typename T>
void doSomething(T& obj1, T& obj2){
...
swap(obj1,obj2);
..
}
应该调用哪个 swap?是 std 默认的那个版本,还是某个可能存在的特殊化版本,抑或是一个可能存在的 T 专属版本而且可能栖身于某个命名空间(但当然不可以是 std)内。 最理想的情况是,调用 T 专属版本,并在它不存在的情况下调用 std 内的一般化版本:
template<typename T>
void doSomething(T1& obj1, T2& obj2){
..
using std::swap; //让std::swap对编译器可见
swap(obj1,obj2); //为T类型对象调用最佳swap版本
...
}
当编译器看到要调用 swap 的时候,实参依赖查找会让编译器在全局作用域和实参所在的命名空间里搜索适当的 swap 调用。例如,如果T是 Widget 类型,那么编译器就会使用实参依赖查找找到 Widget 的命名空间里的 swap。如果没有的话,编译器使用 std 内的 swap,这归功于 using std::swap
让 std::swap 在函数内曝光。然而编辑器还是比较喜欢 std::swap 的 T 专属特殊化版本,而非默认的 swap,所以如果你已针对 T 将 std::swap 特殊化,特殊化版本会被编译器挑中。
因此,令适当的 swap 被调用是很容易的,但是需要注意的是,别为这一调用添加额外的修饰符,因为这会影响C++挑选适当的函数,如下:
std::swap(obj1,obj2); //这是错误的调用方式
这强迫了编译器只认 std 内的swap(包括其任何特殊化的模板),因此不可能再调用一个适合它的 swap 函数。
我们讨论了 swap 的默认版本、成员函数版本、非成员函数版本 、std 特殊化版本以及对 swap 的调用,现在我们来总结一下:
- 首先,如果 swap 的默认版本对你的类或类模板提供可接受的效率,你不需要额外做任何事
- 其次,如果 swap 的默认版本的效率不足(意味着你的类和模板使用了某种 pimpl 手法),试着做以下事情:
- 提供一个 public swap 成员函数,让它高效的交换你的类型的两个对象值,这个函数绝不能抛出异常
- 在你的类或模板所在的命名空间内提供一个非成员函数 swap ,并令它调用上面的 swap 成员函数
- 如果你正编写一个类(而非类模板),为你的类特殊化 std::swap ,并令它调用你的 swap 成员函数
- 最后,如果你调用 swap ,请加上
using std::swap
,以便让 std::swap 在你的函数内曝光,然后不加任何命名空间名字调用 swap
为什么成员函数 swap 不能抛出异常? 因为 swap 这个功能本身经常会被用来实现异常安全。但是非成员函数的 swap 则可能会抛出异常,因为它还包括了拷贝构造等功能,而这些功能则是允许抛出异常的。当你写一个高效的 swap 实现时,要记住不仅仅是为了高效,更要保证异常安全,但总体来讲,高效和异常安全是相辅相成的。
Note:
- 当 std::swap 对你的类型效率不高时,提供一个 swap 成员函数,并确定这个函数不抛出异常
- 如果你提供一个成员函数 swap,也该提供一个非成员函数 swap 用来调用前者,对于 类(而非模板),也请特殊化 std::swap
- 调用 swap 时应针对 std::swap 使用 using 声明式,然后调用 swap 并且不带任何命名空间资格修饰符
- 为用户定义类型进行 std 模板全特殊化时好的,但千万不能在 std 内加入任何新模板