洛译小筑

别来无恙,我的老友…
随笔 - 45, 文章 - 0, 评论 - 172, 引用 - 0
数据加载中……

[ECPP读书笔记 条目25] 最好不要让swap抛出异常

swap是一个非常有趣的程序。它起初是作为STL的一部分引入C++的,而后就成为了异常安全编程的一个重要的支柱(参见条目29),同时对于可以自赋值的对象而言它还是一个常用的复制处理机制。由于swap如此神通广大,那么以一个恰当的方式去实现它就显得十分重要了,但是它举足轻重的地位也决定了实现它并不是一件手到擒来的事情。在本条目中,我们就会针对swap函数展开探索,逐步掌握如何去驾驭它。

swap函数的功能是交换两个对象的值。在默认情况下,交换工作是通过标准库的swap算法完成的。它的标准实现方式就能精确地完成你所期望的工作:

namespace std {

 

  template<typename T>          // std::swap的标准实现

  void swap(T& a, T& b)         // 交换ab的值

  {

    T temp(a);

    a = b;

    b = temp;

  }

 

}

只要你的类型支持复制(通过拷贝构造函数和拷贝复制运算符),那么默认的swap实现就可以让两个该类型的对象互相交换,你不需要专门做任何工作来支持这一功能。

然而,你可能对默认的swap实现抱有诸多不满。它会带来3次对象复制工作:a复制到tempbatempb。对于一些类型来说,这些复制操作并不都是必需的。对于这些类型来说,默认的swap会成为你程序的桎梏。

上述的那种类型大都符合下面的特征:它的主要成分是一个指针,这一指针会指向另一个类型,真实的数据包含在另一个类型中。对这一设计的一种常见的形式是“pimpl惯用法”(pointer to implementation,指向实现的指针,参见条目31)。举例说明,Widget类可以使用这种设计模式:

class WidgetImpl {                      // 保存Widget的数据的类

public:                                 // 细节不重要

  ...

 

private:

  int a, b, c;                           // 可能会有很多数据

  std::vector<double> v;                 // 复制它们的代价是很高的!

  ...

};

 

class Widget {                           // 使用pimpl惯用法的类

public:

  Widget(const Widget& rhs);

 

  Widget& operator=(const Widget& rhs)  // 要复制一个Widget对象,只要

  {                                     // 复制对应的WidgetImpl对象。

   ...                                  // 关于operator=实现的一般信息

   *pImpl = *(rhs.pImpl);               // 参见条目101112

   ...

  }

 

  ...

 

private:

  WidgetImpl *pImpl;                    // 指向包含当前Widget数据的对象

};

为了交换两个Widget对象的值,我们所要做的仅仅是交换他们的pImpl指针,但是默认的swap算法是不可能知道这一切的,它不仅会复制三个Widget对象,同时也会复制三个Widget对象。这样做效率太低了。

我们要做的是告诉std::swap当交换Widget时,执行的交换操作应当仅仅针对它们内部的pImpl指针。有一种精确的说法来描述这一方法:将Widgetstd::swap特化。下面是基本的思想,尽管以这种方式不能通过编译:

 

namespace std {

 

  template<>                       // TWidget时,

  void swap<Widget>(Widget& a,     // 这是std::swap的一个特化版本

                    Widget& b)     // 这段代码不能通过编译

  {

    swap(a.pImpl, b.pImpl);        // 要交换两个Widget

  }                                // 只需要交换它们的pImpl指针

 

}

程序开端的“template<>”告诉我们这是std::swap的一个完全特化模板,函数名后面的“<Widget>”告诉我们当前的特化针对TWidget的情况。换种说法,当一般的swap模板应用于Widget时,应当使用这一具体实现。一般情况下,我们没有权限去改动std名字空间内部的内容,但是我们有权针对我们自己创建的类型(比如Widget)来完整地特化标准模板(就像swap)。这就是我们所要做的。

然而,就像我说过的,这段代码是不能通过编译的。这是因为它尝试访问ab内部的pImpl指针,但是它们是私有的。我们可以将我们的特化函数声明为友元,但这里的规则有些不同:这里要求我们让Widget包含一个名为swap的公共成员函数,让这个swap进行实际的交换工作,然后特化std:swap来调用这一成员函数。

class Widget {                     // 同上,

public:                            // 仅添加了一个swap成员函数

  ...

  void swap(Widget& other)

  {

    using std::swap;               // 本节后面会解释为什么这样声明

 

    swap(pImpl, other.pImpl);      // 交换pImpl指针来交换Widget

  }

  ...

};

 

namespace std {

 

  template<>                       // 特化的std::swap (已修正)

  void swap<Widget>(Widget& a, Widget& b)

  {

    a.swap(b);                     // 要交换Widget

  }                                // 只要调用它们的swap成员函数

 

}

这样的代码不仅仅可以通过编译,而且也与STL容器相协调,它不仅仅提供了公有的swap成员函数,而且还提供了特化的std::swap来调用这些成员函数。

然而,我们不难发现,WidgetWidgetImpl都是类模板,而不是类,似乎我们可以自定义WidgetImpl中保存的数据的类型:

template<typename T> class WidgetImpl { ... };

 

template<typename T> class Widget { ... };

将一个swap成员函数放入Widget中(如果需要,也可以是WidgetImpl)仍然十分简单,但是我们对std::swap特化时将会遇到问题。下面是我们希望编写的代码:

namespace std {

  template<typename T>

  void swap<Widget<T> >(Widget<T>& a, Widget<T>& b)

                                   // 错误!非法代码

  { a.swap(b); }

 

}

这样的代码看上去完美无瑕,但是它是非法的。因为其中尝试对一个函数模板(std::swap)进行不完全的特化,但是,尽管C++允许对类模板进行不完全特化,而函数模板就不行了。这一代码不应通过编译(尽管一些编译器会错误的接受)。

当你期望对一个函数模板进行“不完全特化”时,通常的做法非常简单,就是添加一个该函数的重载。代码可能是下面的样子:

namespace std {

 

  template<typename T>             // std::swap的一个重载

  void swap(Widget<T>& a, Widget<T>& b)

                                   // (注意swap后边没有<...>

  { a.swap(b); }                   // 下文解释了为什么这样做不合法

 

}

一般情况下,重载函数模板是可以的,但是std是一个很特殊的名字空间,它的规则也是独特的。对std中的模板进行完全特化是合法的,但是为std添加一个新的模板却是不合法的(类或函数或其他一切都不可以)。std的内容是由C++标准化委员会一手确定的,我们无法修改他们所规定的任何形式,只能“望码兴叹”。越轨的代码似乎可以运行,但它们的行为却是未定义的。如果你希望你的代码拥有可预知的行为,你就不应该在std中添加新的内容。

那么应该怎么办呢?我们仍然需要一种方法来让其他人通过调用swap来访问我们更加高效的特化版本。答案很简单。我们仍然可以通过声明一个非成员函数swap来调用成员函数swap实现,只要这个非成员函数不是std::swap的特化或者重载版本即可。比如说,如果我们所有与Widget相关的功能都在名字空间WidgetStuff中,那么代码看上去应该是这样:

namespace WidgetStuff {

  ...                              // 模板化的WidgetImpl,等等

 

  template<typename T>             // 同上,包括swap成员函数

  class Widget { ... };

  ...

 

  template<typename T>             // 非成员函数swap

  void swap(Widget<T>& a, Widget<T>& b)

                                   // 不属于std名字空间

  {

    a.swap(b);

  }

}

现在,如果任意位置的代码对两个Widget对象调用了swapC++的名字搜寻守则(更具体地说,就是所谓的参数依赖搜寻或Koenig搜寻)将会在WidgetStuff中查找具体到Widget的版本。这恰恰是我们需要的。

由于这种方法针对类或者类模板可以正常运行,所以看上去似乎我们应该在任何情况下都使用它。但是遗憾的是,我们还是要对于类的std::swap进行特化(稍后会交代理由),所以如果你想要在尽可能多的上下文中(你所需要的)调用具体到类的swap版本,你就需要在你的类所在的名字空间编写一个非成员版本的swap,同时还需要一个std::swap的特化版本。

顺便说一下,即使你没有使用名字空间,上述内容仍然有效(也就是说,你仍需要一个非成员的swap去调用成员函数swap),但是为什么你要把所有的类、模板、函数、枚举类型、enumeranttypedef的名字统统塞进全局名字空间里呢?如果你对编程规范有一点概念的话,都不会这样做的。

到目前为止我所介绍的一切内容都是以swap的作者的角度展开的,但是以一个客户的眼光来审视一下swap也是很有价值的。假设你正在编写一个函数模板,这里你需要交换两个对象的值:

template<typename T>

void doSomething(T& obj1, T& obj2)

{

  ...

  swap(obj1, obj2);

  ...

}

这里应该调用哪一个swap呢? std中存在一个通用版本,这是你所知道的;另外std中可能还有一个针对这一通用版本的特化版本,它可能存在也可能不存在;或者一个模板的版本,它可能存在也可能不存在,它是否在一个名字空间中也不能确定(但可以肯定不在std名字空间中)?此时你所希望的是,如果存在一个模板版本的话,就调用它;如果不存在,就返回调用std中的通用版本。以下是满足这一要求的代码:

template<typename T>

void doSomething(T& obj1, T& obj2)

{

  using std::swap;                 // 确保std::swap在此函数中可用

  ...

  swap(obj1, obj2);                // 为类型T的对象调用最佳的swap

  ...

}

当编译器看到对swap的调用时,它们会寻找恰当的swap来进行调用。C++的名字搜寻原则确保了在全局或T类型所在的名字空间中来查找所有的精确到Tswap。(举例说,如果T是位于WidgetStuff名字空间中的Widget,那么编译器将会使用参数依赖搜寻方式来查找WidgetStuff中的swap。)如果没有精确到Tswap存在,那么编译器将会使用std中的swap,多亏了using声明可以使std::swap在本函数中可见。然而即使这样,编译器也更期望得到一个精确到Tstd::swap的特化版本,而不是未确定类型的模板,因此如果std::swap特化为T版本,那么这一特化的版本将会得到使用。

因此,调用正确的swap十分简单。你所需要关心的事仅仅是不去限制对它的调用,因为如果这样做会使C++如何决定去调用函数的方式受到影响。举例说,如果你用下面的方式调用了swap

std::swap(obj1, obj2);             // 调用swap的错误方法

你强迫编译器仅仅去考虑std中的swap(包括所有的模板特化版本),这样做就排除了得到一个位于其他位置的精确到T版本的swap的可能,即使它是更加合理的。然而,一些进入误区的程序员还是会以这种方式限制swap的调用,这里你就可以看出,为你的类提供一个std::swap的完全特化版本是多么重要:对于那些使用不恰当的编码风格写出的代码(这样的代码也存在于一些标准库的实现当中,如果你感兴趣可以自己编写一些代码,来帮助这样的代码尽可能的提高效率),精确到类的swap实现仍然有效。

此刻,我们已经介绍了默认的swap、成员swap、非成员swapstd::swap的特化版本,以及对swap的调用,现在让我们来做一个总结。

首先,如果对你的类或者类模板使用默认的swap实现能够得到可以接受的效率,你就不需要做任何事情。任何人想要交换你创建的类型的对象时,都会去调用默认的版本,此时可以正常工作。

其次,如果默认的swap实现并不够高效(大多数情况下意味着你的类或模板正在运用pimpl惯用法),请按下面步骤进行:

1. 提供一个公用的swap成员函数,让它可以高效的交换你的类型的两个对象的值。理由将在后面列出,这个函数永远不要抛出异常。

2. 在你的类或模板的同一个名字空间中提供一个非成员的swap。让它调用你的swap成员函数。

3. 如果你正在编写一个类(而不是类模板),要为你的类提供一个std::swap的特化版本。同样让它调用你的swap成员函数。

最后,如果你正在调用swap,要确保使用一条using声明来使std::swap对你的函数可见,然后在调用swap时,不要做出任何名字空间的限制。

文中还有一处欠缺,那就是本文的标题中的敬告:不要让swap的成员函数版本抛出异常。这是因为swap最重要的用途之一就是帮助类(或类模板)来提供异常安全的保证。条目29中详细介绍了这一点,但是这一技术做出了“swap的成员函数版本永远不会抛出异常”这一假设。这一约束仅仅应用于成员函数版本,非成员版本则不受这一限制。这是因为swap的默认版本基于拷贝构造和拷贝赋值,而在一般情况下,这两种函数都可能抛出异常。因此,当你编写一个自定义版本的swap时,在典型情况下你不仅要提供一条更高效的交换对象值的方式,同时你也要提供一个不抛出异常的版本。作为一条一般的守则,这两条swap的特征是相辅相成的,因为高效的swap同时也基于内建数据类型的操作(诸如pimpl惯用法中使用的指针),同时内建数据类型的操作决不会抛出异常。

时刻牢记

在对你的类型使用std::swap时可能会造成效率低下时,可以提供一个swap成员函数。确保你的swap不要抛出异常。

如果你提供了一个swap的成员函数,那么同时要提供一个非成员函数swap来调用这一成员。对于类而言(而不是模板),还要提供一个std::swap的特化版本来调用swap成员函数。

在调用swap时,要为std::swap使用一条using声明,然后在调用swap时,不要做出名字空间的限制。

对用户自定义类型而言,提供std的完全特化版本不成问题,但是决不要尝试在std中添加全新的内容。

posted on 2007-08-02 22:05 ★ROY★ 阅读(1388) 评论(3)  编辑 收藏 引用 所属分类: Effective C++

评论

# re: 【读书笔记】[Effective C++第3版][第25条]最好不要让交换数值函数swap抛出异常  回复  更多评论   

GOOD.
2007-08-03 00:02 | pass86

# re: 【读书笔记】[Effective C++第3版][第25条]最好不要让交换数值函数swap抛出异常  回复  更多评论   

要 up 一下的
2007-08-10 15:29 | 周星星

# re: 【读书笔记】[Effective C++第3版][第25条]最好不要让交换数值函数swap抛出异常  回复  更多评论   

汗,这就我就不会了,我只知道装linux系统一定要有根分区跟swap
2007-09-02 19:24 | 深蓝色的音符

只有注册用户登录后才能发表评论。
网站导航: 博客园   IT新闻   BlogJava   知识库   博问   管理