本文共 4213 字,大约阅读时间需要 14 分钟。
swap()属于STL的一部分(算法),而后成为异常安全性编程的脊柱,以及用来处理自我赋值的可能性的一个常见的机制。它是如此的有用,适当的实现就显得十分重要。然而在它的实现的复杂度也比较高。这个条款就在讲swap()函数的实现问题。
STL 缺省情况下,由STL提供的swap算法完成,如下:
namespace std { template<typename T> void swap(T&a, T&b) { T temp(a); a = b; b = temp; }}
只要类型T支持copying函数(copy构造、copy赋值符号函数),就可以调用STL的缺省的swap。
然而,标准库版本的swap却存在一个问题,它的实现进行了3次复制,对于某些实现来说,它导致的效率太低了。这些情况主要是指“以指针指向一个对象,内含真正数据”那种类型。这种设计的常见表现形式是所谓的"pimpl"手法。以这种手法实现widget 代码如下:
class WidgetImpl { private: int i;};class Widgt { Widgt(const Widgt&rhs); Widgt&operator = (const Widgt&rhs) { *imp = *rhs.imp; }private: WidgetImpl* imp;};
在这种情况下,实际上我们只交换两个指针的指向便可,没有必要交换所指物。但是怎么才能告诉标准库的swap呢?
答案:在std空间内全特化一个swap函数,然后在widget类内写一个成员函数swap,调用该全特化的swap函数。具体代码如下:
class WidgetImpl { public: int i;};class Widgt { public: Widgt() = default; Widgt(const Widgt&rhs); Widgt&operator = (const Widgt&rhs) { *imp = *rhs.imp; } void swap(Widgt& other) { using std::swap; swap(imp, other.imp); }public: WidgetImpl* imp; }; namespace std { template<> void swap<Widgt>(Widgt&a, Widgt&b) { a.swap(b); }}
这种做法的好处:
template<typename T>class WidgetImpl{ }; template<typename T>class Widget{ }; namespace std { template<typename T> void swap<Widget<T>>(Widget<T>&a, Widget<T>&b) { a.swap(b); }}
这种实现是不能通过编译的。widget内放一个swap成员函数时没问题的,但是我们无法偏特化std::swap()。
namespace std { template<typename T> void swap(Widget<T>&a, Widget<T>&b) { a.swap(b); }}
但是c++不允许重载std中的模板函数,因为这其实是试图扩充std。C++允许在std中全特化某个模板,但是不允许添加新的模板。
为了简化起见,假设Widget的所有相关机能被置于命名空间WidgetStuff,于是:
namespace WidgetStuff { ... //模板化的WidgetImpl等等 template<typename T> //和前面一样,内含swap成员函数 class Widget { ... }; ... template<typename T> //non-member swap函数 void swap(Widget<T>& a, //这里并不属于std命名空间 Widget<T>& b) { a.swap(b); }}
此时调用swap,C++的名称查找法则(name lookup rules;更具体的说是所谓argument-dependent lookup或Kobeig lookup法则)将会找到WidgetStuff内的Widget专属版本,这正是我们所希望的。
注意:
从用户角度考虑,假设正在写一个function template,其内需要置换两个对象值。
template<typename T>void doSomething(T& obj1, T& obj2){ ... swap(obj1, obj2); ...}
我们希望的是调用T专属版本,并在该版本不存在的情况下,再去调用std内的一般化版本,那么正确的写法如下:
template<typename T>void doSomething(T& obj1, T& obj2){ usint std::swap; //令std::swap在此函数内可用 ... swap(obj1, obj2); //为T型对象调用最佳swap版本 ...}
一旦编译器看到了对swap的调用,它们便查找适当的swap并加以调用。C++的名称查找法则会确保将找到global作用域或者T所在的命名空间内的任何T专属的swap。
如果已经针对T将std::swap进行了特化,这个特化版本也直接会被优先使用。因此,令适当的swap被调用是比较容易的。但需要小心的是:不要添加额外的修饰符,这样会影响C++挑选适当的函数:
std::swap(obj1, obj2); //错误的swap调用方式
上面这个举动,会迫使编译器只认std内的swap,因而不再可能调用一个定义于其他地方的适当T专属版本。
关于:
做一个总结:
首先,如果swap的缺省实现对我们的class或class template提供可接受的效率,那么我们并不需要做其他的事情。
其次,如果swap的缺省版本效率不足(通常就是因为class或者class template使用了某种pimpl手法),则:
最后,如果我们调用swap,请确定包含一个using声明式,以便让std::swap在我们的函数内部可以曝光可见,然后不加任何namespace修饰符,直接去调用swap。
swap的一个最好的应用就是为了帮助class(和class template)提供强烈的异常安全性(exception-safety)保障。
当然,这一约束只施行于成员版!不可实施于非成员版,因为swap缺省版本是以copy构造函数和copy assignment操作符为基础的,在一般情况下是允许抛出异常的。
因此,当我们写一个自定义版本的swap时,往往需要提供以下两点:
一般而言,上面这两个特性是连在一起的,因为高效率的swap几乎总是基于对内置类型的操作(例如pimpl首发的底层指针),而内置类型上的操作绝不会抛出异常。
转载地址:http://efgq.baihongyu.com/