在C++中写一个Swap函数

swap函数几乎是所有初学者都写过的一个最基本的函数之一,通常是用它来了解函数概念、形参与实参、引用和指针。然而,这个极为基础的函数却有着非常重要的作用。正因为它的重要性、普遍性,意味着swap函数必须有着极高的安全性,因此如何写一个不抛出异常的swap函数是本章讨论的主题。

普通的Swap函数

让我们先看看一个例子:

void swap(int a, int b)
{
    int swap = a;
    a = b;
    b = swap;
}

这是最常见的一种错误,ab是一个int类型的值,它们只是实参的一个拷贝,作用域只在swap函数内,所以无法起到交换的作用。

这是一个最普遍的swap函数实现:

void swap(int& a, int& b)
{
    int swap = a;
    a = b;
    b = swap;
}

通过使用引用来保证swap函数中对参数做出的改变传递到swap的作用域外。

还有一个取巧的办法:

void swap(int& a, int& b)
{
    a = a + b;   // a + b
    b = a - b;   // a + b - b = a
    a = a - b;   // a + b - a = b
}

这个函数通过对ab的一系列运算从而避免了申请新的变量作为中间容器,节省了空间。然而这种做法实在是得不偿失,因为它的毛病实在太多了:

  1. 只适用于内置类型 由于使用了加减运算,这中做法对于类类型的变量来说并不能使用(除非类重载了+-运算符)。
  2. 节省空间可以说是毫无卵用 因为这种方式一般只使用于内置类型,而现在的计算机大多都是64位,内置类型最多占用8个字节,这点内存对于现在的机器来说不算什么,通过3个加减运算来替换它,得不偿失。
  3. 可能出现数据溢出或损失精度现象 举个栗子:int类型变量的范围是-2^31~2^31-1 ,当参数为int类型时,如果ab都为2^31-1时,第一步a = a + b就会造成数据溢出。如果参数的类型为floatdouble时,对其进行基本运算会造成精度的损失。

上述swap函数的另一个版本:

void swap(int* a, int* b)
{
    *a = *a + *b;
    *b = *a - *b;
    *a = *a - *b;
}

这是一种更恶劣的做法,这个函数除了上述3个缺点之外,还有一个缺点:当指针ab指向同一个地址,也就是使用了别名的时候,产生的结果就是*a为原来的两倍,而*b等于0。

这是一个通过位运算而且不需要额外的存储空间来实现swap函数的方法:

void swap(int a, int b)
{
    a = a ^ b;      // a ^ b
    b = a ^ b;      // a ^ b ^ b = a
    a = a ^ b;      // a ^ b ^ a = b
}

类和容器的swap函数

标准程序库中提供了缺省的swap函数,其实现与我们所预料的大致相同:

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

只要T拥有copy构造函数,这个缺省的swap函数就能工作。然而它只能够助你淌过小溪,但是要跨过太平洋,它就有点不够看了。如果对象中含有大量数据,这种方法就要进行三次长时间的赋值运算,效率极其低下,尤其是当置换的类是以pimpl手法实现的时候。

注:pimpl手法(pointer to implementation)是一种将对象实现的细节通过一个私有指针成员进行隐藏的常用手法,可以使接口与实现分离,解除它们之间的耦合关系,来降低文件间的编译依赖性。

一个pimpl手法的栗子:

class BoxImpl
{
public:                 // 细节不重要
    ...
private:
    string name;            // 包含大量数据
    string id;
    std::vector<int> vec;
    ...
};

class Box
{
public:
    Box(const Box& x);          // copy构造函数
    Box& operator=(const Box& x)
    {
        ...
        *pImpl = *(x.pImpl);        // 重载'='操作符,复制Box的实现
        ...
    }
    ...
private:
    BoxImpl* pImpl;             // 指向Box的实现
};

正如我们所想的,一旦我们想要置换两个Box对象,除了复制三个Box外,还要复制三个BoxImpl,所以我们需要另一种方法来实现swap函数。现在有一个好主意,如果我们生成一个特化版本的std::swap函数只调换pImpl指针(我们不能改变std命名空间里的任何东西,但是我们可以为标准模板添加特化版本),那么我们在调用swap函数的时候会对Box对象使用我们定制的方法,而对其他对象使用缺省的方法:

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

然而尴尬的是这个函数并不能编译,因为pImpl指针是Box的私有成员,所以我们需要在Box类中添加一个public的接口来隐藏对私有成员的调用:

class Box {
public:
    ...
    void swap(Box& x)
    {
        using std::swap;        // 稍后解释为什么这么做
        swap(pImpl, x.pImpl);       // 置换pImpl指针
    }
    ...
};

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

这种做法与STL容器对swap的实现有着一致性,STL容器通过std::swap的特化版本来调用容器的public swap成员函数,而这个成员函数只是置换数据的指针。

我们完成了目标,但是又发现如果我们要置换的不是class而是class templates,那么我们swap函数又不够看了。读者可能觉得很烦,我们仅仅只是实现一个小小的swap函数而已,为什么要兼容这么多东西啊!!!你变了,你不再是从前那个纯洁质朴的swap函数了( TДT)然而为了效率这样做还是值得的。。。

所以我们又对std::swap偏特化:

namespace std {
    template<typename T>    
    void swap< Box<T> >(Box<T>& a, Box<T>& b)
    {
        a.swap(b);          // 错误!这样做并不能通过编译。
    }
}

这样做看起来很合理,但是C++只允许对类模板进行偏特化,而不支持对函数模板的偏特化操作Orz。于是聪明的你又想出了一个好主意,重载swap函数:

namespace std {
    template<typename T>
    void swap(Box<T>& a, Box<T>& b)
    {
        a.swap(b);          // 依然错误!
    }
}

这里出现的问题我之前已经提到过来,我们可以向std命名空间中添加特化的std模板,而不能添加新的模板。

注:如果我们这也做程序可能还是会通过编译并运行,但是它们的行为并没有明确的定义,所以请不要在std中添加任何新东西。

我们所想到的两个实现方法都被否决了,不过天无绝人之路,我们可以声明一个非成员swap函数来调用类模板的swap成员函数,但并不将它声明为std::swap函数的特化或重载版本。我们将所有与Box相关的机能都置于命名空间BoxTool中(我们也可以置于global命名空间中,但是大量的类、模板、方法等充斥在你的global命名空间中会让你的代码很不优雅):

namespace BoxTool {
    ...                 // 模板化的BoxImpl和Box等的实现
    template<typename T>
    void swap(Box<T>& a, Box<T>& b)
    {
        a.swap(b);          // 该函数没有在std命名空间中
    }                   // 不用担心std::swap的特化或重载问题
}

于是我们可以放心地置换Box对象了。但这时你会发现这种方法好像对类也样适用,所以一直用这种方法岂不是更方便?然而并不是这样。如果你想让你的对某个类特化的swap函数在尽可能多的语境下调用,就需要在该类所在的命名空间中添加一个非成员的swap函数以及一个std::swap函数。

好了,终于写好了swap函数,我们可以来使用它了\(>0<)/:

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

然而swap会调用哪一个版本呢?std中的一般化版本?特化的std::swap版本?类所在命名空间中的那个swap版本?懵逼了(◎_◎;)我们所希望的是调通T的专属版本,如果不存在则调用一般的std::swap函数:

template<typename T>
void doSomething(T& a, T& b)
{
    using std::swap;        // 令std::swap在函数中可用
    ...
    swap(a, b);         // 不能这样:std::swap(a, b);
    ...
}

通过C++的名称查找规则,确保我们所使用的swap函数是最合适的那个。不过编译器还是会比较喜欢自家人,所以偏向于使用std中的特化版本而不是T所在的命名空间中的模板函数。

我们讨论了这么多swap函数的实现,为自己找到一个合适的swap函数吧(●'◡'●)ノ♥

本文参考自《Effective C++》第25条

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

推荐阅读更多精彩内容