声明:
- 文中内容收集整理自《Effective C++(中文版)第三版》,版权归原书所有。
- 本内容在作者现有能力的基础上有所删减,另加入部分作者自己的理解,有纰漏之处敬请指正。
条款25:考虑写出一个不抛出异常的swap函数
Consider support for a non-throwing swap.
swap是个有趣的函数。原本它只是STL的一部分,而后成为异常安全性编程(exception-safe programming)的脊柱,以及用来处理自我赋值可能性的一个常见机制。
缺省情况下swap动作可由标准库std命名空间中的swap算法完成:
namespace std
{
template<typename T>
void swap(T& a, T& b)
{
T temp(a);
a = b;
b = temp;
}
}
只要类型T支持拷贝构造和拷贝赋值就可以使用swap,缺省的swap就可以实现两个对象值的置换。
然而有时候对于某些类型而言,默认swap中的复制动作无一必要。其中最主要的就是“以指针指向一个对象,内含真正的数据”那种类型。这种设计的常见表现形式是所谓的“pimpl手法”(pimpl是“pointer to implementation”的缩写)。对pimpl手法的类使用swap很缺乏效率。 以这种手法设计的Widget class如下:
class WidgetImpl
{
public:
private:
//可能有很多数据,意味着复制时间很长
int a, b, c;
std::vector<double>v;
};
class Widgt {
public:
Widgt(const Widgt& rhs);
Widgt& operator = (const Widgt& rhs)
{
*pImpl = *(rhs.pImpl);
}
private:
WidgetImpl* pImpl; //指针所指对象内含Widget
};
一旦要交换两个Widget对象值,我们唯一要做的就是置换其pImpl指针,但缺省的swap算法不只会复制三个Widgets,还复制是哪个WidgetImpl对象,效率非常低!!!一点都不让人兴奋!!!
一个思路是像std::swap针对Widget特化:
namespace std
{
template<> //表示它是std::swap的一个全特化版本
void swap<Widget>(Widget& a, Widget& b) //“T是Widget”的特化版本
{
swap(a.pImpl, b.pImpl);
}
}
当一般性的swap template施行于Widgets身上便会启用这个版本。通常我们不被允许改变std命名空间内的任何东西,但可以被允许为标准templates制造特化版本,使它专属于我们自己的classes(例如Widget)。但这个函数无法通过编译,因为它无法访问private数据成员。
那么现在可以令Widget内声明一个名为swap的public成员函数做真正的置换工作,然后将std::swap特化,令它调用该成员函数,代码如下:
class WidgetImpl
{
public:
WidgetImpl(const vector<double>& vec = {0.0}, int a1 = 0, int b1 = 0, int c1 = 0) : a(a1), b(b1), c(c1), v(vec)
{ }
~WidgetImpl() = default;
private:
//可能有很多数据,意味着复制时间很长
int a, b, c;
std::vector<double>v;
};
class Widget
{
public:
Widget() = default;
Widget(WidgetImpl& widgetimpl) : pImpl(&widgetimpl)
{ }
Widget(const Widget& rhs);
~Widget() = default;
Widget& operator = (const Widget& rhs)
{
*pImpl = *(rhs.pImpl);
}
void swap(Widget& other)
{
using std::swap; //这个声明是必要的
swap(pImpl, other.pImpl); //若要置换Widgets就置换其pImpl指针
}
private:
WidgetImpl* pImpl;
};
namespace std //修改后的std::swap特化版本
{
template< >
void swap<Widget>(Widget& a, Widget& b)
{
a.swap(b); //若要置换Widgets,调用其swap成员函数
}
}
在main函数中可进行如下调用:
std::vector<double>vec = { 1.0, 2.0, 3.0, 4.0, 5.0 };
WidgetImpl wi(vec, 2, 3, 4);
Widget w(wi);
这种做法不仅能够通过编译,还与STL容器有一致性。因为所有STL容器也都提供有public swap成员函数和std::swap特化版本(用以调用前者)。
然而假设Widget和WidgetImpl都是class templates而非classes,也许我们可以试试将Widget和WidgetImpl中的数据类型加以参数化:
template<typename T>
class WidgetImpl{...};
template<typename T>
class Widget{...};
在Widget中放个swap同样很简单,但我们却在特化std::swap时遇上乱流,我们想写成这样:
namespace std //修改后的std::swap特化版本
{
template<typename T>
void swap< Widget<T> >(Widget<T>& a, Widget<T>& b)
{
a.swap(b); //若要置换Widgets,调用其swap成员函数
}
}
看起来合情合理但并不合法。我们企图偏特化一个function template,但C++只允许对class templates偏特化,在function templates身上偏特化是行不通的。这段代码不会通过编译。
当我们打算偏特化一个function template时,惯常的做法是简单为它添加一个重载版本,像这样:
namespace std //修改后的std::swap特化版本
{
template<typename T>
void swap(Widget<T>& a, Widget<T>& b) //注意swap之后没有"<>"
{
a.swap(b); //若要置换Widgets,调用其swap成员函数
}
}
但是C++对std命名空间的管理规则比较特殊,客户可以完全特化std内的templates,但不可以添加新的templates或classes或functions或其他任何东西到std里头。
那该如何是好?毕竟我们总是需要一个办法让他人调用swap时能够取得我们提供的比较高效的template特定版本。答案很简单,我们还是声明一个non-member swap让它调用member swap,但不再将那个non-member swap声明为std::swap的特化版本或重载版本。
不允许重载std中的函数,解决方法是在一个命名空间中定义一个swap 和 Widget WidgetImpl 等等模板, 在这个空间中的swap调用类中的成员函数。但与此同时建议提供一个std的特化Swap版本。
namespace WidgetStuff
{
...
template<typename T>
class Widget{...}; //同前,内含swap成员函数
...
template<typename T>
void swap(Widget<T>& a, Widget<T>& b)
{
a.swap(b);
}
}
如果你想让你的class专属版在尽可能多的语境下被调用,你需要同时在该class所在的命名空间中写一个non-member版本以及一个std::swap特化版本。
假设我们正在写一个function template,其内置需要置换两个对象值:
template<typename T>
void doSomeething(T& obj1, T& obj2)
{
...
using std::swap;
swap(obj1, obj2);
...
}
一旦编译器看到对swap的调用,它们便会查找适当的swap并调用之,C++的名称查找法则将确保找到global作用域或T所在的命名空间中的任何T专属的swap,优先使用T所在命名空间中的swap,如果没有T专属之swap存在,编译器就使用std内的swap,前提是需要在函数中使用使用using std::swap使之在函数内曝光。而如果已针对T将std::swap特化,那么特化版本将会优先被编译器挑中。
需要小心的是,在调用swap函数时,千万不要为它添加额外的修饰符(如std::swap(obj1, obj2)),那会影响C++挑选适当的函数。
现在我们对上面的情势做一个总结:
- 如果缺省的swap可以对你的class或者class template提供可接受的效率,那不需要做任何事情。
- 如果swap缺省实现版本效率不足(那几乎总是意味着你的class或template使用了某种pimpl手法),试着做以下事情:
- 提供一个public swap成员函数,让它高效地置换你的类型的两个对象值;
- 在你的class或者template所在的命名空间内提供一个non-member swap,并令它调用上述swap成员函数。
- 如果你正在编写一个class(而非class template),为你的class特化std::swap,并令它调用你的swap成员函数。
- 如果你调用swap,请确定包含一个using声明式,以便让std::swap在你的函数内曝光可见,然后不加任何namespace修饰符,赤裸裸地调用swap。
请记住
- 当std::swap对你的类型效率不高时,提供一个swap成员函数,并确定这个函数不抛出异常。
- 如果你提供一个member swap,也该提供一个non-member swap用来调用前者。对于classes(而非templates),也请特化std::swap。
- 调用swap时应针对std::swap使用using声明式,然后调用swap并且不带任何“命名空间资格修饰”。
- 为"用户定义类型"进行std templates全特化是好的,但千万不要尝试在std内增加某些对std而言全新的东西。