条款 25:考虑写出一个不抛异常的 swap 函数

Effective C++ 中文版 第三版》读书笔记

** 条款 25:考虑写出一个不抛异常的 swap 函数 **

缺省情况下 swap 动作可由标准程序库提供的 swap 算法完成:

namespace std{ 
    template<typename T> 
    void swap(T& a, T& b) 
    { 
        T temp(a); 
        a = b; 
        b = temp; 
    } 
}

但是对某些类型而言,这些复制动作无一必要:其中主要的就是“以指针指向一个对象,内含真正数据”那种类型。多为 “pimpl 手法”(pointer to implementation 的缩写)。设计 Widget class:

class WidgetImpl 
{ 
public: 
    ... 
protected: 
    ... 
private: 
    int a, b, c; 
    std::vector<double> v; 
    ... 
}; 

class Widget 
{ 
public: 
    Widget(const Widget& rhs); 
    Widget& operator=(const Widget& rhs) 
    { 
        ... 
        *pImpl = *(rhs.pImpl); 
    } 
protected: 

private: 
    WidgetImpl* pImpl; 
};

要置换两个 Widget 对象值,唯一要做的就是置换 pImpl 指针,缺省的 swap 算法不知道这一点。不只复制 3 个 Widget 还复制 3 个 WidgetImpl 对象。非常缺乏效率!

我们可以将 std::swap 全特化:

namespace std{
    template<>
    void swap<Widget>(Widget& a, Widget& b)
    {
        swap(a.pImpl, b.pImpl); //< 不能通过编译,访问 private 成员变量
    }
}

虽然可以把这个特化版本声明为 friend,但这和以往的规矩不太一样,我们可以令 Widget 声明一个 swap 的 public 成员函数做真正的替换工作,然后将 std::swap 特化,令他调用该成员函数:

class Widget 
{ 
public: 
    Widget(const Widget& rhs); 
    Widget& operator=(const Widget& rhs) 
    { 
        ... 
        *pImpl = *(rhs.pImpl); 
    } 

    void swap(Widget& other) 
    { 
        using std::swap; 
        swap(pImpl, other.pImpl); 
    } 

protected: 

private: 
    WidgetImpl* pImpl; 
};

namespace std{ 
    template<> 
    void swap<Widget>(Widget& a, Widget& b) 
    { 
        a.swap(b); 
    } 
}

假设 Widget 和 WidgetImpl 都是 class templates 而非 classes:

template <typename T>
class WidgetImpl{…};

template <typename T>
class Widget{…};

在特化std::swap时:

namespace std{ 
    template<typename T> 
    void swap<Widget<T>>(Widget<T>& a, Widget<T>& b) //wrong!不合法! 
    { 
        a.swap(b); 
    } 
}

我们企图偏特化一个 function template,但是 c++ 中只允许对 class templates 偏特化,在 function template 身上偏特化是行不通的。这段代码不该通过编译(虽然有些编译器错误地接受了它)。

当打算偏特化一个 function template 时,惯常的做法是简单的为它添加一个重载版本:

namespace std{ 
    template<typename T> 
    void swap(Widget<T>& a, Widget<T>& b) //注意“swap后没有<>” 
    { 
        a.swap(b); 
    } 
}

一般而言,重载 function template 没有问题,但 std 是个特殊的命名空间,管理也就比较特殊。客户可以全特化 std 内的 templates,但不可以添加新的 templates(或 class 或 function 或任何其他东西)到 std 里头。其实跨越红线的程序几乎仍可编译执行,但他们行为没有明确定义。所以不要添加任何新东西到 std 里头。

未提供较高效的 template 特定版本。我们还是声明一个 non-member swap 但不再是 std::swap 的特化版本或重载版本:

``cpp
namespace WidgetStuff{
template<typename T>
class Widget{...};

template<typename T> 
void swap(Widget<T>& a, Widget<T>& b) 
{ 
    a.swap(b); 
} 

}


现在,任何地点的任何代码如果打算置换两个 Widget 对象,因而调用 swap,C++ 的名称查找法则就是所谓 “argument-dependent lookup” 会找到 WidgetStuff 内的专属版本。

```cpp
template<typename T> 
void doSomething(T& obj1, T& obj2) 
{ 
    ... 
    swap(obj1, obj2); 
    ... 
}

上面的应该使用哪个 swap?是 std 既有的那个一般化版本还是某个可能存在的特化版本等?你希望应该是调用 T 专属版本,并在该版本不存在的情况下调用 std 内的一般化版本,下面是你希望发生的事:

template<typename T> 
void doSomething(T& obj1, T& obj2) 
{ 
    using std::swap; //< 令 std::swap 在此函数内可用 
    ... 
    swap(obj1, obj2); //< 为 T 型对象调用最佳 swap 版本 
    ... 
}

c++ 的名称查找法则(name lookup rules)确保将找到 global 作用域或 T 所在之命名空间内的任何 T 专属的 swap。如果 T 是 Widget 并且位于命名空间 WidgetStuff 内,编译器会找出 WidgetStuff 内的 swap。如果没有 T 专属的 swap 存在,编译器就是用 std 内的 swap,然而即便如此,编译器还是比较喜欢 std::swap 的 T 专属特化版本,而非一般化的那个 template。

std::swap(obj1,obj2); //< 这是错误的 swap 调用方式

这便强迫编译器只认 std 内的 swap(包括其任何 template 特化),因此不再调用一个定义于他处的较适当 T 专属版本。那正是“你的 classes 对 std::swap 进行全特化的”重要原因:使得类型专属的 swap 实现版本可以被这些迷途代码所用。

如果 swap 的缺省实现码对你的 classes 或 class template 提供可接受的效率,不需要额外做任何事。

如果 swap 缺省实现版效率不足(某种 pimpl):

  1. 提供一个 public swap 成员函数,这个函数绝不该抛出异常。
  2. 在 class 或 template 所在的命名空间内提供一个 non-member swap, 并令他调用上述 swap 成员函数。
  3. 如果正编写一个 class(而非 class template),为你的 class 特化 std::swap。并令他调用 swap 成员函数。

如果调用 swap,确保包含一个 using 声明式,然后不加任何 namespace 修饰符,赤裸裸调用 swap。

成员版 swap 绝不可抛出异常。

唯一还未明确的劝告:成员版本的 swap 决不可抛出异常。那是因为 swap 的一个最好的应用是帮助 classes(class templates)提供强烈的异常安全性保障。(条款 29 对此提供了所有细节)此技术基于一个假设:成员版的 swap 绝不抛出异常。这一约束只施行于成员版!不可施行于非成员版,因为 swap 缺省版本是以 copy 构造函数和 copy assignment 操作符为基础,而一般情况下两者都允许抛出异常。因此当你写一个自定版本的 swap,往往提供的不只是高效置换对象值的办法,而且不抛出异常。一般,这两个 swap 特性是连在一起的,因为高效的 swaps 几乎总是基于对内置类型的操作(例如 pimpl 手法的底层指针),而内置类型上的操作绝不会抛出异常。

** 请记住: **

  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 而言全新的东西。
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 200,045评论 5 468
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 84,114评论 2 377
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 147,120评论 0 332
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 53,902评论 1 272
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,828评论 5 360
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,132评论 1 277
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,590评论 3 390
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,258评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,408评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,335评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,385评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,068评论 3 315
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,660评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,747评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,967评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,406评论 2 346
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,970评论 2 341

推荐阅读更多精彩内容