0.概述
- 当std::swap对你的类型效率不高时,提供一个swap成员函数,并确定这个函数不抛出异常。
- 如果提供一个member swap,也该提供一个non-member swap用来调用前者。对于classes (而非templates),请特化std::swap。
- 调用swap时应针对std::swap使用using声明式,然后就可以直接调用swap。
- 为“用户定义类型”进行std templates全特化是好的,但千万不要尝试在std内加入某些新东西。
1.标准库提供的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;
}
}
只要类型T支持copying操作,swap函数就可以置换类型为T的对象。
缺点:涉及三个对象(a,b,temp)的复制
2.pimpl实现的Widget类
class WidgetImpl
{
public:...
private://可能会有很多数据,复制时间很长
int a,b,c;
std::vector<double> v;
};
使用pimpl手法实现的class:
class Widget//这个class使用pimpl手法
{
public:
Widget(const Widget& rhs);
Widget& operator=(const Widget& rhs)//复制widget时,令它复制其widgetImpl对象
{
...
*pimpl=*(rhs.pImpl);
}
private:
WidgetImpl* pImpl; //指针,所指对象内含Widget数据
};
要置换两个widget对象值,唯一需要做的就是置换其pImpl指针。但缺省的swap算法不只复制三个widgets,还复制三个 widgetImpl对象。效率低下!
2.1 将std::swap针对widget特化
置换指针的一个做法是:将std::swap针对widget特化。下面是基本构想,但目前这个形式无法通过编译:
namespace std {
template<> // this is a specialized version
void swap<Widget>(Widget& a, Widget& b)// of std::swap for when T is Widget
{
swap(a.pImpl, b.pImpl); // to swap Widgets, swap their
} // pImpl pointers; this won’t compile
}
tamplate<>表示这是一个std::swap的全特化版本,函数名称后的<Widget>表示这一特化版本是针对T是Widget而设计,也就是说当一般性的swap template施加于Widgets身上便会启用这个版本。
通常不允许改变std命名空间内的任何东西,但可以为标准templates制造特化版本,使其专属于我们自己的class。
通不过编译的原因:企图访问a,b的pImpl指针(私有数据成员),可以将这个特化版本声明为friend。
2.2 声明一个public成员函数做置换操作
这里采用的方法是令Widget声明一个swap的public函数做真正的置换工作,将swap特化令其调用该成员函数:
class Widget { // same as above, except for the
public: // addition of the swap mem func
...
void swap(Widget& other)
{
using std::swap; // 这个声明很重要
swap(pImpl, other.pImpl); // 置换指针
}
...
};
namespace std {
template<> // revised specialization of
void swap<Widget>(Widget& a, Widget& b)
{
a.swap(b); // to swap Widgets, call their
} // swap member function
}
这样做可以通过编译,而且与STL容器具有一致性(所有STL容器也都提供有public swap成员函数和std: :swap特化版本(用以调用前者))。
3. 假设Widget和WidgetImpl都是类模板
可以尝试将WidgetImpl内的数据类型加以参数化
template<typename T>
class WidgetImpl { ... };
template<typename T>
class Widget { ... };
3.1 函数模板偏特化(错)
回顾一下2.2,①在Widget内放一个swap成员函数;②特化std::swap。
第①步很简单,第②步:
namespace std
{
template<typename T>
void swap<Widget<T>>(Widget<T>& a, Widget<t>& b)//error!
{a.swap(b);}
}
错误原因:企图偏特化函数模板。C++只允许对类模板偏特化,不能对函数模板偏特化,这段代码无法通过编译。
3.2 为函数模板添加重载版本(不可)
namespace std
{
template<typename T>
void swap(Widget<T>& a, Widget<t>& b)//error!
{a.swap(b);}
}
一般而言,重载函数模板没有任何问题。但std是一个特殊的命名空间,可以全特化std内的模板,但不可向内添加新的(类、函数或其它)模板。
3.3 新建一个命名空间
namespace WidgetStuff {
... // 模板化的WidgetImpl,等
template<typename T> // 内含swap成员函数
class Widget { ... };
...
template<typename T> // 非成员swap函数;
void swap(Widget<T>& a, //不属于std命名空间
Widget<T>& b)
{
a.swap(b);
}
}
现在任何代码想要置换两个Widget对象,C++会找到WidgetStuff内的专属版本
但是如果还需要可以能够调用默认版本:
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的调用,它们便查找适当的swap并调用。C++的名称查找法则(name lookup rules)确保将找到global作用域或T所在命名空间内的任何T专属的swap。如果T是widget并位于命名空间widgetstuff内,编译器会使用“实参取决之查找规则”( argument-dependent lookup)找出 widgetstuff内的swap。如果没有T专属之swap存在,编译器就使用std内的swap,这得感谢using 声明式让std: :swap在函数内曝光。
4. 总结
首先,如果swap的缺省实现对你的class或class template提供可接受的效率,你不需要额外做任何事。
其次,如果缺省版本的效率不足,可以:
- 提供一个public swap成员函数,让它高效地置换你的类型的两个对象值。这个函数绝不该抛出异常。
- 在你的class或template所在的命名空间内提供一个non-member swap,并令它调用上述swap成员函数。
- 如果你正编写一个class(而非class template),为你的class特化 std::swap。并令它调用你的swap成员函数。
最后,如果调用swap,请确定包含一个using声明式,以便让 std::swap在你的函数内曝光可见,然后不加任何namespace修饰符,赤裸裸地调用swap。
note:<成员版>swap绝不可抛出异常