021 交换操作

除了定义拷贝控制成员,管理资源的类通常还定义一个名为 swap 的函数。对于那些与重排元素顺序的算法一起使用的类,定义 swap 是非常重要的。这类算法在需要交换两个元素时会调用 swap。

如果一个类定义了自己的 swap,那么算法将使用类自定义版本。否则,算法将使用标准库定义的 swap。

编写我们自己的 swap 函数

class HasPtr
{
public:
    HasPtr(const std::string &s = std::string()) : ps(new std::string(s)), i(0),
        use_count(new std::size_t(1))
    {}
    HasPtr(const HasPtr &p) : ps(p.ps), i(p.i), use_count(p.use_count)
    {
        ++*use_count;
    }
    ~HasPtr();
    HasPtr& operator=(const HasPtr&p);

    friend void swap(HasPtr& lh, HasPtr& rh);

private:
    std::string *ps;
    int i;
    std::size_t *use_count;
};

inline void swap(HasPtr &lh, HasPtr &rh)
{
    using std::swap;
    swap(lh.ps, rh.ps);
    swap(lh.i, rh.i);
}

与拷贝控制成员不同,swap 并不是必要的。但是,对于分配了资源的类,定义 swap 可能是一种很重要的优化手段。

swap 函数应该调用 swap,而不是 std::swap

此代码中有一个很重要的微妙之处:虽然这一点在这个特殊的例子中并不重要,但在一般情况下它非常重要——swap 函数中调用的 swap 不是 std::swap。在本例中,数据成员是内置类型的,而内置类型是没有特定版本的 swap 的,所以在本例中,对 swap 的调用会调用标准库 std::swap。

但是,如果一个类的成员有自己类型特定的 swap 函数,调用 std::swap 就是错误的了。例如,假定我们有个名为 Foo 的类,它有一个类型为 HasPtr 的成员 h。如果我们未定义 Foo 版本的 swap,那么就会使用标准库版本的 swap。如我们所见,标准库 swap 对 HasPtr 管理的 string 进行了不必要的拷贝。

我们可以为 Foo 编写一个 swap 函数,来避免这些拷贝。但是,如果这样编写 Foo 版本的 swap:

inline void swap(Foo& lh, Foo& rh) {
    // 错误:这个函数使用了标准库版本的 swap,而不是 HasPtr 版本
    std::swap(lh.h, rh.h);
    // 交换其他成员
}

使用此版本与简单使用默认版本的 swap 并没有任何性能差异。虽然我们显示地调用了标准库版本的 swap。但是,我们不希望 std 中的版本,我们希望调用为 HasPtr 对象定义的版本。

因此,正确的 swap 函数如下所示:

inline void swap(Foo& lh, Foo& rh) {
    using std::swap;
    swap(lh.h, rh.h);
    // 交换其他成员
}

每个 swap 调用应该都是未加限定的。即,每个调用都应该是 swap,而不是 std::swap。如果存在类型特定的 swap 版本,其匹配程度会由于 std 中定义的版本。

在赋值运算符中使用 swap

定义 swap 的类通常用 swap 来定义它们的赋值运算符。这些运算符使用了一种名为 拷贝并交换 (copy and swap)的技术。这种技术将左侧运算对象与右侧运算对象的一个副本进行交换:

HasPtr &HasPtr::operator=(HasPtr p)
{
    swap(*this, p);
    return *this;
}

在这个版本的赋值运算符中,参数不再是引用,而是传值。因此,p 是右值运算符对象的一个副本。参数传递时拷贝 HasPtr 的操作会分配该对象的 string 的一个新副本。

在赋值运算符的函数体中,我们调用 swap 来交换 p 和 *this 中的数据成员。这个调用将左侧运算对象中原来保存的指针存入 p 中,并将 p 中原来的指针存入 *this 中。因此,在 swap 调用之后,*this 中的指针成员将指向新分配的 string —— 右侧运算对象中 string 的一个副本。

当赋值运算符结束时,p 被销毁,HasPtr 的析构函数将执行。此析构函数 delete p 现在指向的内存,即,释放掉左侧运算对象中原来的内存。

这个技术自动处理了自赋值的情况并且是异常安全的。它通过在改变左侧运算对象之前拷贝右侧运算对象保证了自赋值的正确,这与我们在原来的赋值运算符中使用的方法是一致的。它保证异常安全的方法也与原来的赋值运算符实现一样。代码中唯一可能抛出异常的是拷贝构造函数中的 new 表达式。如果真的发生了异常,它也会在我们改变左侧运算对象之前发生。

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

推荐阅读更多精彩内容