有如下两个类 当前需要交换两个Widget对象
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指针 但std::swap置换算法并不知道这一点
它会复制三个Widgets,三个WidgetImpl对象 (temp=a;a=b;b=temp;)
因此我们需要对std::swap做特例化来实现我们的优化
namespace std{
template<>
void swap<Wideget>(Widget& a,Widget& b){
swap(a.pImpl,b.pImpl);
}
这个版本当前会有编译错误 原因是pImpl是private数据成员
特例化版本无法获得访问类内成员的权限
接下来如若考虑将该特例化版本设计为friend function
但根据条款23 封装性与提供的弹性变化成正比,与改动破坏性成反比.
friend function提供的封装性显然不足
不利于后续的改动 所以作一下的优化
考虑使用一个public的 Widget::swap(Widget& other)function进行指针交换
并作std::swap的特例化版本调用函数
此方法可以提升了类的封装性
Widget类的交换指针函数的实现如下
void Widget::swap(Widget& other){
using std::swap;//后续会讲这行代码的作用
swap(pImpl,other.pImpl);//交换内部指针
}
特例化版本函数调用类内swap实现如下
namespace std{
template<>
void swap<Widget>(Widget& a,Widget& b)
{
a.swap(b);
}
}
这种做法不止能通过编译 还与STL容器有一致性 容器内部实现也是这个方法
接下来考虑Widget,WidgetImpl不是class 而是class templates 能否做到类模板做参数实现置换
class templates设计代码如下
template<typename T>
class WidgetImpl{...};
template<typename T>
class Widget{...};
同样在Widget内放入public的swap函数给特例化版本std::swap调用 这个同样可以实现 此处省略
下面是std::swap函数的特例化版本实现
namespace std{
template<typename T>
void swap<Widget<T>>(Widget<T>& a,WIdget<T>& b)
{
a.swap(b);
}
}
这里会报错 为什么?
这是因为我们企图对特例化std::swap 这是一个function template
C++只允许class template特例化和部分特例化 该代码过不了编译
思考一下
考虑如下解决办法 实现class template的交换
member函数不变 同样为交换提供服务
non-member函数不再声明为std::swap的特例化版本或重载版本
将Widget的所有相关都放入一个名命空间WidgetStuff
实现代码如下
namespace WidgetStuff{
...
template<typename T> //template Widget
class Widget{...};
...
template<typename T>//non-member swap function
void swap(Widget<T>& a,Widget<T>& b){
a.swap(b);
}
}
那么现在任何地点的任何代码想要置换两个Widget对象 会调用swap
根据C++名称查找法则name lookup rules
会找到在名命空间WidgetStuff下的我们定义的WIdget置换的专属版本
这个作法无论是class or class template都行的通
namespace WidgetStuff实际上是做了内容的整理简化
避免了将各式各样的class,template,function,enum等塞入global名命空间
也同时将我们的Widget类和其std::swap特例化版本放在了一起 保证了查找
接下来讲述using std::swap;该行代码的作用
我们可以从std::swap设计者的角度考虑一下这个算法设计
假设我们正在写一个function template 其中需要置换函数
template <typename T>
void doSomething(T& obj1,T& obj2)
{
...
swap(obj1,obj2);
...
}
这时候我们希望调用哪个swap?改调用点上可能会有三个函数可供调用
1.std::swap的一般化版本
2.std::swap的特例化版本 需要打开namespace std写入
3.存在于某个namespace的T类型专属版本
namespace当然不能是std 上面讨论过这个问题
原因:function template不能被特例化
回到问题 现实设计当中大多数情况下应该希望当具有T类型的专属版本(3)的时候调用其专属版本
在专属(3)版本不存在的情况下 我们也同样希望std::swap能给与我们置换帮助(1或2 具体要看数据类型分析)
那么显然代码应该这么设计 如下
template<typename T>
void doSomething (T&obj1,T&obj2)}{
using std::swap;//该行代码使std::swap在此函数内可用
...
swap(obj1,obj2);//1.如果存在专属版本T调用专属版本swap
//2.如若不存在专属版本T,由于std::swap在该函数内可见也能实现数据置换
...
}
一旦编译器看到对swap的调用,首先会对swap进行查找
由于专属版本T与其class template同样写在namespace WidgetStuff下
根据name lookup rules(名称查找法则)确保找到改名命空间下的专属版本(前提是存在 也可能不存在)
如若不存在其专属版本 也就是编译器没找到
using std::name;由于该行代码会使标准库STL下名命空间std内的std置换函数曝光
那么如果存在针对std::swap的特例化版本 而非一般化版本
特化版本会被编译器选中 若不存在特化则一般化会被选中
三类都没有编译器就会报错
上述详见C++名称查找法则
下面是错误的调用
std::swap(obj1,obj2);//显式调用std::swap 错误调用方式
上述对于数据置换的操作术语是pimpl手法
即pointer to implementation的缩写
在std::swap的缺省实现效率不足的情况下可以考虑使用
总结一下 做以下事情:
1.提供一个public swap成员函数 高效地置换你的类型对象值 这个函数决不能抛出异常 后面解释
2.在你的class或class template所在名命空间内提供non-member swap 并令它调用上面这个函数
3.如果编写的是class而非class template 为你的class特化std::swap,(即开名命空间std写特例化)
并令它调用你的swap成员函数
成员函数不能抛出异常的原因(仅是一个劝告)
因为swap置换函数的一个最好的应用是帮助classes提供高效置换(废话)和强烈的异常安全性保障。
swap高效的置换总是基于对内置类型的操作(如底层指针),而内置类型的操作绝不会抛出异常
所以为我们做异常安全性的保障提供了条件。
根据条款29(后续写出)强烈的异常安全性保障总是值得的 所以应当保证成员函数不抛出异常。
为什么是成员函数不能抛出异常而不是非成员函数?
swap的缺省版本是以copy构造函数和copy assignment运算符作为基础的
通常情况下 这两个函数都有可能抛出异常
上述设计理念均出自Effective C++一书
by Core.
2020-1-30