首页 > 代码库 > 《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