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;
} 这里utility.swap()是一个泛型函数,作用是交换两个参数:
class utility
...{
public static void swap<T>(ref T lhs, ref T rhs) ...{
T t_=lhs;
lhs=rhs;
rhs=t_;
}
} 如果类有关键性的资源需要释放,那么可以实现IDisposable接口,然后在copy()中使用using:
public void copy(CountStr& cs) ...{
using(CountStr t_=new CountStr(cs))
...{
t_.swap(this);
}
} 如此,对象原有的数据和资源被交换到临时对象t_中之后,在退出using作用域的时候,会立即得到释放。这是RAII的一个应用,详细内容参见本系列的前一篇《C++的营养——RAII》。
swap的基本作用是维持强异常保证语义。但是,作为一种基础性的技术,它还可以拥有更多的用途。下面介绍几种主要的应用,为了节省篇幅,案例直接使用C#,不再给出C++的代码。
在我们的开发过程中,有时需要是一些对象复位,即回复对象的初始状态。一般情况下,我们会在类中增加一个reset()之类的成员,在这个函数中释放资源,恢复各成员数据的初值。但是,在拥有swap的情况下,这种操作变得非常容易:
class X
...{
public X() ...{
... //初始化对象
}
public X(int v) ...{
... //以v初始化对象
}
public void swap(X val) ...{...}
public void reset() ...{
this.swap(new X());
}
...
} reset()用X的默认构造函数创建了一个临时对象,将其内容与this交换,this的内容便成为了初始值。重要的是,这个成员函数也是强异常保证的。如果需要通过一些参数复位,那么同样可以做到:
class X
...{
...
public void reset(int v) ...{
this.swap(new X(v));
}
...
} 有时甚至可以不需要reset这个成员,而直接在代码中使用swap复位一个对象:
X x=new X();
... //对x的操作,改变了内容
x.swap(new X()); //复位了 如果X有资源需要释放,那么只需实现IDispose,然后使用using:
class X : IDisposable
...{
...
public void reset() ...{
using(X t=new X())
...{
this.swap(t);
}
}
public void Dispose() ...{...}
...
} 上面这些应用都有一个共同点,即重新初始化一个对象,使其恢复到一个初始状态。下面的应用,则反其道而行之,将一个对象切换到另一个状态。
有时,我们会做一些类,在构造函数中执行一些复杂的操作,比如解析一个文本文件,然后向外公布解析后的结果。之后,我们需要在这个对象上load另一个文 件,那么通常都写一个load成员函数,先释放掉原先占用的资源,然后再加载新的文件。如果有了swap,那么这个load函数同样极其简单:
class Y : IDisposable
...{
public Y(string filename) ...{
... //打开文件,执行解析
}
public void swap(Y val) ...{...}
public load(string filename) ...{
using(Y t=new Y(filename))
...{
this.swap(t);
}
}
public void Dispose() ...{
... //关闭文件,释放资源
}
} 还有一种情况,有一些类,通过一些数据创建,创建之后在绝大多数的情况下都是只读的,但偶尔会需要改变其内部数据。为了代码的可靠性,我们可以把类写成只读的。但是如何修改其内部的数据呢?也可以通过swap:
class Z
...{
public Z(int a, float b) ...{
m_a=a;
m_b=b;
}
public void swap(Z val) ...{...}
public int a ...{ get...{return m_a;}}
public float b ...{ get...{return m_b;}}
private int m_a;
private float m_b;
}
Z z=new Z(3, 4.5);
z.swap(new Z(5, 5.4)); //z的值已修改 这样便可避免对Z的实例的随意修改。但是,这种修改方式会造成性能损失,特别是数据成员存在非O(1)复制的情况下(如有字符串、数组等),只有在修改偶尔发生的情况下才能使用。
有些类,构造函数需要一些数据初始化对象,并且会创建的过程中会验证其有效性,和执行一些计算。也就是构造函数存在一定的数据逻辑。如果需要修改对象的某 些值,会牵涉到相应的复杂数据逻辑。通常都是把这些逻辑独立在private成员函数中,由构造函数和数据修改操作共享。这样的做法往往不能带来强异常保 证,在构造函数里的数据验证往往会抛出异常。因此,如果使用swap,便可以消除这类问题,并且使代码简化:
class A
...{
public A(int a, string b, Rectangle c) ...{
... //数据逻辑、计算等
}
public int a ...{
set...{ this.swap(new A(value, m_b, m_c));}
}
public string b ...{
set...{ this.swap(new A(m_a, value, m_c));}
}
public Rectangle c ...{
set...{ this.swap(new A(m_a, m_b, value));}
}
...
} 当然,也可以在类外直接进行这样的数据设置:
A a=new A(2, "zzz", Rectangle(1,1, 10,10));
a.swap(new A(3, "zzz", Rectangle(1,1, 10,10))); 这种用法可以用于某些只保存对构造函数参数的计算结果,而不需要保存这些参数的类(m_a,m_b,m_c都不需要了),只是使用上过于琐碎。
所有这些与对象状态设置有关的swap用法,都集中表现了一个特性,即使得我们可以将对象的初始化代码集中在构造函数中,数据和资源清理的代码集中在 Dispose()中。这种做法可以大大提高代码的可维护性。如果一个软件项目中,每个类都实现swap和复制构造函数(除非该类不允许复制),并尽可能 集中数据逻辑代码,那么会使得代码质量有答复的提高。
在上一篇《C++的营养——RAII》中,我提到守卫一个结构复杂的类:在代码中修改一个对象,然后再回复原来的状态。如果单纯手工地保存对象数据,通常 很困难(有时甚至是不可能的),而且也难以维持异常安全性(强异常保证)。但是如果使用了swap,那么将会易如反掌:
void ScopeObject(MyCls obj) ...{
using(MyCls t_=new MyCls(obj))
...{
... //操作obj,改变其状态或数据
obj.swap(t_); //恢复原来的状态
}
} 当然,也可以直接使用t_执行操作,这就不需要执行swap。在一般情况下两者是等价的。但是,在某些特殊情况下,比如类持有特殊资源,或者obj是并发 中的共享对象的时候,两种方法有可能不等价。swap方案使用上更全面些。总的来说相差不多,放在这里仅供参考。
作为更进一步的发展,可以构造一个ISwapable泛型接口:
interface ISwapable<T>
...{
void swap(T v);
} 对于需要实现swap手法的类,实现这个接口:
class B : ISwapable<B>
...{
public B() ...{...}
public void swap(B v) ...{...}
...
} 这将会带来一个好处,通过泛型算法实现某些特定的操作:
class utility
...{
public static void reset<T>(T obj)
where T : ISwapable
where T : new()
...{
obj.swap(new T());
}
} 这样便无须为每一个类编写reset成员函数,只需这一个泛型算法即可:
X x;
Y y;
utility.reset(x);
utility.reset(y);
... swap手法可能在存在其他诸多应用,在编码的时候可以不断地发掘。只需要抓住一个原则:swap可以无抛掷,简洁地修改一个对象的值。swap所带来的 一个问题主要是性能方面。swap通常伴随着临时对象的构造,多数情况下,这种构造不会引发更多的性能损失,但在某些数据修改的应用中,会比直接的数据修 改损失更多的性能。如何取舍,需要根据具体情况分析和权衡。总的来说,swap手法所带来的好处是显而易见的,特别是强异常保证,往往是至关重要的。而诸 如简化代码等的作用,则无需多言,一用便知。
或许swap手法非常基础,非常细小,而且很多人不用swap也过来了。但是,聚沙成塔,每一处细小的优化,积累起来则是巨大的进步。还是刘皇叔说得好:“勿以善小而不为,勿以恶小而为之”。
分享到:
相关推荐
标题中的"swap_1位swap电路_logisim_swap_"指的是一个关于数字逻辑设计中的1位交换(swap)电路,该电路使用逻辑门实现输入信号的交换功能。在电子工程和计算机科学领域,这样的电路通常用于数据处理或计算过程中...
### CentOS清理SWAP交换区内存 #### SWAP分区机制与问题背景 在深入探讨如何清理CentOS中的SWAP交换区之前,我们先了解下SWAP的基本概念及其在系统中的作用。SWAP空间(或称为SWAP分区)是在硬盘上预留的一块区域...
在Linux操作系统中,交换(swap)空间是一种至关重要的内存管理机制。它允许系统在物理RAM不足时,将部分内存中的数据暂时存储到硬盘上,从而腾出物理内存供其他进程使用。标题"swap_state.rar_swap"暗示我们关注的...
### Linux 修改 Swap 大小详解 #### 一、Swap 分区概述 在 Linux 系统中,Swap 分区(或文件)充当一种辅助内存的角色。当系统的物理内存不足时,Linux 会将一部分暂时不用的数据从内存中移出并保存到 Swap 分区,...
在Linux操作系统中,swap分区是一种特殊的分区类型,用于充当系统的虚拟内存。当物理RAM不足时,Linux会将部分数据暂时存储到swap分区中,以缓解内存压力。然而,有时在安装Linux后,可能会发现预设的swap分区大小不...
在深入探讨"openwrt swap启用脚本"这一主题前,我们先来了解下几个关键概念:OpenWRT、Swap以及脚本。 ### OpenWRT OpenWRT是一款基于Linux的开源固件项目,主要用于无线路由器和其他嵌入式设备。它提供了一个可...
### SUSE上扩容Swap空间方法详解 #### 一、引言 在Linux系统中,Swap空间作为虚拟内存的一部分,对于提升系统性能具有重要作用。当物理内存不足时,系统会将部分不常用的数据移动到Swap空间中,从而释放物理内存供...
在Windows操作系统中,浏览Linux系统的EXT(Extended)和SWAP分区通常需要借助第三方软件,因为Windows内核本身并不支持这两种文件系统。EXT是Linux的主要文件系统类型,包括EXT2、EXT3、EXT4等,而SWAP分区则作为...
在深入探讨如何增大swap分区之前,我们先来理解一下什么是swap分区以及它在系统中的作用。在计算机系统中,特别是Linux环境下,swap分区扮演着虚拟内存的角色。当系统的物理内存(RAM)不足时,操作系统会将一部分不...
标题"swap color.rar_dug9ab_swap_swapping与swap"可能指的是一个关于颜色交换的项目,其中可能包含了对背景和前景颜色进行交换的算法或应用。"dug9ab"可能是项目的一个特定标识符,而"swapping与swap"则强调了这个...
### Linux新建和增加swap分区 #### 一、概述 在Linux系统中,swap分区的作用类似于Windows系统的虚拟内存,它能够提供额外的存储空间,当物理内存不足时,系统会将部分不活跃的数据移动到swap分区中,从而释放物理...
Linux 扩展 SWAP 分区和根目录空间不足解决方案 Linux 系统中的 SWAP 分区和根目录空间不足是一个常见的问题,对于服务器和计算机的性能和稳定性造成了影响。解决这个问题的方法有多种,本文将介绍如何扩展根目录...
在 Solaris 操作系统中,Swap空间是一种至关重要的资源,它扮演着虚拟内存的角色,用于扩展物理内存的容量。当物理内存不足时,系统会将部分数据移到Swap空间,以便腾出内存供其他进程使用。Swap空间的管理是系统...
Linux下的swap分区是操作系统管理内存的一种机制,尤其在物理内存有限的情况下显得尤为重要。Swap分区的主要作用在于,当系统的物理内存不足时,它可以充当一个临时的扩展内存,将暂时不活跃的程序数据存储到硬盘上...
### 如何动态增加Linux系统的Swap分区 在Linux系统中,Swap分区的作用类似于Windows系统中的虚拟内存,当物理内存不足时,系统会将一部分暂时不用的数据交换到Swap分区中,从而释放物理内存空间供其他进程使用。这...
用于查看LINUX下进程占用SWAP大小
交换分区swap 交换分区swap是计算机操作系统中的一种机制,用于在内存不足时临时存储数据,以避免系统崩溃或性能下降。swap交换空间是指系统中的一块专门用于交换数据的存储区域。 以下是关于交换分区swap的知识点...
Linux 配置 SWAP 分区指南 在 Linux 系统中,SWAP 分区扮演着非常重要的角色,它可以帮助系统在物理内存不足时提供额外的虚拟内存空间,从而确保系统的稳定运行。然而,在实际系统使用过程中,我们可能会遇到没有...
"如何增加swap分区的大小" 在 Linux 系统中,swap 分区是系统的虚拟内存,它可以根据需要临时分配物理内存,从而提高系统的性能。但是,如果 swap 分区太小,可能会导致系统出现问题,例如内存溢出、程序崩溃等。在...
### 两种添加Swap分区的方法 #### 一、背景与意义 在Linux系统中,Swap分区是一种虚拟内存技术,用于在物理内存不足时将一部分数据临时存放到硬盘上,以达到扩展内存的效果。对于刚安装完的Linux系统而言,如果...