swap 是一个有趣的函数。最早作为 STL 的一部分被引入,后来它成为异常安全编程(exception-safe programming)的支柱和压制自赋值可能性的 通用机制。因为 swap 太有用了,所以正确地实现它非常重要,但是伴随它的不同寻常的重要性而来的,是一系列不同寻常的复杂性。在本文中,我们就 来研究一下这些复杂性究竟是什么样的以及如何对付它们。
交换两个对象的值就是互相把自己的值送给对方。缺省情况下,通过标准的交 换算法来实现交换是非常成熟的技术。典型的实现完全符合你的预期:
namespace std {
template<typename T> // typical implementation of std::swap;
void swap(T& a, T& b) // swaps a’s and b’s values
{
T temp(a);
a = b;
b = temp;
}
}
可是,缺省的 swap 实现可能不那么酷。它涉及三个对象的拷贝:从 a 到 temp,从 b 到 a,以及从 temp 到 b。对一些类型来说,这些副 本全是不必要的。对于这样的类型,缺省的 swap 就好像让你坐着快车驶入小巷。
这样的类型中最重要的就是那些主要由一个指针组成的 类型,那个指针指向包含真正数据的另一种类型。这种设计方法的一种常见的表现形式是 "pimpl idiom"("pointer to implementation")。一个使用了这种 设计的 Widget 类可能就像这样:
class WidgetImpl {
// class for Widget data;
public: // details are unimportant
...
private:
int a, b, c; // possibly lots of data -
std::vector<double> v; // expensive to copy!
...
};
class Widget {
// class using the pimpl idiom
public:
Widget(const Widget& rhs);
Widget& operator=(const Widget& rhs) // to copy a Widget, copy its
{
// WidgetImpl object. For
... // details on implementing
*pImpl = *(rhs.pImpl); // operator= in general,
... // see Items 10, 11, and 12.
}
...
private:
WidgetImpl *pImpl; // ptr to object with this
}; // Widget’s data
当交换 Widgets 的是时候, 我们应该告诉 std::swap 我们打算做什么,执行交换的方法就是交换它们内部的 pImpl 指针。这种方法的正规说法是:针对 Widget 特化 std::swap(specialize std::swap for Widget)。下面是一个基本的想法,虽然在这种形式下它还不能通过编译:
namespace std {
template<> // this is a specialized version
void swap<Widget>(Widget& a, // of std::swap for when T is
Widget& b) // Widget; this won’t compile
{
swap(a.pImpl, b.pImpl); // to swap Widgets, just swap
} // their pImpl pointers
}
可是,就像我说的,这个函数还不能编译。那是因为它试图访问 a 和 b 内部的 pImpl 指针,而它们是 private 的。我们可以将我们的特化声 明为友元,但是惯例是不同的:让 Widget 声明一个名为 swap 的 public 成员函数去做实际的交换,然后特化 std::swap 去调用那个成员函数 :
class Widget { // same as above, except for the
public: // addition of the swap mem func
...
void swap(Widget& other)
{
using std::swap; // the need for this declaration
// is explained later in this Item
swap(pImpl, other.pImpl); // to swap Widgets, swap their
} // pImpl pointers
...
};
namespace std {
template<> // revised specialization of
void swap<Widget>(Widget& a, // std::swap
Widget& b)
{
a.swap(b); // to swap Widgets, call their
} // swap member function
}
可是,假设 Widget 和 WidgetImpl 是类模板,而不 是类,或许因此我们可以参数化存储在 WidgetImpl 中的数据类型:
template<typename T>
class WidgetImpl { ... };
template<typename T>
class Widget { ... };
namespace std {
template<typename T>
void swap<Widget<T> >(Widget<T>& a, // error! illegal code!
Widget<T>& b)
{ a.swap(b); }
}
当我们想要“部分特化”一个函数模板时,通常做法是简单地增加一个重载。看起来就像 这样:
namespace std {
template<typename T> // an overloading of std::swap
void swap(Widget<T>& a, // (note the lack of "<...>" after
Widget<T>& b) // "swap"), but see below for
{ a.swap(b); } // why this isn’t valid code
}
因此该怎么做呢?我们还是需要一个方法,既使其他人能调用 swap,又能让我们得到更高效的模板特化版本。答案很简单。我们还是声明一个非成员 swap 来调用成员 swap,只是不再将那个非成员函数声明为 std::swap 的特化或重载。例如,如果我们的 Widget 相关机能都在 namespace WidgetStuff 中,它看起来就像这个样子:
namespace WidgetStuff {
... // templatized WidgetImpl, etc.
template<typename T> // as before, including the swap
class Widget { ... }; // member function
...
template<typename T> // non-member swap function;
void swap(Widget<T>& a, // not part of the std namespace
Widget<T>& b)
{
a.swap(b);
}
}
这个方法无论对于类模板还是对于类都能很好地工作,所以看起来我们应该总是使用它。不幸的是,此处还是存在一个需要为类特化 std::swap 的动机(过一会儿我会讲到它),所以如果你希望你的 swap 的类专用版本在尽可能多的上下文中都能够调用(而你也确实这样做了),你就既 要在你的类所在的 namespace 中写一个非成员版本,又要提供一个 std::swap 的特化版本。
顺便提一下,如果你不使用 namespaces, 上面所讲的一切依然适用(也就是说,你还是需要一个非成员 swap 来调用成员 swap),但是你为什么要把你的类,模板,函数,枚举(此处作者连用了 两个词(enum, enumerant),不知有何区别——译者注)和 typedef 名字都堆在全局 namespace 中呢?你觉得合适吗?
迄今为止我所 写的每一件事情都适用于 swap 的作成者,但是有一种状况值得从客户的观点来看一看。假设你写了一个函数模板来交换两个对象的值:
template<typename T>
void doSomething(T& obj1, T& obj2)
{
...
swap(obj1, obj2);
...
}
template<typename T>
void doSomething(T& obj1, T& obj2)
{
using std::swap; // make std::swap available in this function
...
swap(obj1, obj2); // call the best swap for objects of type T
...
}
得到正确的 swap 调用是如此地容易。你需要小心的一件事是不要对调用加以限定,因为这将影响 C++ 确定该调用的函数,如果你这样写对 swap 的调用,
std::swap(obj1, obj2); // the wrong way to call swap
这将强制编译器只考虑 std 中的 swap(包括任何模 板特化),因此排除了定义在别处的更为适用的 T 专用版本被调用的可能性。唉,一些被误导的程序员就是用这种方法限定对 swap 的调用,这也就是为 你的类完全地特化 std::swap 很重要的原因:它使得以这种被误导的方式写出的代码可以用到类型专用的 swap 实现。(这样的代码还存在于现在的一些标 准库实现中,所以它将有利于你帮助这样的代码尽可能高效地工作。)到此为止,我们讨论了缺省的 swap,成员 swaps,非成员 swaps,std::swap 的特化版本,以及对 swap 的调用,所以让我们总结一下目前的状况。
首先,如果 swap 的缺省实现为你的类或类模板 提供了可接受的性能,你不需要做任何事。任何试图交换你的类型的对象的人都会得到缺省版本的支持,而且能工作得很好。
第二,如果 swap 的缺省实现效率不足(这几乎总是意味着你的类或模板使用了某种 pimpl idiom 的变种),就按照以下步骤来做:
提供一个能高效地 交换你的类型的两个对象的值的 public 的 swap 成员函数。出于我过一会儿就要解释的动机,这个函数应该永远不会抛出异常。
在你的类 或模板所在的同一个 namespace 中提供一个非成员的 swap。用它调用你的 swap 成员函数。
如果你写了一个类(不是类模板),就为你 的类特化 std::swap。用它也调用你的 swap 成员函数。
最后,如果你调用 swap,请确保在你的函数中包含一个 using 声明使 std::swap 可见,然后在调用 swap 时不使用任何 namespace 限定条件。
唯一没有解决的问题就是我的警告——绝不要让 swap 的成员版本抛出异 常。这是因为 swap 的非常重要的应用之一是为类(以及类模板)提供强大的异常安全(exception-safety)保证。这项技术基于 swap 的成员版本绝不会 抛出异常的假设。这一强制约束仅仅应用在成员版本上!它不能够应用在非成员版本上,因为 swap 的缺省版本基于拷贝构造和拷贝赋值,而在通常情况 下,这两个函数都允许抛出异常。如果你写了一个 swap 的自定义版本,那么,典型情况下你是为了提供一个更有效率的交换值的方法,你也要保证这个 方法不会抛出异常。作为一个一般规则,这两种 swap 的特型将紧密地结合在一起,因为高效的交换几乎总是基于内建类型(诸如在 pimpl idiom 之下的指 针)的操作,而对内建类型的操作绝不会抛出异常。
Things to Remember
·如果 std::swap 对于你的类型来说是低效的, 请提供一个 swap 成员函数。并确保你的 swap 不会抛出异常。
·如果你提供一个成员 swap,请同时提供一个调用成员 swap 的非成员 swap。对于类(非模板),还要特化 std::swap。
·调用 swap 时,请为 std::swap 使用一个 using 声明,然后在调用 swap 时不使用任何 namespace 限定条件。
·为用户定义类型完全地特化 std 模板没有什么问题,但是绝不要试图往 std 中加入任何全新的东西。
