C++ 为自定义类型提供不抛出异常的swap gaunthan Posted on Nov 5 2016 ? C++ ? > 当定义自定义类型时,提供一个不抛出异常的 `swap` 操作是很有用处的。C++ 中的很多惯用法都使用到了 swap 操作,比如在处理拷贝赋值运算符(`copy operator=`)时,使用 copy-swap 惯用法可以很轻易的实现自赋值和异常安全,具体可以参阅[《C++ 处理好赋值运算符的自赋值》](https://leanote.com/blog/post/58076e00ab64413a7e00fb99)。此外,对于含有指针成员的自定义类型,提供 swap 操作往往会得到很大的性能提升。swap 作为一个最有用的异常安全代码的基础操作,这使得它任何时候都不能抛出异常。 <!--more--> ## 何为 swap `swap` 的含义就是交换两个对象的值,而一般情况下我们需要借用到一个中间变量。在 C 语言中,常常会把 swap 定义为如下形式: ```c typedef int ElemType ; ... // 交换两个变量的值 void swap(ElemType* a, ElemType* b) { ElemType tmp = *a; *a = *b; *b = tmp; } ``` ## std::swap ### std::swap的实现 C++标准库提供了泛型的 swap 实现,其定义在 std 名字空间中,除了实现类型参数化以及传递引用,其他没有什么变化: ```cpp namespace std { template<typename T> void swap(T& a, T& b) { T temp(a); a = b; b = temp; } } ``` 只要我们定义的类型支持拷贝操作(拷贝构造和拷贝赋值),默认的 swap 实现就可以正确地实现交换操作。 ### std::swap 的不足 `std::swap` 虽然是正确的,但对于某些类型效率不是很高,因为有时候我们不需要这些拷贝操作。比如使用 pimpl 惯用法的类,其通常只含有一个指针成员: ```cpp 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; // 指向包含真正数据的对象的指针 }; ``` 由于含有指针成员,`copy operator=`需要实现深拷贝,而这对于交换操作来说是不必要的。如果我们直接交换指针,将避免大量的拷贝赋值操作。 因此,我们需要实现自己的swap版本。 ## 特例化std::swap 因为标准库中很多操作都是使用 std::swap 的,为了获得更高的效率,我们需要特例化 std::swap,使其对我们定义的类型执行简单地交换指针操作: ```cpp namespace std { template<> void swap<Widget>(Widget& a, Widget& b) { swap(a.pImpl, b.pImpl); } } ``` 然而上面的代码无法通过编译,因为 pImpl 是私有成员,我们无法在类外访问。因此,需要一种方法能够访问私有成员。让一个非成员函数访问一个类的私有成员的方法,最容易想到的便是将其声明为友元(friend)。然而这违反了封装性原则,因此,传统上的做法是转发操作。 传统的实现是让自定义类型提供一个成员函数版本的swap,然后特化 std::swap 使其调用这个成员版本的 swap。 ### 提供成员版本的 swap ```cpp class Widget { public: ... void swap(Widget& other) { using std::swap; // 注意这里没有直接用std::swap,这与ADL有关。后面会解释 swap(pImpl, other.pImpl); } ... }; ``` ### 使 std::swap 特化调用成员版本的 swap ```cpp namespace std { template<> void swap<Widget>(Widget& a, Widget& b) { a.swap(b); } } ``` 现在,代码不仅可以通过编译,而且还与STL容器保持了一致性:提供公有的成员函数swap,以及提供调用该成员版本的 std::swap 特化。 ## 类模版带来的一些问题 ### 错误的模板函数偏特化 现在假设我们定义的是模板类,即我们的类需要参数化一些类型: ```cpp template<typename T> class WidgetImpl { ... }; template<typename T> class Widget { ... }; ``` 按照前面的讨论,我们很自然地就会写出下面的 std::swap 特化: ```cpp namespace std { template<typename T> void swap<Widget<T> >(Widget<T>& a, Widget<T>& b) { a.swap(b); } } ``` 然而,上面的代码是错误的,它并不能通过编译,因为我们正在尝试偏特化函数模版 std::swap。**C++标准仅支持模板类偏特化,而不支持函数模板偏特化**。因此这段代码无法通过编译。 ### 未定义的 std 注入行为 "正确"的实现方式是提供一个重载版本,因为对于我们自己的模板函数就是这样做的: ```cpp namespace std { template<typename T> void swap(Widget<T>& a, Widget<T>& b) { a.swap(b); } } ``` 然而上面的代码虽然能够通过编译,但却是不可移植的,这也意味着它具有未定义的行为。C++ 标准规定仅允许特化 std 名字空间中的模板,而不允许为其增加新的模板。跨越了这条线的程序员,需要为这一行为负责! 如果真的需要向 std 注入新内容来帮助项目,一定要考虑清楚,并且仔细阅读 C++ 标准中关于 namespace 的名字查找规则,还需要在使用的编译器上实际测试。 ### 在类所属的名字空间中定义 swap 上面提到的两种行为都行不通,那该怎么办呢?先想想我们想要的到底是什么:能够被其他人调用的、高效率的非成员 swap 模版特化。为了达到这一目标,需要用到 C++ 名字查找的规则:关联名称查找(ADL)。 这一做法是在类所属的名字空间中重载 swap: ```cpp namespace WidgetStuff { template<typename T> class Widget { ... }; ... template<typename T> void swap(Widget<T>& a, Widget<T>& b) { a.swap(b); } } ``` 现在,得益于 ADL,任何以两个 Widget 对象调用 swap 的代码都会调用到名字空间 WidegetStuff 中的特化了 Widget 的 swap 版本。而这就是我们一直想要的。 **注意,直接在全局名字空间中定义也是可以的,但那不是一个好的方式。** ## ADL查找规则 现在,是时候说说这一查找过程是如何进行的,而这也将解决我们在前文中的疑惑: ```cpp class Widget { public: ... void swap(Widget& other) { using std::swap; // 注意这里没有直接用std::swap,这与ADL有关。后面会解释 swap(pImpl, other.pImpl); } ... }; ``` 具体的内容可以阅读[《C++ 模版的名字查找问题》](http://leanote.com/blog/post/57e74e75ab64416b5501bfe0)。 总的来说就是:C++名字查找规则确保查找类型 T 特定的 swap。当找不到的时候,就使用 std::swap。然而如果有提供类型特化的 std::swap 版本,则会调用这一版本。 这意味着我们在调用 swap 的时候,不能指定它的名字空间,即不能使用 std::swap。不然,即使提供了更好的 swap 实现,编译器也不会去调用它。因此,我们只需要确保在没有比 std::swap 更好的存在时,使得编译器至少能够为我们选择 std::swap,而这需要使其可见。由此使用到了 using std::swap 这一声明。 ## 总结 - 如果对于自定义类型,std::swap 不高效,则提供一个 swap 成员函数。请确保它不抛出异常。 - 如果提供了成员函数 swap,则同时提供非成员函数 swap,并让它调用该成员版本。如果自定义类型不是类模板,则同时特化 std::swap。 - 当调用 swap 时,使用 using std::swap 并不带限定符地调用 swap。 - 为自定义类型全特化 std::swap 是一件好事,但不应该因此就向 std 名字空间注入新内容。 ## References - Meyers S. Effective C++[M]. 电子工业出版社, 2011. 赏 Wechat Pay Alipay 二维码的生成细节和原理 Matlab 结构体数组