C++的营养——swap手法

longshanks 2008-02-26 03:20:26
C++的营养

莫华枫

上一篇《C++的营养——RAII》中介绍了RAII,以及如何在C#中实现。这次介绍另一个重要的基础技术——swap手法。
swap手法
swap手法不应当是C++独有的技术,很多语言都可以实现,并且从中得到好处。只是C++存在的一些缺陷迫使大牛们发掘,并开始重视这种有用的手法。这个原本被用来解决C++的资源安全和异常保证问题的技术在使用中逐步体现出越来越多的应用,有助于我们编写更加简洁、优雅和高效的代码。
接下来,我们先来和swap打个招呼。然后看看在C#里如何玩出swap。最后展示swap手法的几种应用,从中我们将看到它是如何的可爱。
假设,我要做一个类,实现统计并保存一个字符串中字母的出现次数,以及总的字母和数字的个数。
class CountStr
...{
public:
explicit CountStr(std::string const& val)
:m_str(val), m_nLetter(0), m_nNumber(0) ...{
do_count(val);
}
CountStr(CountStr const& cs)
:m_str(cs.m_str), m_counts(cs.m_counts)
, m_nLetter(cs.m_nLetter), m_nNumber(cs.m_nNumber)
...{}
void swap(CountStr& cs) ...{
std::swap(m_str, cs.m_str);
m_counts.swap(m_str);
std::swap(m_nLetter, cs.m_nLetter);
std::swap(m_nNumber, cs.m_nNumber);
}
private:
std::string m_str;
std::map<char, int> m_counts;
int m_nLetter;
int m_nNumber;
}
在类CountStr中,定义了swap成员函数。swap接受一个CountStr&类型的参数。在函数中,我们可以看到一组函数调用,每一个对应一个数据成员,其任务是将相对应的数据成员的内容相互交换。此处,我使用了两种调用,一种是使用std::swap()标准函数,另一种是通过 swap成员函数执行这个交换。一般情况下,std::swap()通过一个临时变量实现对象的内容交换。但对于string、map等非平凡的对象,这种交换会引发至少三次深拷贝,其复杂度将是O(3n)的,性能极差。因此,标准库为这些类定义了swap成员函数,通过成员函数可以实现O(1)的交换操作。同时将std::swap()针对这些拥有swap()成员函数的标准容器特化,使其可以直接使用swap()成员函数,而避免性能损失。但是,对于那些拥有swap()成员,但没有被特化的用户定义,或第三方的类,则不应使用std::swap(),而改用swap()成员函数。所以,一般情况下,为了避免混淆,对于拥有swap()成员函数的类,调用swap(),否则调用标准std::swap()函数。
顺便提一下,在未来的C++0x中,由于引入了concept机制,可以允许一个函数模板自动识别出所有“具有swap()成员”的类型,并使用相应的特化版本。这样便只需使用std::swap(),而不必考虑是什么样的类型了。
言归正传。这里,swap()成员函数有两个要求,其一是复杂度为O(1),其二是具备无抛掷的异常保证。前者对于性能而言至关重要,否则swap操作将会由于性能问题而无法在实际项目中使用。对于后者,是确保强异常保证(commit or rollback语义)的基石。要达到这两个要求,有几个关键要点:首先,对于类型为内置类型或小型POD(8~16字节以内)的成员数据,可以直接使用 std::swap();其次,对于非平凡的类型(拥有资源引用,复制构造和赋值操作会引发深拷贝),并且拥有符合上述要求的swap()成员函数的,直接使用swap()成员函数;最后,其余的类型,则保有其指针,或智能指针,以确保满足上述两个要求。
听上去有些复杂,但在实际开发中做到并不难。首先,尽量使用标准库容器,因为标准库容器都拥有满足两个条件的swap()成员。其次,在编写的每一个类中实现满足两个条件的swap()成员。最后,对于那些不具备swap()成员函数的第三方类型,则使用指针,最好是智能指针。(也就是Sutter所谓的 PImpl手法)。只要坚持这些方针,必能收到很好的效果。
下面,就来看一下这个swap()的第一个妙用。假设,这个类需要复制。通常可以通过operator=操作符,或者copy(或其他有明确的复制含义的)成员函数实现,这两者实际上是等价的,只是形式不同而已。这里选择operator=,因为它比较C++:)。
最直白的实现方式是这样:
class CountStr
...{
public:
...
CountStr& operator=(CountStr& val) ...{
m_str=val.m_str;
m_counts=val.m_counts;
m_nLetter=val.m_nLetter;
m_nNumber=val.m_nNumber;
}
...
}
很简单,但是不安全,或者说没有满足异常保证。
先解释一下异常保证。异常保证有三个级别:基本保证、强异常保证和无抛掷保证。基本保证是指异常抛出时,程序的各个部分应当处于有效状态,不能有资源泄漏。这个级别可以轻而易举地利用RAII确保,这在前一篇已经展示过了。强异常保证则更加严格,要求异常抛出后,程序非但要满足基本保证,其各个部分的数据应保持原状。也就是要满足“Commit or Rollback”语义,熟悉数据库的人,可以联想一下Transaction的行为。而无抛掷保证要求函数在任何情况下都不会抛出异常。无抛掷保证不是说用一个catch(...)或throw()把异常统统吞掉。而是说在无抛掷保证的函数中的任何操作,都不会抛出异常。能满足无抛掷保证的操作还是很多的,比如内置POD类型(int、指针等等)的复制,swap手法便以此为基础。(多说一句,用catch(...)吞掉异常来确保无抛掷并非绝对不行,在特定情况下,还是可以偶尔一用。不过这等烂事也只能在西构函数中进行,而且也只有在迫不得已的情况下用那么一下)。
如果这四个赋值操作中,任意一个抛出异常,便会退出这个函数(操作符)。此时,至少有一个成员数据没有正确修改,而其他的则全部或部分地发生改变。于是,一部分成员数据是新的,另一部分是旧的,甚至还有一些是不完全的。这在软件中往往会引发很多令人苦恼的bug。无论如何,此时应当运用强异常保证,使得数据要么是新的值,要么没有改变。那么如何获得强异常保证?在swap()的帮助下,惊人的简单:
class CountStr
...{
public:
...
CountStr& operator=(CountStr& val) ...{
swap(CountStr(val)); // 或者CountStr(val).swap(*this);
raturn *this;
}
...
}
我想世上没有比这等代码更加漂亮的了吧!不仅仅具有简洁动人的外表,而且充满了丰富的内涵。这就叫优雅。不过,优雅之下还需要一些解释。在这两个版本中,都是先用复制构造创建一个临时对象,这个临时对象同传入的参数对象拥有相同的值。然后用swap()成员函数将this对象的内容与临时对象交换。于是, this对象拥有了临时对象的值,也就是与传入的实参对象具有相同的值(复制)。当退出函数的时候,临时对象销毁,自然而然地释放了this对象原先的资源(前提是CountStr类实现了RAII)。
那么抛出异常的情况又是怎样的呢?
先来看看operator=里执行了哪些步骤,并考察这些步骤的异常抛掷的情况。如果将代码改写成另一个等价的形式,就很容易理解了:
CountStr& operator=(CountStr& val) ...{
CountStr t_(val); //此处可能抛出异常,但只有t_的值发生变化
t_.swap(*this); //由于swap拥有无抛掷保证,所以不会抛出异常
return *this;
}
在构造临时对象的时候,可能会抛出异常,因为此时执行了数据的复制和构造。请注意,这时候this对象的内容没有改变。如果此时抛出异常,数据发生改变的只有t_,this对象并未受到影响。而随着栈清理,t_也将被析构,在RAII的作用下,t_所占用的资源也会依次释放。而下一步,swap()成员的调用,则是无抛掷保证的,不会抛出异常,this的内容可以得到充分地、原子地交换,不会发生数据值修改一半的情况。
在C#中,实现swap非常容易,甚至比C++更容易。因为在C#中,大部分对象都在堆上,代码中定义的所谓对象实际上是引用。对于引用的赋值操作是无抛掷的,因此在C#中可以采用同C++几乎一样的代码实现swap:
class CountStr
...{
public CountStr(string val) ...{
m_str=val;
m_nLetter=0;
m_nNumber=0;
do_count(val);
}
public CountStr(CountStr cs) ...{
m_str=new string(cs.m_str);
m_counts=new Dictionary<char, int>(cs.m_counts);
m_nLetter=cs.m_nLetter;
m_nNumber=cs.m_nNumber
}

public void swap(CountStr& cs) ...{
utility.swap(ref m_str, ref cs.m_str);
utility.swap(ref m_counts, ref cs.m_counts);
utility.swap(ref m_nLetter, ref cs.m_nLetter);
utility.swap(ref m_nNumber, ref cs.m_nNumber);
}
public void copy(CountStr& cs) ...{
this.swap(new CountStr(cs));
}

private string m_str;
private Dictionary<char, int> m_counts;
private int m_nLetter;
private int m_nNumber;
}
...全文
1134 45 打赏 收藏 转发到动态 举报
写回复
用AI写文章
45 条回复
切换为时间正序
请发表友善的回复…
发表回复
jianxixiao_c 2008-04-13
  • 打赏
  • 举报
回复
太感谢了,我正在学C++,thank you!
greenkiller 2008-03-01
  • 打赏
  • 举报
回复
存着等看得懂了再来看
wpalhm 2008-02-29
  • 打赏
  • 举报
回复
jf
xieqidong 2008-02-29
  • 打赏
  • 举报
回复
谢谢,接分。
yydrewdrew 2008-02-29
  • 打赏
  • 举报
回复
谢谢楼主
homesos 2008-02-29
  • 打赏
  • 举报
回复
强 收藏!
longshanks 2008-02-29
  • 打赏
  • 举报
回复
std::swap()是怎么保证无抛掷异常的?
===================================
首先,如何执行swap?最简单的方法就是通过一个临时中间变量过度:
A t(a);
a=b;
b=t;
这里有三个复制操作,那么如何保证这三个复制操作无抛掷的呢?
先得找出在什么样的情况下,复制操作不会抛出异常。最简单的,内置类型的赋值和复制都是无抛掷的,比如int、float、指针等等。于是,我们只需要在swap函数中只使用这些内置类型,便可以确保是无抛掷的。
那么需要交换两个复制操作可能抛出异常的对象,怎么办呢?依然是依赖于内置类型的交换操作。这里,指针起到了关键的作用。由于指针的赋值操作是无抛掷的,那么只需让一个类持有一个对象的指针,便可以对此对象实施无抛掷的交换,即交换他们的指针。
标准容器,如vector等,都持有一个指向内存缓冲的指针,届时只需交换两者的指针即可。标准容器的swap成员便是以此为基础,保证无抛掷。
std::swap()都针对拥有swap的标准容器进行了特化,所以会直接调用相应的swap成员函数。而内置类型的赋值操作也是无抛掷的,那么可以直接使用std::swap()。对于那些带有swap成员的类,现有的std::swap()无能为力,应该直接使用swap成员。未来,C++0x中引入concept后,swap可以针对“拥有swap成员”这一类类型特化,从而解决这个问题。对于那些没有swap成员,但有无法保证复制操作无抛掷的,只能持有他们的指针,通过交换指针实现swap。当然,应当鼓励使用智能指针,而智能指针本身就拥有O(1)和non-throw的swap成员。
hslinux 2008-02-28
  • 打赏
  • 举报
回复
看得眼花。
longshanks 2008-02-28
  • 打赏
  • 举报
回复
vitin总结的好,高屋建瓴啊。
我现在编程已经养成习惯了,凡是类都至少一个复制构造函数,一个swap。或许很多时候用不上,如果需要了,那就方便了。这样写程序一劳永逸。反过来,如果原本没有,一旦需要了,再改程序那就烦了。我甚至觉得语言本身都可以提供这样一个O(1)和non-throw的内置swap操作,我们可以省去很多事情。
swap的顺序正如vitin所说的,是没有关系的。除非存心把swap的语义搞成不对称的,这我就无语了。

to Kenmark:
这两个文章原本是为了向我身边的一些C#er们介绍一些有用的惯用法,提高代码质量。觉得有用,就补充完整,贴出来。
目录还没有,我现在是脚踩西瓜皮。下面写什么,还没准主意呢。:)
C++有很多基础的东西。但是我这个系列打算写那些在C++中和其他语言中都很有价值的技术,须得在其他语言中也能实现的。这就比较难找了。诸位有什么建议吗?
yydrewdrew 2008-02-28
  • 打赏
  • 举报
回复
菜鸟请教一下:
std::swap()是怎么保证无抛掷异常的?
rickymathit 2008-02-27
  • 打赏
  • 举报
回复
class CountStr
...{
public:
...
CountStr& operator=(CountStr& val) ...{
swap(CountStr(val)); // 或者CountStr(val).swap(*this); ------->应该是(*this).swap(CountStr(val));
raturn *this;
}
...
}
还有这个
CountStr& operator=(CountStr& val) ...{
CountStr t_(val); //此处可能抛出异常,但只有t_的值发生变化
t_.swap(*this); //由于swap拥有无抛掷保证,所以不会抛出异常 ---------->(*this).swap(t_);
return *this;
}
有点问题吧
你那样把*this放在后面是把*this的值赋给临时对象,那样做没有意义,这样的赋值操作没有赋值成功,不知道有没有道理
makewater 2008-02-27
  • 打赏
  • 举报
回复
xiexie
ENO_REZ 2008-02-27
  • 打赏
  • 举报
回复
mark
rczjp 2008-02-27
  • 打赏
  • 举报
回复
强人贴 挂号..
ouyh12345 2008-02-27
  • 打赏
  • 举报
回复
学习
koko1998 2008-02-27
  • 打赏
  • 举报
回复
呵呵,牛,可以给个签名么!哈哈
星羽 2008-02-27
  • 打赏
  • 举报
回复
liufangbj 2008-02-27
  • 打赏
  • 举报
回复
xiexie~~~~
hxxwcc 2008-02-27
  • 打赏
  • 举报
回复
mark
systemthink 2008-02-27
  • 打赏
  • 举报
回复
学习..
加载更多回复(25)

64,637

社区成员

发帖
与我相关
我的任务
社区描述
C++ 语言相关问题讨论,技术干货分享,前沿动态等
c++ 技术论坛(原bbs)
社区管理员
  • C++ 语言社区
  • encoderlee
  • paschen
加入社区
  • 近7日
  • 近30日
  • 至今
社区公告
  1. 请不要发布与C++技术无关的贴子
  2. 请不要发布与技术无关的招聘、广告的帖子
  3. 请尽可能的描述清楚你的问题,如果涉及到代码请尽可能的格式化一下

试试用AI创作助手写篇文章吧