C++的营养
莫华枫
上一篇《C++的营养——RAII》中介绍了RAII,以及如何在C#中实现。这次介绍另一个重要的基础技术——swap手法。
swap手法
swap手法不应当是C++独有的技术,很多语言都可以实现,并且从中得到好处。只是C++存在的一些缺陷迫使大牛们发掘,并开始重视这种有用的手法。这 个原本被用来解决C++的资源安全和异常保证问题的技术在使用中逐步体现出越来越多的应用,有助于我们编写更加简洁、优雅和高效的代码。
接下来,我们先来和swap打个招呼。然后看看在C#里如何玩出swap。最后展示swap手法的几种应用,从中我们将看到它是如何的可爱。
假设,我要做一个类,实现统计并保存一个字符串中字母的出现次数,以及总的字母和数字的个数。
classCountStr
...{
public:
explicitCountStr(std::stringconst&val)
:m_str(val),m_nLetter(0),m_nNumber(0)...{
do_count(val);
}
CountStr(CountStrconst&cs)
:m_str(cs.m_str),m_counts(cs.m_counts)
,m_nLetter(cs.m_nLetter),m_nNumber(cs.m_nNumber)
...{}
voidswap(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::stringm_str;
std::map<char,int>m_counts;
intm_nLetter;
intm_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++:)。
最直白的实现方式是这样:
classCountStr
...{
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()的帮助下,惊人的简单:
classCountStr
...{
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)...{
CountStrt_(val);//此处可能抛出异常,但只有t_的值发生变化
t_.swap(*this);//由于swap拥有无抛掷保证,所以不会抛出异常
return*this;
}
在构造临时对象的时候,可能会抛出异常,因为此时执行了数据的复制和构造。请注意,这时候this对象的内容没有改变。如果此时抛出异常,数据发生改变的 只有t_,this对象并未受到影响。而随着栈清理,t_也将被析构,在RAII的作用下,t_所占用的资源也会依次释放。而下一步,swap()成员的 调用,则是无抛掷保证的,不会抛出异常,this的内容可以得到充分地、原子地交换,不会发生数据值修改一半的情况。
在C#中,实现swap非常容易,甚至比C++更容易。因为在C#中,大部分对象都在堆上,代码中定义的所谓对象实际上是引用。对于引用的赋值操作是无抛掷的,因此在C#中可以采用同C++几乎一样的代码实现swap:
classCountStr
...{
publicCountStr(stringval)...{
m_str=val;
m_nLetter=0;
m_nNumber=0;
do_count(val);
}
publicCountStr(CountStrcs)...{
m_str=newstring(cs.m_str);
m_counts=newDictionary<char,int>(cs.m_counts);
m_nLetter=cs.m_nLetter;
m_nNumber=cs.m_nNumber
}
publicvoidswap(CountStr&cs)...{
utility.swap(refm_str,refcs.m_str);
utility.swap(refm_counts,refcs.m_counts);
utility.swap(refm_nLetter,refcs.m_nLetter);
utility.swap(refm_nNumber,refcs.m_nNumber);
}
publicvoidcopy(CountStr&cs)...{
this.swap(newCountStr(cs));
}
privatestringm_str;
privateDictionary<char,int>m_counts;
privateintm_nLetter;
privateintm_nNumber;
}
这里utility.swap()是一个泛型函数,作用是交换两个参数:
classutility
...{
publicstaticvoidswap<T>(refTlhs,refTrhs)...{
Tt_=lhs;
lhs=rhs;
rhs=t_;
}
}
如果类有关键性的资源需要释放,那么可以实现IDisposable接口,然后在copy()中使用using:
publicvoidcopy(CountStr&cs)...{
using(CountStrt_=newCountStr(cs))
...{
t_.swap(this);
}
}
如此,对象原有的数据和资源被交换到临时对象t_中之后,在退出using作用域的时候,会立即得到释放。这是RAII的一个应用,详细内容参见本系列的前一篇《C++的营养——RAII》。
swap的基本作用是维持强异常保证语义。但是,作为一种基础性的技术,它还可以拥有更多的用途。下面介绍几种主要的应用,为了节省篇幅,案例直接使用C#,不再给出C++的代码。
在我们的开发过程中,有时需要是一些对象复位,即回复对象的初始状态。一般情况下,我们会在类中增加一个reset()之类的成员,在这个函数中释放资源,恢复各成员数据的初值。但是,在拥有swap的情况下,这种操作变得非常容易:
classX
...{
publicX()...{
...//初始化对象
}
publicX(intv)...{
...//以v初始化对象
}
publicvoidswap(Xval)...{...}
publicvoidreset()...{
this.swap(newX());
}
...
}
reset()用X的默认构造函数创建了一个临时对象,将其内容与this交换,this的内容便成为了初始值。重要的是,这个成员函数也是
强异常保证的。如果需要通过一些参数复位,那么同样可以做到:
classX
...{
...
publicvoidreset(intv)...{
this.swap(newX(v));
}
...
}
有时甚至可以不需要reset这个成员,而直接在代码中使用swap复位一个对象:
Xx=newX();
...//对x的操作,改变了内容
x.swap(newX());//复位了
如果X有资源需要释放,那么只需实现IDispose,然后使用using:
classX:IDisposable
...{
...
publicvoidreset()...{
using(Xt=newX())
...{
this.swap(t);
}
}
publicvoidDispose()...{...}
...
}
上面这些应用都有一个共同点,即重新初始化一个对象,使其恢复到一个初始状态。下面的应用,则反其道而行之,将一个对象切换到另一个状态。
有时,我们会做一些类,在构造函数中执行一些复杂的操作,比如解析一个文本文件,然后向外公布解析后的结果。之后,我们需要在这个对象上load另一个文 件,那么通常都写一个load成员函数,先释放掉原先占用的资源,然后再加载新的文件。如果有了swap,那么这个load函数同样极其简单:
classY:IDisposable
...{
publicY(stringfilename)...{
...//打开文件,执行解析
}
publicvoidswap(Yval)...{...}
publicload(stringfilename)...{
using(Yt=newY(filename))
...{
this.swap(t);
}
}
publicvoidDispose()...{
...//关闭文件,释放资源
}
}
还有一种情况,有一些类,通过一些数据创建,创建之后在绝大多数的情况下都是只读的,但偶尔会需要改变其内部数据。为了代码的可靠性,我们可以把类写成只读的。但是如何修改其内部的数据呢?也可以通过swap:
相关推荐
在C++编程语言中,掌握基础知识是至关重要的,本篇文章将重点总结C++的基础内容,包括头文件、命名空间、输入输出以及引用的概念和使用。 首先,C++中的头文件是包含函数声明和类型定义的文件,例如`<iostream>`是...
在C++编程语言中,`swap`函数是一个非常基础且重要的工具,用于交换两个变量的值。这个功能在处理数组、容器或者需要重新排列数据顺序的场景中非常常见。本篇文章将深入探讨`swap`函数的工作原理,以及如何在C++中...
在C++编程语言中,模板是一种强大的特性,它允许我们编写通用代码,以处理不同类型的对象,无需重复编写相似的代码。本篇文章将深入探讨C++模板的基本概念、类型和函数模板,以及模板特化和元编程等高级主题。 首先...
C++中的`swap`函数是程序中用于交换两个变量值的常见工具,它在各种编程场景中都有着广泛的应用。这篇文章将探讨C++中`swap`函数的不同实现方式,包括基本的非模板版本、模板版本以及C++11的优化版本。 1. **基本的...
在这个“C++ STL学习——各种变异算法技术总结和用法代码实例(1)”的主题中,我们将深入探讨STL中的变异算法,这些算法用于改变容器中元素的排列或状态。以下是一些主要的变异算法及其详细解释: 1. **交换算法(`...
在C++编程语言中,`std::swap`是一个非常重要的工具,用于交换两个变量的值。这个函数在标准库中定义,可以在 `<algorithm>` 或 `<utility>` 头文件中找到。`std::swap` 的核心功能是快速而安全地交换两个对象的状态...
在这个“c++STL学习——各种变异算法技术总结和用法代码实例(2)”的主题中,我们将深入探讨STL中的非变异算法,这些算法在不改变容器元素数量的情况下,对容器中的数据进行操作。非变异算法包括交换、查找、匹配、...
标题所述的主题是"C/C++和Java达到swap不同功能",主要探讨了在C/C++和Java两种编程语言中实现变量交换的差异。在C/C++中,我们可以直接通过指针或引用来交换变量的值,而在Java中,由于不支持指针操作,我们需要...
在C++编程中,交换两个变量的值是一个常见的操作,`swap`函数就是用来完成这一任务的。在C++中,`swap`函数有多种不同的实现方式,每种都有其特定的适用场景和优缺点。这里我们将探讨几种常见的`swap`函数实现方法。...
std::swap(arr[j], arr[j + 1]); swapped = true; } } // 如果一轮下来没有交换,说明数组已经有序 if (!swapped) break; } } int main() { int arr[] = {64, 34, 25, 12, 22, 11, 90}; int n = sizeof...
Dev-C++是一个C和C++集成开发环境,可以用来编译和运行这种代码。 总的来说,堆排序是一种效率较高的排序算法,时间复杂度为O(n log n),适用于大数据集。通过减治法的策略,我们能有效地管理和解决问题,而C++提供...
1. 函数模板:用于编写可处理多种类型的通用函数,如`std::swap`。 2. 类模板:如`std::vector`和`std::map`,是实现泛型容器的基础。 3. 模板元编程:在编译时进行计算,如`std::enable_if`用于类型检查和条件编译...
标题中的"swap_1位swap电路_logisim_swap_"指的是一个关于数字逻辑设计中的1位交换(swap)电路,该电路使用逻辑门实现输入信号的交换功能。在电子工程和计算机科学领域,这样的电路通常用于数据处理或计算过程中...
描述部分通过重复文本的方式强调了增大swap分区的重要性,虽然这种方式在实际文档中并不常见,但其核心意图是明确的——提醒读者关注并执行这一操作。增大swap分区对于提升系统稳定性和性能有着不可忽视的作用,特别...
用c++写的快速排序 Swap交换两个int类型的数据 Sort排序 QuickSort快速排序(递归) main
在C++中实现快速排序,可以使用函数指针来处理不同类型的元素,使其具有良好的泛型性。以下是一个简单的快速排序函数模板的示例: ```cpp #include using namespace std; template int partition(T arr[], int ...
### CentOS清理SWAP交换区内存 #### SWAP分区机制与问题背景 在深入探讨如何清理CentOS中的SWAP交换区之前,我们先了解下SWAP的基本概念及其在系统中的作用。SWAP空间(或称为SWAP分区)是在硬盘上预留的一块区域...
在C++中,策略模式通常通过组合而非继承来实现,这与模板模式有所不同。模板模式是通过继承将算法的不变部分封装在父类中,而变化的部分由子类来实现。策略模式则将算法封装在独立的策略类中,客户端可以根据需要...