swap原本只是STL的一部分,而后成为异常安全便利的脊柱,以及用来处理自我赋值可能性的一个常见机制。由于swap如此有用,适当的实现很重要。
默认情况下swap操作可由标准库提供的swap算法完成。其典型实现完全如你所想:
namespace std {
template <typename T>
void swap(T& a, T& b) {
T = temp(a);
a = b;
b = temp;
}
}
只要T支持copying,默认的swap实现代码就会帮你置换类型T的对象,你不需要为此另外再做任何操作。
而需要复杂实现的是:以指针指向一个对象,内含真正数据的类型。这种设计的常见表现形式是pimpl手法(pointer to implementation)。如果以这种手法设计Widget class,看上去会像这样:
class WidgetImpl {
public:
...
private:
int a, b, c;
std::vector<double> v;
...
};
class Widget {
public:
Widget(const Widget& rhs);
Widget& operator=(const Widget& rhs) {
...
*pImpl = *(rhs.pImpl);
...
}
...
private:
WidgetImpl* pImpl;
};
一旦要置换两个Widget对象值,我们唯一需要做的就是置换其pImpl指针,但默认swap算法不知道这一点。它复制了三次Widget和WidgetImpl,的效率极低。
我们希望告诉 std::swap
,当Widget被置换时真正该做的是置换其内部的pImpl指针。确切的思路是对 std::swap
做针对Widget的特化。下面是基本构想,但这个形式无法通过编译:
namespace std {
template<>
void swap<Widget>(Widget& a, Widget& b) {
swap(a.pImpl, b.pImpl);
}
}
通常我们不能够改变std命名空间里的任何东西,但可以为标准template制造特化版本,使他专属于我们自己的class。以上便是如此。
这个函数无法通过编译,因为它企图访问a和b内的pImpl指针,而它是private的。我们可以将这个特化版本声明为friend。我们令Widget声明要给swap的public成员函数做真正的置换工作,然后将 std::swap
特化,令他调用该成员函数:
class Widget {
public:
...
void swap(Widget& other) {
using std::swap; // 此行有必要,稍后解释
swap(pImpl, other.Impl);
}
...
}
namespace std {
template<>
void swap<Widget>(Widget& a, Widget& b) {
a.swap(b_;)
}
}
这种做法不仅能通过编译,还与STL容器有一致性,因为所有的STL容器也都提供有public swap成员函数和 std::swap
特化版本(用以调用前者)。
如果Widget和WidgetImpl都是class template,我们需要为swap添加一个重载版本,因为C++不允许对函数模板进行偏特化。像这样:
namespace std {
template <typename T>
void swap(Widget<T>& a, Widget<T>& b) { // 这也不合法
a.swap(b);
}
}
std命名空间中,客户可以全特化template,但不可以添加新的template(包括重载)。这是一个未定义行为。
于是,我们还是可以声明一个non-member swap让它调用member swap,不再将swap声明为 std::swap
的特化或重载版本。并把Widget相关功能都放进命名空间WidgetStuff内:
namespace WidgetStuff {
...
template <typename T>
class Widget {...};
...
template <typename T>
void swap(Widget<T>& a, Widget<T>& b) { // 不属于std命名空间
a.swap(b);
}
}
现在,交换两个Widget对象,会找到WidgetStuff内的Widget专属版本的swap。那正是我们想要的。
现在,如何让编译器确定最合适的swap版本?使用 using std::swap
,让该函数与特化版本一起参与重载决议,编译器会选择最合适的版本执行。
template <typename T>
void doSomething(T& obj1, T& obj2) {
using std::swap;
...
swap(obj1, obj2);
...
}
现在我们做个总结:
首先,如果swap的默认代码对你的class提供可接受的效率,你不需要额外做任何事。
其次,如果swap的默认代码效率不足,试着做以下事情:
- 提供一个public swap成员函数,让它高效地置换你的类型的两个值。这个函数绝不该抛出异常。
- 在你的class或template所在命名空间内提供一个non-member swap,并令他调用上述swap成员函数。
- 如果你正在编写一个class,为你的class特化
std::swap
,并令他调用你的swap成员函数。
最后,如果你调用swap,请确认包含一个 using
声明式,让 std::swap
参与重载决议,然后不加任何namespace修饰符,直接调用 swap
。
请记住
- 当
std::swap
对你的类型效率不高时,提供一个swap成员函数,并确定这个函数不抛出异常。- 如果你提供一个member swap,也该提供一个non-member swap用来调用前者。对于class,也请特化
std::swap
。- 调用swap时应针对
std::swap
写using
声明式,然后调用swap而不加任何namespace修饰符。- 为用户自定义类型进行std template全特化是好的,但千万不要尝试在std内部加入某些对std而言全新的东西。