首页 > 代码库 > 《Effective C++》学习笔记——条款25
《Effective C++》学习笔记——条款25
***************************************转载请注明出处:http://blog.csdn.net/lttree********************************************
四、Designs and Declarations
Rule 25:Consider support for a non-throwing swap
规则 25:考虑写出一个不抛异常的 swap 函数
swap 是一个有趣的函数。
原本它只是STL的一部分,而后成为异常安全性编程(exception-safe programming,详见条款29)的脊柱,以及用来处理自我赋值可能性的一个常见机制。
然而在非凡的重要性之外它也带来了非凡的复杂度。
本条款将探讨这些复杂度及因应之道。
1.一些基本的东西
所谓swap(置换)两对象的值,意思是将两对象的值彼此赋予对方。
缺省的情况下 swap动作可由标准程序库提供的swap算法完成。
namespace std { template<typename T> // std::swap的典型实现 void swap( T& a, T& b ) // 置换a和b { T temp(a); a = b; b = temp; } }
只要类型T支持copying(copy构造函数和copy assignment操作符),缺省的swap实现代码就会帮你置换类型为T的对象,所以不需要为此另外再做任何工作。
但是对于某些类型而言,这些复制动作无一必要,对它们而言swap的缺省行为太慢了!
2.以指针指向一个对象,内含真正数据
上面的某些类型中,最主要的就是这种——以指针指向一个对象,内含真正数据。
对于这种设计的常见表现形式是所谓的 “pimpl 手法”(pimpl是 "pointer to implementation"的缩写,详见条款31)。
用这种手法设计Widget class,就是这样:
class WidgetImpl { // 针对Widget数据而设计的class public: ... private: int a,b,c; std::vectorK<double> v; // 意味着复制时间更长 ... }; class Widget { // 这个class使用pimpl手法 public: Widget( const Widget& rhs ); Widget& operator=( const Widget& rhs ) // 复制Widget时,令它复制其WidgetImpl对象 { ... *pImpl = *(rhs.pImpl); ... } ... private: WidgetImpl* pImpl; };
一旦要置换两个Widget对象值,我们唯一需要做的就是置换其pImpl指针,但缺省的 swap算法不知道这一点。
它不只复制三个Widgets,还复制了三个WidgetImpl对象。
非常非常非常缺乏效率!
我们希望能告诉 std::swap:当Widgets被置换时真正该做的是置换其内部的pImpl指针。
确切实践这个思路的一个做法是:将std::swap针对Widget特化。
下面是基本构想:
namespace std { template<> void swap<Widget>( Widget& a, Widget& b ) { swap(a.pImpl,b.pImpl); // 只要置换它们指针即可 } }
但是,很遗憾这种形式,无法通过编译。
这个函数,一开始的"template<>"表示它是 std::swap的一个全特化版本,
函数名称之后的 <Widget>表示这一特化版本是针对“T是Widget”而设计。
换句话说:当一般性的swap template施行于 Widgets身上便会启用这个版本。
通常我们不能够(不被允许)改变std命名空间内的任何东西,但可以(被允许)为标准templates(如swap)制造特化版本,使它专属于我们自己的classes(例如Widget)。
> 但是!
这个函数无法通过编译,因为它企图访问a和b内的pImpl指针,但那个是 private!
解决方法:
?1、我们可以将这个特化版本声明为friend
?2、我们可以令Widget声明为一个名为swap的public成员函数做真正的置换工作,然后将std::swap特化,令它调用该成员函数。
class Widget { // 与之前相同,唯一差别是增加swap函数 public: ... void swap( Widget& other ) { using std::swap; // 这个声明很必要 swap(pImpl,other.pImpl); // 若要置换Widgets就置换其pImpl指针 } ... }; namespace std { template<> // 修订后的std::swap特化版本 void swap<Widget>( Widget& a,Widget& b ) { a.swap(b); } }
这种做法不但能够通过编译,还与STL容器有一致性,因为所有STL容器也都提供有public swap成员函数 和 std::swap特化版本(用以调用前者)。
3.如果面对class templates而非classes
然而假设Widget和WidgetImpl 都是class templates 而非 classes,也许我们可以试试将WidgetImpl 内的数据类型加以参数化:
template<typename T> class WidgetImpl { ... }; template<typename T> class Widget { ... };
在Widget内(以及WidgetImpl内,如果需要的话)放个swap成员函数就像以往一样简单,但我们却在特化std::swap时遇上乱流,
我们想这样写:
namespace std { template<typename T> void swap<Widget T>(Widget<T>& a,Widget<T>& b) // 可惜,不合法,是错误的 { a.swap(b); } }
这个看起来合情合理,但是不合法。
因为我们企图偏特化一个function template(std::swap),但C++只允许对class templates进行偏特化,在function templates身上偏特化是行不通的。
这段代码不该通过编译(虽然有些编译器错误地接受了它)。
当你打算偏特化一个function templates时,惯常的做法是简单地为它添加一个重载模板:
namespace std { template<typename T> void swap(Widget<T>& a,Widget<T>& b) { a.swap(b); } }
一般而言,重载function templates没有问题,但std是个特殊的命名空间,其管理规则也比较特殊。
客户可以全特化std内的templates,但不可以添加新的templates(或classes或functions或其他任何东西)到std里头。
std的内容完全由C++标准委员会决定,标准委员会禁止我们膨胀那些已经声明好的东西。
其实,违反上面那些,也可以通过编译,但是会产生不可预期的行为。
所以,断了那个念头吧。
4.non-member函数,再次出场
声明一个non-member swap让它调用 member swap,
但不再将那个non-member swap声明为std::swap的特化版本或重载版本。
假设Widget的所有相关机能都被置于命名空间WidgetStuff内,就是这样:
namespace WidgetStuff { ... template<typename T> class Widget { ... }; ... template<typename T> void swap( Widget<T>& a,Widget<T>& b ) // 这里并不属于std命名空间 { a.swap(b); } }
现在,任何地点任何代码如果打算置换两个Widget对象,因而调用swap,C++的名称查找法则会找到WidgetStuff内的Widget专属版本。
>PS:
这种做法对classes和class templates都行得通,所以似乎我们应该在任何时候都使用它。
可惜,有一个理由让你应该为classes特化std::swap,稍后将讲述。
所以,如果你想让你的“class 专属版”swap在尽可能多的语境下被调用,你需同时在该class所在命名空间内写一个non-member版本以及一个std::swap特化版本。
目前为止,我们所写的每一样东西都和swap编写者有关。
换位思考一下,从客户观点来看看事情也有必要。
假设你正在写一个function template,其内需要置换两个对象值:
template <typename T> void doSomething(T& obj1, T& obj2 ) { ... swap(obj1,obj2); ... }
应该调用哪个swap呢?是std既有的那个一般化版本?还是某个可能存在的特化版本?
亦或是一个可能存在的T专属版本而且可能栖身于某个命名空间(但当然不可能是std)内?
你希望的应该是调用T专属版本,并在该版本不存在的情况下调用std内的一般化版本。
下面是我们希望的版本:
template<typename T> void doSomething(T& obj1,T& obj2) { using std::swap; // 令std::swap在此函数内可用 ... swap(obj1,obj2); // 为T型对象调用最佳的swap版本 ... }
一旦编译器看到对swap的调用,它们便会查找适当的swap并调用之。
>额外knowledge:
? C++的名称查找法则(name lookup rules )确保将找到global作用域或T所在之命名空间内的任何T专属的swap。
? 如果T是Widget并位于命名空间WidgetStuff内,编译器会使用“实参取决之查找规则”找出WidgetStuff内的swap。
? 如果没有T专属之swap存在,编译器就会使用std内的swap,这需要感谢using声明式让std::swap在函数内曝光。
? 然而即便如此编译器还是比较喜欢std::swap的T专属特化版,而非一般化的那个template,所以如果你已针对T将std::swap特化,特化版本会被编译器挑中。
注意,
令适当的swap被调用是很容易的。
但不要为这一调用添加额外的修饰符,因为那会影响C++挑选适当函数。
比如用这种方式,就是错误的:
std::swap(obj1,obj2);
5.小小总结一下吧
我们已经讨论过default swap、member swaps、non-member swaps、std::swap特化版本 以及 对swap的调用。
(1) 如果swap的缺省实现码对你的class或class template提供可接受的效率,你不需要额外做任何事。任何尝试置换那种对象的人都会取得缺省版本,而那将有良好的运作。
(2) 如果swap缺省实现版的效率不足(那几乎总是意味你的class或template使用了某种pimpl手法),试着做下面这些事:
?1、提供一个 public swap成员函数,让它高效地置换你的类型的两个对象值。稍后将解释,这个函数绝不该抛出异常。
?2、在你的class或template所在的命名空间内提供一个 non-member swap,并令它调用上述swap成员函数。
?3、如果你正编写一个class(而非class template),为你的class特化std::swap。并令它调用你的swap成员函数。
(3) 如果你调用swap,请确定包含一个using声明式,以便让std::swap在你的函数内曝光可见,然后不加任何namespace修饰符,赤裸裸的调用swap。
(4) 唯一还未明确的就是:成员版swap决不可抛出异常。
6.请记住
★ 当std::swap对你的类型效率不高时,提供一个swap成员函数,并确定这个函数不抛出异常。
☆ 如果你提供一个member swap,也该提供一个non-member swap用来调用前者。对于 classes(而非templates),也请特化 std::swap。
★ 调用swap时应针对 std::swap 使用using声明式,然后调用swap并且不带任何“命名空间资格修饰”。
☆ 为“用户定义类型”进行std templates全特化是好的,但千万不要尝试在std内加入某些对std而言全新的东西。
***************************************转载请注明出处:http://blog.csdn.net/lttree********************************************
《Effective C++》学习笔记——条款25