这个成语是什么?什么时候用?它解决了哪些问题?当使用C++11时,习惯用法会改变吗
虽然在很多地方都提到过它,但我们没有任何单一的“它是什么”问题和答案,所以就在这里。以下是之前提到的部分地点列表:
-
你最喜欢的C++编码风格成语:复制交换
- C++中的复制构造函数和=运算符重载:是否可以使用公共函数
- 什么是复制省略以及它如何优化复制和交换习惯用法
- C++:动态分配对象数组
概述
为什么我们需要复制和交换习惯用法
任何管理资源的类(像智能指针一样的包装器)都需要实现三大功能。虽然复制构造函数和析构函数的目标和实现很简单,但复制赋值操作符可以说是最微妙和最困难的。应该怎么做?需要避免哪些陷阱
复制和交换习惯用法是解决方案,它优雅地帮助赋值操作符实现两件事:避免代码重复和提供强大的异常保证
它是如何工作的
从概念上讲,它使用复制构造函数的功能创建数据的本地副本,然后使用swap函数获取复制的数据,用新数据交换旧数据。然后,临时副本将销毁,并带走旧数据。我们只剩下一份新数据的副本
为了使用复制和交换习惯用法,我们需要三样东西:工作的复制构造函数、工作的析构函数(两者都是任何包装器的基础,因此无论如何都应该是完整的)和交换函数
交换函数是一个非抛出的函数,它将一个类的两个对象(成员对成员)交换。我们可能会尝试使用std::swap而不是提供我们自己的,但这是不可能的标准::交换在其实现中使用复制构造函数和复制赋值运算符,我们最终将尝试根据赋值运算符本身定义赋值运算符
(不仅如此,对swap的非限定调用将使用我们的自定义swap操作符,跳过std::swap将需要的类的不必要的构造和销毁。)
深入的解释
目标
让我们考虑一个具体的例子。我们想要在一个无用的类中管理一个动态数组。我们从工作构造函数、复制构造函数和析构函数开始:
#包括<;算法>;//复制
#包括<;cstdef>;//标准:尺寸
类dumb_数组
{
公众:
//(默认)构造函数
哑数组(std::size\u t size=0)
:mSize(大小),
mArray(mSize?新整数[mSize]():nullptr)
{
}
//复制构造函数
哑数组(常数哑数组和其他)
:mSize(其他.mSize),
mArray(mSize?新整数[mSize]:nullptr)
{
//注意,这是非抛出的,因为数据
//正在使用的类型;更多地注意细节
//然而,例外情况必须在更一般的情况下给出
std::copy(other.mArray,other.mArray+mSize,mArray);
}
//析构函数
~dumb_数组()
{
删除[]玛丽;
}
私人:
标准:尺寸尺寸;
国际*玛丽酒店;
};
这个类几乎成功地管理了数组,但它需要操作符=才能正常工作
失败的解决方案
下面是一个幼稚的实现的外观:
//最难的部分
dumb_阵列&;运算符=(常量哑数组和其他)
{
如果(此!=&;其他)/(1)
{
//清除旧数据。。。
删除[]mArray;/(2)
mArray=nullptr;//(2)*(基本原理见脚注)
//…并加入新的
mSize=other.mSize;//(3)
mArray=mSize?新int[mSize]:nullptr;/(3)
std::copy(other.mArray,other.mArray+mSize,mArray);//(3)
}
归还*这个;
}
我们说我们完了;现在,它可以管理阵列,而不会出现泄漏。但是,它有三个问题,在代码中顺序标记为(n)
-
第一个是自我分配测试。
此检查有两个目的:它是一种简单的方法,可以防止我们在自我分配时运行不必要的代码,并且可以保护我们免受细微的错误(例如删除数组只是为了尝试复制它)。但在所有其他情况下,它只会减慢程序的速度,并在代码中起到噪音的作用;自我分配很少发生,因此大多数情况下,此检查是一种浪费。
如果操作员没有它也能正常工作,那就更好了 -
第二,它只提供了基本的例外保证。如果
new int[mSize]失败,*此将被修改。(即大小错误,数据丢失!)
对于强有力的例外保证,它需要类似于:哑数组&;运算符=(常量哑数组和其他) { 如果(此!=&;其他)/(1) { //在我们替换旧数据之前,先准备好新数据 std::size\u t newSize=other.mSize; int*newArray=newSize?new int[newSize]():nullptr;//(3) std::copy(other.mArray,other.mArray+newSize,newArray);//(3) //替换旧数据(所有数据均为非抛出) 删除[]玛丽; mSize=新闻化; mArray=新阵列; } 归还*这个; } -
代码已经扩展!这就引出了第三个问题:代码重复
我们的赋值运算符有效地复制了我们在别处已经编写的所有代码,这是一件可怕的事情
在我们的例子中,它的核心只有两行(分配和拷贝),但是对于更复杂的资源,这段代码膨胀可能是相当麻烦的。我们应该努力做到永不重复
(有人可能会想:如果正确管理一个资源需要这么多代码,那么如果我的类管理多个资源呢?
虽然这似乎是一个值得关注的问题,而且确实需要非常重要的try/catch子句,但这不是一个问题。
这是因为一个类应该只管理一个资源
成功的解决方案
如上所述,复制和交换习惯用法将解决所有这些问题。但是现在,除了一个要求外,我们有所有的要求:一个swap函数。虽然三的规则成功地要求存在复制构造函数、赋值运算符和析构函数,但它实际上应该被称为;“三大半”:任何时候,您的类管理资源时,提供交换函数也是有意义的
我们需要向类中添加交换功能,我们的做法如下†:
类哑数组
{
公众:
// ...
好友无效交换(哑数组&第一,哑数组&第二)//nothrow
{
//启用ADL(在我们的情况下不需要,但需要良好的实践)
使用std::swap;
//通过交换两个对象的成员,
//这两个对象被有效地交换
交换(first.mSize,second.mSize);
交换(第一个是mArray,第二个是mArray);
}
// ...
};
(这里解释了为什么公共好友交换)现在我们不仅可以交换dumb_数组,而且交换通常可以更有效;它只是交换指针和大小,而不是分配和复制整个数组。除了在功能性和效率方面的优势之外,我们现在已经准备好实现复制和交换习惯用法
无需进一步说明,我们的赋值运算符为:
哑数组&;运算符=(哑数组其他)/(1)
{
互换(*本,其他);/(2)
归还*这个;
}
就这样!一举,所有三个问题都能优雅地同时解决
它为什么有效
我们首先注意到一个重要的选择:参数参数按值取值。虽然我们可以很容易地做到以下几点(事实上,许多简单的习语实现都可以做到):
哑数组&;运算符=(常量哑数组和其他)
{
哑_阵列温度(其他);
交换(*本,临时);
归还*这个;
}
我们失去了一个重要的优化机会。不仅如此,这个选择在C++11中也是至关重要的,后面将讨论这个问题。(一般来说,下面是一条非常有用的指导原则:如果要在函数中复制某些内容,请让编译器在参数列表中执行此操作。”——)
无论哪种方式,这种获取资源的方法都是消除代码重复的关键:我们可以使用复制构造函数中的代码进行复制,而不需要重复任何一点。既然复制完成了,我们就可以交换了
请注意,进入该功能后,所有新数据都已分配、复制并准备好使用。这给了我们一个强大的免费异常保证:如果副本构造失败,我们甚至不会进入函数,因此不可能改变*This的状态。(我们以前为获得强大的异常保证而手动执行的操作,现在编译器正在为我们执行;多么友好。)
在这一点上,我们是自由的,因为swap是非抛出的。我们用复制的数据交换当前数据,安全地改变我们的状态,旧数据被放入临时数据。然后,当函数返回时,旧数据被释放。(其中,参数的作用域结束并调用其析构函数。)
因为这个习惯用法不重复代码,所以我们不能在操作符中引入bug。请注意,这意味着我们不需要进行自我分配检查,从而允许对操作符=进行单一的统一实现。(此外,我们不再对非自我分配进行绩效处罚。)
以及