Design and Declaration-25

25.Consider support for a non-throwing swap.(考虑写出一个不抛出异常的swap函数)
swap是个有趣的函数。原本它只是STL的一部分,而后成为异常安全性编程的脊柱,以及用来处理自我赋值可能性的一个常见机制。

(1)所谓swap两对象值,意思是将两对象的值彼此赋予对方。缺省情况下swap动作可由标准程序库提供的swap算法完成。其典型实现如下:

只要类型T支持copying(通过copy构造函数和copy assignment操作符完成),缺省的swap实现代码就会帮你置换类型为T的对象。

(2)然而缺省的swap版本十分平淡,它涉及三个对象的复制:a复制到temp,b复制到a,以及temp复制到b。但是对某些类型而言,这些复制动作无一必要!其中最主要的就是“以指针指向一个对象,内含真正数据”那种类型。这种设计的常见表现形式是所谓“pimpl(pointer to implementation)手法”。考虑如下Widget class:

1)一旦要置换两个Widget对象值,我们唯一需要做的是置换其pImpl指针,但缺省的swap不知道这一点。它不仅复制三个Widget,还复制三个WidgetImpl对象。
2)因此,我们希望告诉std::swap,当Widgets被置换时真正该做的是置换其内部的pImpl指针。一个做法是:将std::swap针对Widget特化。有如下改进(目前仍无法通过编译):

3)其中,template<>表示它是std::swap的一个全特化版本,<Widget>表示这一特化版本是针对“T是Widget”而设计的。换句话说,当一般性的swap template施于Widget身上便会启用这个版本。

4)而这个函数无法通过编译是因为,它企图访问a和b内的pImpl,那这是private的。

(3)对此我们可以将这个特化版本声明为friend,但和以往的规矩不太一样:我们令Widget声明一个名为swap的public成员函数做真正的置换工作,然后将std::swap特化,令它调用该成员函数:

1)这种做法不仅能通过编译,还与STL容器有一致性,因为所有STL容器也都提供有public swap成员函数和std::swap特化版本(用以调用前者)。

(4)我们继续假设,假设Widget和WidgetImpl都是class template而非class:

1)接着,像之前那样,在Widget内放个swap成员函数,并且在类外部特化std::swap(这是就出现问题了):

2)那么,问题出在哪里了?
问题在我们企图偏特化(partially specialize)一个function template,但C++只允许对class template偏特化,在function template身上偏特化是行不通的!

3)那么,要怎么解决呢?
解决办法是:当我们打算偏特化一个function template时,惯常做法是简单地为它添加 一个重载版本:

4)为什么上述重载不合法?
一般而言,重载function template没有问题,但std是个特殊的命名空间,客户可以全特化std内的template,但不可以添加新的template到std里。
5)对此,我们还是声明一个non-member swap让它调用member swap,但不再将那个non-member swap声明为std::swap的特化版本或重载版本。为简化起见,假设Widget的所有相关技能都被置于命名空间WidgetStuff内(也不非得如此,也可置于global命名空间):

6)现在,任何地点的任何代码如果打算置换两个Widget对象而调用swap,C++的名称查找法则会找到WidgetStuff内的Widget专属版本。

7)这种做法对class和class template都行得通,所以似乎我们应该在任何时候都使用它?不幸的是有一个理由使你应该为class特化std::swap,所以如果你想让你的“class 专属版”swap在尽可能多的语境下被调用,你需要同时在该class所在命名空间内写一个non-member版本以及一个std::swap特化版本

(5)写完swap,我们接着从客户的角度来看看这样写的必要性:

1)上述代码会调用哪个swap?是std既有的一般化版本?还是某个可能存在的特化版本?亦或是一个可能存在的T的专属版本而且可能栖身与某个命名空间(但不是std)内?

2)我们希望调用T的专属版本,并在该版本不存在的情况下调用std内的一般化版本:

3)一旦编译器看到swap的调用,它们便查找适当的swap并调用之。C++名称查找法则确保将找到global作用域或T所在命名空间内的任何T专属的swap。
如果T是Widget并位于命名空间WidgetStuff内,编译器会找出WidgetStuff内的swap。如果没有T专属的swap存在,编译器就会使用std内的swap,而这得感谢using声明让std::swap在函数内曝光。

(6)此刻,我们已经讨论过default swap、member swap、non-member swap、std::swap特化版本、以及对swap的调用,现在如下总结:

1)首先,如果swap的缺省实现码对你的class或class template提供可接受的效率,你不需要额外做任何事。
2)其次,如果swap缺省实现版效率不足(那几乎总是意味你的class或template使用了某种pimpl手法),试着做一下事情:

  • 提供一个public swap成员函数,让让高效地置换你的类型的两个对象值
  • 在你的class或template所在的命名空间内提供一个non-member swap,并令它调用上述swap成员函数
  • 如果你正编写一个class(而非class template),为你的class特化std::swap,并令它调用你的swap成员函数

3)最后,如果你调用swap,请确保一个using声明式,以便让std::swap在你的函数内曝光可见,然后不加任何namespace修饰符,赤裸裸的调用swap即可。

请记住:
(1)当std::swap对你的类型效率不高时,提供一个swap成员函数,并确定这个函数不抛出异常。
(2)如果你提供一个member swap,也该提供一个non-member swap来调用前者。对于class(而非template),也请特化std::swap。
(3)调用swap时应针对std::swap使用using声明式,然后调用swap并且不带任何“姓名空间修饰”。
(4)为“用户定义类型”进行std template全特化是好的,但千万不要尝试在std内加入某些对std而言全新的东西。

发表评论

电子邮件地址不会被公开。 必填项已用*标注