这个成语是什么?什么时候用?它能解决哪些问题?当使用C++ 11时,成语会发生变化吗?
虽然很多地方都提到过这个问题,但我们没有任何单一的"它是什么"问题和答案,所以这里就是。以下是之前提到过的地方的部分列表:
- 你最喜欢的C++编码风格成语:拷贝交换
- C++中复制构造函数和=运算符重载:可能的公共函数吗?
- 什么是复制省略以及它如何优化复制和交换习惯用法
- C++:动态分配对象数组?
- gotw.ca / gotw / 059.htm萨特从草本植物
- 真棒,我这个问题的答案从我的联动的语义。
- 有一个好主意全蓝解释这个成语,它太普通,每个人都应该知道它。
- 警告:成语是用来复制/交换更多的问题远比它是有用的。它是一个经常和强异常安全保证的性能当一个拷贝赋值是不需要的。当强异常安全的拷贝赋值是必要的,它是很容易提供的一个短的通用功能,除了A具有多拷贝赋值操作符。slideshare.net /湖泊/ hinnant ripplelabs霍华德- accu2014幻灯片43 53。摘要:复制/交换是一个有用的工具在工具箱中。但它已经过后一直abused随后安切洛蒂和所有。
- "是的,howardhinnant认为:+ 1。我写的一本在一个时间的问题在C + +几乎是"帮助我的舱崩溃当它复制",这是我的反应。它是适当的,当你只是想工作拷贝/移动语义或什么,所以你可以继续其他的东西,但它不是真正的优化。随时把免责声明顶部我的回答,如果你认为这会有帮助的。
- 我gmannickg @。现在有一个视频演示幻灯片可在大学论文:youtube.com /手表吗?V=vlinb2fgkhk AT&;T = 35m30s
概述为什么我们需要复制和交换习语?
任何管理资源的类(包装器,如智能指针)都需要实现三大类。尽管复制构造函数和析构函数的目标和实现很简单,但复制分配操作符可以说是最细微和最困难的。怎么做?需要避免哪些陷阱?好的。
复制和交换习语是解决方案,优雅地帮助赋值操作符实现两件事:避免代码重复,并提供强大的异常保证。好的。它是如何工作的?
从概念上讲,它通过使用复制构造函数的功能来创建数据的本地副本,然后使用swap函数获取复制的数据,用新数据交换旧数据。然后,临时副本将销毁,并带走旧数据。我们只剩下一份新数据的副本。好的。
为了使用复制和交换习惯用法,我们需要三件事情:一个工作的复制构造函数、一个工作的析构函数(两者都是任何包装器的基础,因此无论如何都应该是完整的)和一个swap函数。好的。
交换函数是一个非抛出函数,用于交换类中的两个对象,即成员与成员。我们可能会尝试使用std::swap而不是提供自己的,但这是不可能的;std::swap在其实现中使用复制构造函数和复制分配运算符,我们最终会尝试根据自身来定义分配运算符!好的。
(不仅如此,对swap的无条件调用将使用我们的自定义交换运算符,跳过std::swap将导致的不必要的类构造和破坏。)好的。深入的解释目标
让我们考虑一个具体的例子。我们想在一个本来无用的类中管理一个动态数组。我们从一个工作的构造函数、复制构造函数和析构函数开始:好的。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34
| #include // std::copy
#include <cstddef> // std::size_t
class dumb_array
{
public:
// (default) constructor
dumb_array(std::size_t size = 0)
: mSize(size),
mArray(mSize ? new int[mSize]() : nullptr)
{
}
// copy-constructor
dumb_array(const dumb_array& other)
: mSize(other.mSize),
mArray(mSize ? new int[mSize] : nullptr),
{
// note that this is non-throwing, because of the data
// types being used; more attention to detail with regards
// to exceptions must be given in a more general case, however
std::copy(other.mArray, other.mArray + mSize, mArray);
}
// destructor
~dumb_array()
{
delete [] mArray;
}
private:
std::size_t mSize;
int* mArray;
}; |
这个类几乎可以成功地管理数组,但它需要operator=才能正常工作。好的。失败的解决方案
下面是一个幼稚的实现的样子:好的。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| // the hard part
dumb_array& operator=(const dumb_array& other)
{
if (this != &other) // (1)
{
// get rid of the old data...
delete [] mArray; // (2)
mArray = nullptr; // (2) *(see footnote for rationale)
// ...and put in the new
mSize = other.mSize; // (3)
mArray = mSize ? new int[mSize] : nullptr; // (3)
std::copy(other.mArray, other.mArray + mSize, mArray); // (3)
}
return *this;
} |
我们说我们已经完成了;现在它可以管理一个数组,而不会泄漏。但是,它有三个问题,在代码中按顺序标记为(n)。好的。
第一个是自我分配测试。这种检查有两个目的:它是一种简单的方法,可以防止我们在自分配时运行不必要的代码,并且它可以保护我们免受细微的错误(例如,删除数组只是为了尝试和复制它)。但在所有其他情况下,它只会减慢程序的运行速度,并在代码中起到干扰的作用;自分配很少发生,因此大多数时候这种检查都是浪费。如果操作员没有它就可以正常工作,那就更好了。好的。
第二,它只提供基本的例外保证。如果new int[mSize]失败,*this将被修改。(即大小错误,数据丢失!)为了获得强有力的例外保证,它需要类似于:好的。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| dumb_array& operator=(const dumb_array& other)
{
if (this != &other) // (1)
{
// get the new data ready before we replace the old
std::size_t newSize = other.mSize;
int* newArray = newSize ? new int[newSize]() : nullptr; // (3)
std::copy(other.mArray, other.mArray + newSize, newArray); // (3)
// replace the old data (all are non-throwing)
delete [] mArray;
mSize = newSize;
mArray = newArray;
}
return *this;
} |
代码已扩展!这就导致了第三个问题:代码复制。我们的赋值操作符有效地复制了我们已经在别处编写的所有代码,这是一件可怕的事情。好的。
在我们的例子中,它的核心仅仅是两行(分配和复制),但是对于更复杂的资源,这种代码膨胀会非常麻烦。我们应该努力不让自己重蹈覆辙。好的。
(有人可能会想:如果正确管理一个资源需要这么多代码,那么如果我的类管理多个资源呢?虽然这似乎是一个值得关注的问题,而且确实需要非常重要的try/catch条款,但这不是问题。这是因为一个类应该只管理一个资源!)好的。一个成功的解决方案
如前所述,复制和交换习语将解决所有这些问题。但是现在,除了一个:swap函数之外,我们还有所有的需求。虽然"三"规则成功地要求存在我们的复制构造函数、赋值运算符和析构函数,但它实际上应该被称为"三大半":每当您的类管理资源时,提供swap函数也是有意义的。好的。
我们需要将交换功能添加到我们的类中,并按如下所示进行操作?:好的。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| class dumb_array
{
public:
// ...
friend void swap(dumb_array& first, dumb_array& second) // nothrow
{
// enable ADL (not necessary in our case, but good practice)
using std::swap;
// by swapping the members of two objects,
// the two objects are effectively swapped
swap(first.mSize, second.mSize);
swap(first.mArray, second.mArray);
}
// ...
}; |
(这就是为什么public friend swap现在不仅可以交换我们的dumb_array,而且交换通常更有效;它只是交换指针和大小,而不是分配和复制整个数组。除了在功能和效率上的额外好处,我们现在已经准备好实现复制和交换习惯用法。好的。
无需进一步说明,我们的赋值运算符是:好的。
1 2 3 4 5 6
| dumb_array& operator=(dumb_array other) // (1)
{
swap(*this, other); // (2)
return *this;
} |
就这样!一举解决了这三个问题。好的。为什么有效?
我们首先注意到一个重要的选择:参数参数是按值取的。虽然一个人可以很容易地做到以下几点(实际上,许多幼稚的成语实现都是这样做的):好的。
1 2 3 4 5 6 7
| dumb_array& operator=(const dumb_array& other)
{
dumb_array temp(other);
swap(*this, temp);
return *this;
} |
我们失去了一个重要的优化机会。不仅如此,而且这种选择在C++ 11中是至关重要的,这将在后面讨论。(一般来说,一个非常有用的指导原则如下:如果要在函数中复制某个内容,请让编译器在参数列表中执行该操作。)好的。
不管怎样,获取资源的这种方法都是消除代码重复的关键:我们可以使用复制构造函数中的代码进行复制,并且不需要重复任何一点。既然复印好了,我们就可以交换了。好的。
请注意,在输入函数时,所有新数据都已分配、复制并准备好使用。这就是为什么我们可以免费获得一个强有力的例外保证:如果拷贝的构造失败,我们甚至不会进入这个功能,因此不可能改变*this的状态。(我们以前手工做过的,为了获得强有力的异常保证,编译器现在正在为我们做;真是太好了。)好的。
在这一点上,我们是免费的,因为swap不是投掷。我们将当前数据与复制的数据交换,安全地改变我们的状态,然后将旧数据放入临时数据中。当函数返回时,将释放旧数据。(其中,参数的作用域结束并调用其析构函数。)好的。
因为这个习语不重复任何代码,所以我们不能在操作符中引入错误。请注意,这意味着我们不需要进行自分配检查,只需统一执行operator=。(此外,对于非自我分配,我们不再有绩效惩罚。)好的。
这就是复制和交换习语。好的。C++ 11怎么办?
下一个版本的C++、C++ 11对我们如何管理资源做出了一个非常重要的改变:三的规则现在是四(半)的规则。为什么?因为我们不仅需要能够复制构造我们的资源,还需要移动构造它。好的。
幸运的是,这很容易:好的。
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| class dumb_array
{
public:
// ...
// move constructor
dumb_array(dumb_array&& other)
: dumb_array() // initialize via default constructor, C++11 only
{
swap(*this, other);
}
// ...
}; |
这是怎么回事?回想一下move construction的目标:从类的另一个实例中获取资源,使其处于保证可分配和可销毁的状态。好的。
因此,我们所做的很简单:通过默认构造函数(C++ 11特性)初始化,然后与EDCOX1(7)进行交换;我们知道我们的类的默认构造实例可以被安全地分配和销毁,因此我们知道EDCOX1×7将在交换之后也能这样做。好的。
(注意,一些编译器不支持构造函数委托;在这种情况下,我们必须手动默认地构造类。这是一项不幸但幸运的琐碎任务。)好的。为什么会这样?
这是我们班上唯一需要做的改变,那为什么它起作用呢?请记住,我们做出的重要决定是将参数设为一个值,而不是一个引用:好的。
1
| dumb_array& operator=(dumb_array other); // (1) |
现在,如果用右值初始化other,它将被移动构造。很完美。以同样的方式,C++ 03让我们重新使用复制构造函数,通过按值取值,C++ 11也会在适当的时候自动选择移动构造函数。(当然,正如前面链接的文章中提到的,复制/移动值可能会被完全忽略。)好的。
复制和交换习语到此结束。好的。脚注
*为什么要将mArray设置为空?因为如果在操作符中有任何进一步的代码抛出,那么可能会调用dumb_array的析构函数;如果在不将其设置为空的情况下发生这种情况,那么我们将尝试删除已经删除的内存!我们通过将其设置为空来避免这种情况,因为删除空值是一个"否"操作。好的。
?还有其他的说法,我们应该为我们的类型专门开发std::swap,在类内提供swap边上提供一个自由函数swap等,但这都是不必要的:任何正确使用swap都将通过一个不合格的调用,我们的功能将通过adl找到。一个功能就可以了。好的。
?原因很简单:一旦你拥有了你自己的资源,你就可以在需要的任何地方交换和/或移动它(C++ 11)。通过在参数列表中进行复制,可以最大化优化。好的。好啊。
- @gman:交换的自定义重载仍然可能更快,因为移动语义必须在复制源指针后将其设置为零。
- @弗雷德:不过,优化编译器很容易看出这样的赋值是浪费的。我证明在我的回答中,我链接到了帖子中。不过,你可能有一点,如果没有这样的东西,swap可能会更快。不过,我再也不认为值得了。
- @GMAN:如果我们依赖于移动语义,那么类肯定需要一个移动CCTOR和一个移动分配操作符。因此,这实际上比您自己的swap()有更多的需求。(我并不反对这样做,只是希望能提到这一点。)
- @是的,"只要实现了移动语义。"除非我是误会。(编辑:猜不到。P)我会让C++0X部分更具体。顺便说一下,我认为当我说"这是用于实现三大类的类"时,DTOR要求是明确的,我会看看我是否可以将其隐藏起来。
- @GMAN:我认为一个同时管理多个资源的类注定会失败(异常安全变成噩梦),我强烈建议一个类管理一个资源,或者它具有业务功能和使用管理器。
- @不过,我想知道你为什么要在零尺寸的情况下做这个测试?动态零大小数组正常。
- @约翰内斯:我知道零大小的动态数组是可以的,但是我做了检查:1)避免获得任何动态内存,2)让它看起来更复杂。:P很高兴知道这足够了,我承认我对他所说的有点困惑。
- @马修:我同意,我已经补充了这一点。@约翰内斯:关于自我分配,我已经做了一些修改。
- 很好的总结!我个人会对throw()发表评论。将文本留在此处,以表明您认为函数不会抛出,但不考虑潜在的惩罚:boost.org/development/requirements.html exception specificat&zwnj;&8203;ion
- @ gman:或者使用C++ 0x EDOCX1 3限定符:
- @谢谢。好时机,我正计划(并且确实)改进和扩展C++ 0x更改。
- 大四?(1)dtor,(2)copy ctor,(3)copy op=,(4)move ctor,和(5)move op=。哪一个不算?
- @詹姆斯:只有一个作业员。
- @你可以同时声明一个copy op=和一个move op=,不是吗?(如果实现了copy op=则隐式move op=将被抑制)。或者,这是我的理解。为什么我错了?
- @詹姆斯:不,你是对的,你可以。你只需在C++ 03和C++ 0x中使用它。(在C++ 03中,LValk被复制,rValk希望得到它们的拷贝,在C++ 0x LValx中被复制,Ravoues移动,有时希望被删除)。
- Gman:重读这篇文章(C++0X部分),我对四巨头感到好奇。我会说"大五"包括移动分配操作符:p是一个移动分配操作符,如果不声明它,它是自动定义的?
- GMMA:C++0x将支持隐式移动,尽管有一些限制。(我们必须等到月底的下一封邮件时才能知道有什么限制……)
- @GMAN,谢谢你的指导!您不介意举一个例子,一个非重载的copy方法如何使用copy-and-swap习惯用法,例如,一个类似void copy(const dumb_array& other)的方法?
- @哈维尔:我不确定我明白你的意思。如果需要的话,你可以问一个完整的问题,一定要包括代码示例。
- @GMAN,下面的方法在概念上是否正确,遵循复制和交换的习惯用法?void dumb_array::copy(dumb_array other) { swap(*this, other); }
- @Javier:这个习语对于实现复制语义的必要功能(三大功能)很有用。我想我的答案是"是的",但我看不出这种功能有什么用处。
- @哈维尔:我想不出我只需要复制某些成员的情况。也许这些成员应该是具有自己功能的自己的类。
- 一个人必须花费数小时在网上阅读的主题现在被放进一个简单、完整和完美的(c)解释中。多好啊@gman!+ 1
- 如果哑数组的构造函数是explicit呢?打电话给EDOCX1[1]不行,是吗?
- @加布里埃尔:用什么称呼它?如果它是,比如说,dumb_array x(1); x = 5;,那么它就不起作用了。但是,除了显式构造函数之外,这与任何事情都没有关系。
- @gman OK,这意味着如果一个类出于任何原因需要它的构造函数显式化,那么它就不能使用复制和交换习惯用法的这种实现。operator=中的一个微小变化可以解决这个问题:通过引用传递要复制的对象,并在函数开始时声明一个本地对象,复制构造它。
- @加布里埃尔:我不确定我能理解。explicit构造器和copy和swap是两个完全无关的东西。将参数更改为引用仍然不允许上一个示例中的代码工作。
- @很抱歉我说的是显式复制构造函数。按照您的说法,如果需要将dumb_array的复制构造函数设置为显式的,您将无法编写:dumb_array a, b; a = b;。将复制构造函数更改为dumb_array& operator=(dumb_array & other) { swap(*this, dumb_array(other)); return *this; },现在可以了。
- @加布里埃尔:啊,真的。也就是说,我从来没有找到明确说明复制构造函数的理由。
- 我不明白为什么交换方法在这里声明为朋友?
- @ASD:允许通过ADL找到它。
- 我不明白为什么默认的构造函数执行"new int[msize]()",而复制构造函数执行"new int[msize]"。有什么区别?
- @neuviemeporte:使用圆括号,数组元素是默认初始化的。如果没有,则它们是未初始化的。因为在复制构造函数中,我们无论如何都要重写这些值,所以可以跳过初始化。
- 盖曼:谢谢。另外,我想知道swap()是否可以成为dumb_数组的成员函数。对我来说,全球朋友的职能似乎很混乱。
- @Neuviemeporte:如果你想在ADL期间找到它(using std::swap; swap(x, y);),它需要成为全球的朋友。
- @GMAN:我不确定我是否理解;是否将swap()作为一个成员会使它变得更简单,这样你就不必关心adl了?
- @Neuviemeporte:如果你想让你的swap在ADL中工作在你遇到的大多数通用代码中,比如boost::swap和其他各种交换实例中,你需要在ADL中找到你的swap。交换是C++中一个棘手的问题,通常我们都同意单点访问是最佳的(一致性),而唯一的方法是自由函数(EDCOX1,11),例如,不能有交换成员。请看我的问题了解一些背景。
- 如何对派生类执行swap?
- @凯瑞克:这听起来是一个合理的问题。我承认我不确定我是否完全理解你所追求的用例。
- @gmannick:直接的动机就是这个问题,但是我最近一直在想,一般来说,如果基提供了一个交换,是否可以通过切片为派生类编写一个交换。
- @Kerreksb:我只采用您所采用的方法,在交换最派生的类之前交换基类。
- @gmannickg:我得出的结论是,切片的存在正是为了实现派生类的复制构造函数、复制分配和交换函数。就在今天,我才明白这一点。听起来合理吗?
- @凯瑞克:嗯,实际上我没有看到任何切片。据我所知,A::swap(rhs)与this->A::swap(rhs)是相同的;如果A::swap调用了任何虚拟函数,例如(不应该这样),它们可以被调度到最派生的类。
- @格曼尼克:嗯,说得对,打电话给基地swap并不是真的切。但是假设每个类实现自己的"标准"交换语义,它只"切片交换"对象的基础部分。
- @凯瑞克:这是个好结论,是的。
- @gmannick移动构造函数依赖于默认ctor。这是否意味着(自移动语义以来)每个资源包装器都必须具有1)有效的"空"状态和2)初始化到此状态的默认ctor?
- @科斯:技术上没有。你可以在你的移动构造函数中做任何你想做的,并且从状态中移动任何东西。但将其重置为简单的可重用状态不仅仅是一个好的实践。标准库类(如std::vector<>类)会做这样的事情。
- @gmannickg在之前的评论中,你说swap应该是的全球朋友,以便通过adl找到它。在您的示例中,swap看起来像dumb_array的公共成员。我错过了什么?
- @zmb:"全局"部分(即完全在类外部声明)是可选的。重要的是,它是一个非成员朋友函数,因此它将在ADL期间找到,例如,当标准库调用swap(x, y);时。有关详细信息,请参阅此部分。
- 如果dumb_array::dumb_array()很昂贵(例如,它总是保留一些空间),那么关于dumb_array(dumb_array&& other)的建议(即默认构造然后交换)是否会有所不同?在这种情况下,通过从另一个对象移动来初始化成员,然后从该对象的状态中去掉"已移动的对象"会更好吗?
- @本海默斯:是的。"复制和交换"习惯用法仅用于简化新资源管理类的一般创建。对于每一个特定的班级,几乎肯定都有一条更有效的路线。这个成语只是一个有用的东西,很难做错。
- @gmannick你的答案是有帮助的,但是对[icce.rug.nl/documents/cplusplus/cplusplus11.html l194][icce.rug.nl/documents/cplusplus/cplusplus09.html l159]的一些赞扬不会伤害Scott Meyers。
- @gmannick:fwiw,类似dumb_array temp(other)的语句;在通用代码(例如stl向量实现)中是不可支持的。原因是哑数组的大小是任意的,对于堆栈来说可能太大,并且由于堆栈空间耗尽而导致应用程序崩溃。我在实践中多次遇到这种情况,尤其是在具有小默认堆栈(如mac os x)的平台上。
- @threebit:数组不存储在堆栈空间中,在使用默认分配器的std::vector中也是如此。你在实践中遇到了一个不同的问题。
- @gmannick关于向量,你是对的,因为几乎所有向量实现都在堆上分配内存。我不应该把它当作一个假定的例子。我的意思是,一般来说,您不能编写在堆栈上创建任意对象实例的通用代码。当然,还有其他选择。
- 我的是一个不寻常的例子,但是我在一个类上使用std::swap时遇到了一个微妙的问题,该类有一个指针,指向对象的一部分字符串。此处讨论:stackoverflow.com/a/20513948/522385
- 为什么这是在该行中声明为friend的交换函数friend void swap(dumb_array& first, dumb_array& second) // nothrow?如果它在一个类中声明,它是否只是一个成员函数?
- @visviva类中的朋友是周围范围中的非成员函数,但作为朋友,它通常可以访问非公共成员。这是一种风格选择,但是可以很简洁地在那里定义小函数,而不是声明它们,并且在启动到函数体之前必须在周围范围内重复签名(可能有更明确的模板参数)。
- @但是为什么它是在类本身中定义的呢?它不应该写在其他地方,而只是在一个带有friend关键字的类中声明吗?
- @visviva,这只是一个风格问题——简洁和清晰地参与类的整体接口,而不是将接口与实现分离(并且可能避免隐式内联)——根据您的喜好来选择。
- 通过复制构造函数实现分配可能会导致不必要的分配,甚至可能导致未调用的内存不足错误。考虑在堆限制小于2GB的计算机上将700MB dumb_array分配给1GB dumb_aaray的情况。一个优化的分配将认识到它已经分配了足够的内存,并且只将数据复制到它已经分配的缓冲区中。在释放1GB缓冲区之前,您的实现将导致另一个700MB缓冲区的分配,导致所有3个缓冲区同时尝试在内存中共存,这将不必要地抛出内存不足错误。
- 正如我们多次提到的,关键是要有一些有效的东西,而不是最佳的东西。在工程时间和应用时间之间有一个权衡。
- @gmannick:因为它牺牲了维护的效率,所以您可能应该在问题或答案中明确指出复制和交换是一个解决方案,而不是解决方案,并且重新使用对象而不是构建一个新的对象可能是有意义的。否则人们将一无所知。(这似乎很明显,但事实并非如此;我花了很长时间才意识到这一点。)
- 对于任何想知道为什么使用不合格交换的人:STD::SWAP通常调用复制构造函数(C++ 11中的移动构造函数)和两个赋值操作符,而对于使用复制和替换习惯的类,每个赋值运算符无论如何都会调用特定于类的交换函数。因此,使用std::swap的成本是使用move或copy构造函数的成本的两倍。这并不是说复制和交换会使std::swap超慢;std::swap只是次优。
- 权衡:我使用三个版本对一个类似于上面的类进行了基准测试:一个带有自分配测试的老式操作符、一个复制和交换,以及一个稍微更手动的"复制和移动",它只交换资源和移动,分配所有其他内容。对于空数组,复制和交换比使用clang的旧样式分配快25%,但是对于非空数组或gcc,旧样式分配快约0-12.5%。""复制和移动"通常会减少复制和交换时间的一小部分,但它不会从上面使用的任何优化clang中受益。(续)
- 我曾希望避免自分配测试会为copy和swap的额外操作付出代价,但这似乎并不总是正确的(可能是由于快速的分支预测)。尽管如此,它还是很接近,有时会根据情况而赢,所以您不必为公式化的正确性和异常安全付出太多的代价,至少对于这个特定的类是这样的。然而,根据霍华德·辛南特的说法,对于std::vector,这个习语的速度可能会慢8倍,所以ymmv。
- @格曼尼克,你为什么不让它成为一个"五大"的移动分配操作员?或者它不再需要分配操作符的给定实现了吗?无论是评论还是文本都不清楚,如果你能加一段,那就太好了!谢谢)
- 这个答案是由胜利和纯粹的敬畏所构成的。很久以前我就不知道我从其他答案或评论中引用了多少次。
- 这篇文章现在已经过时了,也许有人已经提到过它(太多的文章需要回顾),但是您在move构造函数中使用的技术会导致一些人退缩。委托给默认的构造函数,然后交换是低效和不必要的,因为您应该直接将"其他"移动到正在创建的对象中。这是最自然的方法(实际上是imho)。
- 在一个不相关的注释中,在第一个代码示例的构造函数中建议注意。如果有人切换了"msize"和"marray"的声明顺序,那么使用"msize"初始化"marray"将出现问题(因为"marray"将在"msize"之前初始化,这样当调用"new int[msize]"时,后者将包含垃圾)。
- 好文章!不过,我发现了一个错误。在C++ 11中,rValor引用参数仍然是函数内的LValk。您只需要知道可以从中移动它的信息,但是您需要明确地执行它。看看这个活生生的例子。感谢您的时间:)
- 有一件事我不明白。为什么需要与复制构造函数和operator =()的参数进行交换,而不只是通过引用/移动成员将其复制到*this中?修改参数不是浪费吗,因为它只是临时的?
- 移动任务的一个重要方面是它们应该是noexcept。可以肯定地说,统一的operator=(dumb_array other)是noexcept吗?
- 我想知道复制和交换是否比复制和移动有任何优势。
- @Andy和Mikemb-我正在从这篇文章中学习,但是我的理解(如果我错了,请纠正我gmannick)是如果你只是复制或移动到目的地,目的地(目标)对象中的任何指针现在都是内存泄漏;目标中的任何对象也不会被破坏,是吗?通过交换,你基本上摧毁了旧的目标,给它一个机会"清理"的出路。
- 为什么要提供一个std::swapfriend void函数?为什么不在需要的时候使用两次std::swap?我不明白这一点
- 关于std::swap,如果您的类中有一个具有专门交换的对象,我假设您更喜欢这样,对吗?例如:倾向于str.swap(str2)而不是std::swap(str,str2),倾向于myvector.swap(yourvector)而不是std::swap(myvector,yourvector)。对吗?或者,即使在存在专门交换的情况下,您仍然喜欢std::swap吗?
- @std:.string和std::vector的luv2代码,std::swap有一个专门的功能,可以做到这一点。所以不需要打电话给专业人员。希望您的所有类都有自己的免费swap()函数,这样您就不需要调用成员类了。
- 哦,有趣的是,@rozina。您使用swap(a,b)而不是显式地使用类专门化(没有标准范围的交换是这一点的关键部分,对吗?)重载/依赖于参数的查找(ADL)将解析专用方法。当然,在你班的朋友中排名靠前的using std::swap方法,如果不需要或不提供专门的方法,基本上提供了回退/安全网。我的新理解(在这里概括)正确吗?这是推荐的"最佳实践"吗?
- @luv2code:由于vector和其他标准库类型位于std::名称空间中,因此在特定情况下,使用adl执行std::swap或swap并不重要,因为结果相同。std::vector的std::swap函数无论如何只调用vec.swap(othervec)。一般来说,最好是using std::swap,其次是不合格的swap(或者更好的是,boost::swap,这对你有好处)。
- @luv2code是的,你明白这个成语的意思:)
- 您确定std::swap()在其实现中使用赋值运算符吗?Stroustrup说它没有(C++程序设计语言(第四版),和167;175.1,PG 508)。
- 此外,说swap function is a non-throwing function是不正确的。实际上,如果T的复制构造函数抛出,那么std::swap(T&, T&)确实会抛出。
- @gmannick如果this->size==other.size(或在复制std::vector等对象时)如果this->capacity>=other.size()等对象,则您执行复制分配需要不必要的分配和取消分配。顺便说一句,std::vector没有将这个IDOM用于它的复制和移动分配——一定有原因。
- @沃尔特:复制和交换不是复制或分配的方式,而是一种方式。它的目的是简单和正确,而不是高效或专门化。
- 那么,我的赋值运算符应该通过引用还是通过值传递?这个问题不清楚
- 为了平衡利益,在交换方面实施移动分配存在缺陷。可能值得一提的是这里讨论的一些内容,这样程序员就可以了解正在进行的权衡。在大多数情况下,我发现这是一个很好的交易,因为我可以很快得到正确、安全的代码;然后,如果复制和交换效率不够,可以稍后更换。
- EDCOX1的8个参数是移动可构造的,在C++ 11中可移动可分配[实用程序.SWAP ](或EDCOX1,8)回落到提供的自定义交换实现,因此,我猜想,EDCOX1×8可以用来实现拷贝分配。
- 请您更新一下move ctor和move operator=将如何使用copy和swap习语实现的答案好吗?C++ 11节过时了(规则为4?)和目前有关这一问题的资源(IMHO)不清楚,偏差很大。当试图只更改operator=stackoverflow.com/questions/29698747/&hellip;时,会出现过载歧义。
- 以下内容有效吗?江户十一〔11〕。这很简单,所以也许我错过了什么
- @Nisba:不,函数一返回,a1就不存在了。返回引用或指向它的指针将不可避免地导致未定义的行为。
- 我们不需要三原则。我们需要零规则。
- 注意,这意味着我们不需要进行自分配检查,只需要实现一个统一的operator=。(另外,我们不再对非自我分配进行性能惩罚。)这种实现对自我分配有何好处?
任务的核心是两个步骤:分解对象的旧状态,并将其新状态构建为其他对象状态的副本。
基本上,这就是析构函数和复制构造函数所做的,所以第一个想法是将工作委托给它们。然而,既然破坏不能失败,而建设可能失败,我们实际上想用另一种方式来做:首先执行建设性部分,如果成功了,那么就执行破坏性部分。复制和交换习惯用法就是这样做的:它首先调用类的复制构造函数来创建临时的,然后用临时的交换其数据,然后让临时的析构函数破坏旧的状态。由于swap()应该永远不会失败,唯一可能失败的部分就是复制构造。首先执行,如果失败,目标对象中的任何内容都不会更改。
在其改进形式中,复制和交换是通过初始化赋值运算符的(非引用)参数来执行复制来实现的:
1 2 3 4 5
| T& operator=(T tmp)
{
this->swap(tmp);
return *this;
} |
- 我认为提到PIMPL和提到拷贝、交换和销毁一样重要。交换并不是异常安全的。它是异常安全的,因为交换指针是异常安全的。您不必使用PIMPL,但是如果不使用,那么必须确保成员的每个交换都是异常安全的。当这些成员可以改变时,这可能是一场噩梦;当他们隐藏在皮条客背后时,这是微不足道的。然后是皮条客的成本。这使我们得出这样的结论:异常安全往往会在性能上承担成本。
- …您可以为类编写分配器,该类将维护PIMPL的摊余成本。这就增加了复杂性,这影响到了普通的复制和交换习语的简单性。这是一个选择。
- std::swap(this_string, that)没有提供不丢的保证。它提供了强大的异常安全性,但不能保证不丢。
- 这意味着,如果您有两个类类型的成员,那么在赋值运算符中,如果第一个交换有效,而第二个交换失败,那么您必须确保撤消第一个交换以保持强异常安全性。这只是两个类类型的成员。
- @Wilhelmtell:我怀疑在用std::string实例化时,std::swap()违反了不抛出保证。(你评论的+1是我鼠标点击失败。)std::string专门呼叫std::string::swap()。〔C++ 03,21.3.7.8〕:"效果:lhs.swap(rhs);"
- @ WelelMeTele:在C++ 03中,没有提到EDCOX1(6)所引发的异常(这是EDCOX1(7)所调用的)。在C++0x中,EDCOX1×6是EDCOX1×9,并且不能抛出异常。
- @sbi@jamesmcnellis可以,但问题仍然存在:如果您有类类型的成员,那么必须确保交换它们是一个不容错过的过程。如果只有一个成员是指针,那么这很简单。否则就不是了。
- @威廉:是的。我很惊讶,新的C++0x可交换的概念不授权用户定义的EDCOX1,10个函数为noexcept。
- @威尔赫姆特尔:我认为这就是交换的意义:它从不掷球,而且总是O(1)(是的,我知道,std::array…)
- @SBI,如果我们想使用与operator =功能相似的copy method,该怎么办?例如,像void copy(const T & other)这样的方法。在这种情况下,如何使用复制和交换习语?
- @哈维尔:那个方法会做什么?
- @在重载operator=的情况下,我们复制整个类成员。在我的例子中,我只需要复制特定的类成员,我想使用copy-swap协议来实现这一点,例如void myClass::copy(myclass tmp){using std::swap; swap(member1,tmp.member1); swap(member3,tmp.member3); };。这有道理吗?那么,tmp参数应该作为值传递吗?
- @哈维尔:是的,然后复制(通过按值取参数)和交换(通过交换单个成员)是有意义的。好吧,只要有一个名为copy()的方法,只复制对象的一部分就行了。(我至少把它命名为copy_from()或类似的东西,这样方向就更清楚了。)
- "SBI,太棒了!谢谢你的解释!但是,我真的不确定我是否理解为什么这个论点应该是有价值的?
- @哈维尔:因为你无论如何都要复制一份。(你不想和原来的一个交换,但要有一个副本。)你也可以通过const引用,然后在函数中复制参数。然而,要做到这一点,惯用的方法是将论点复制。
- 将T& operator=(T tmp)标记为noexcept是否安全?
- @Becko:一般情况下不会,不会。它调用一个复制ctor,通常这些可以分配资源,这可能会失败,并导致抛出异常。
- @我想我同意丹尼尔·弗雷的回答:stackoverflow.com/a/18848460/855050(我后来发现)。如果你有反驳意见,请与我分享。
- @贝克奥:他的确讲得很好,所以我收回了我的说法,并坚持说相反的话是正确的。:)
已经有了一些很好的答案。我将主要集中在我认为他们缺少的东西上——用复制和交换习语解释"缺点"……
What is the copy-and-swap idiom?
根据交换函数实现赋值运算符的一种方法:
1 2 3 4 5
| X& operator=(X rhs)
{
swap(rhs);
return *this;
} |
基本理念是:
分配给对象最容易出错的部分是确保获取新状态所需的任何资源(例如内存、描述符)
如果复制了新值,则可以在修改对象的当前状态(即*this之前尝试获取,这就是为什么rhs被值(即复制)而不是通过引用接受的原因。
如果本地副本不需要任何特定的状态(只需要析构函数的状态适合运行,就如同从In=C++=11中移动的对象)一样,交换本地副本EDCOX1、1和EDCOX1(0)通常是相对容易的,没有潜在的故障/异常。
When should it be used? (Which problems does it solve [/create]?)
?swap抛出:通常可以可靠地交换对象按指针跟踪的数据成员,但非指针数据成员不具有可抛出交换,或者交换必须作为X tmp = lhs; lhs = rhs; rhs = tmp;实现,复制构造或分配可能抛出,仍然有可能导致某些数据成员交换失败。艾德和其他人没有。这一潜力甚至适用于C++ 03 EDCOX1,9,杰姆斯评论另一个答案:
@wilhelmtell: In C++03, there is no mention of exceptions potentially thrown by std::string::swap (which is called by std::swap). In C++0x, std::string::swap is noexcept and must not throw exceptions. – James McNellis Dec 22 '10 at 15:24
?从不同对象分配时看起来正常的分配运算符实现很容易因自分配而失败。虽然客户机代码甚至尝试自我分配似乎是不可想象的,但在容器上的algo操作过程中,这种情况比较容易发生,在x = f(x);代码中,f是一个宏ala #define f(x) x或返回对x的引用的函数,甚至(likely低效但简洁)代码,如x = c1 ? x * 2 : c2 ? x / 2 : x;。例如:
1 2 3 4 5 6 7 8 9 10 11 12
| struct X
{
T* p_;
size_t size_;
X& operator=(const X& rhs)
{
delete[] p_; // OUCH!
p_ = new T[size_ = rhs.size_];
std::copy(p_, rhs.p_, rhs.p_ + rhs.size_);
}
...
}; |
在自分配时,上述代码删除的x.p_;将p_指向一个新分配的堆区域,然后尝试读取其中的未初始化数据(未定义的行为),如果这不做任何太奇怪的事,copy尝试对每个刚刚被破坏的't'进行自分配!
?"复制和交换"习惯用法可能会由于使用了额外的临时参数(当运算符的参数是"复制构造"时)而导致效率低下或限制:
1 2 3 4 5 6 7 8
| struct Client
{
IP_Address ip_address_;
int socket_;
X(const X& rhs)
: ip_address_(rhs.ip_address_), socket_(connect(rhs.ip_address_))
{ }
}; |
在这里,手写的Client::operator=可能会检查*this是否已经连接到与rhs相同的服务器(如果有用的话,可能会发送"重置"代码),而复制和交换方法将调用复制构造函数,该构造函数可能被写入打开一个不同的套接字连接,然后关闭原始的套接字连接。这不仅意味着远程网络交互而不是简单的进程内变量复制,而且还可能违反客户机或服务器对套接字资源或连接的限制。(当然,这个类有一个非常可怕的接口,但这是另一个问题;-p)。
- 也就是说,套接字连接只是一个例子——同样的原理也适用于任何可能代价高昂的初始化,例如硬件探测/初始化/校准、生成线程池或随机数、某些加密任务、缓存、文件系统扫描、数据库连接等。
- 还有一个(大)骗局。从技术上讲,从当前规格来看,对象将没有移动分配运算符!如果以后用作类的成员,则新类不会自动生成move ctor!资料来源:Youtu.be/Myrbivnruyw?T=43M14S
- Client的拷贝分配操作符的主要问题是不禁止分配。
这个答案更像是对上述答案的一个补充和轻微修改。
在某些版本的Visual Studio(可能还有其他编译器)中,存在一个非常烦人且毫无意义的bug。因此,如果您像这样声明/定义您的swap函数:
1 2 3 4 5 6
| friend void swap(A& first, A& second) {
std::swap(first.size, second.size);
std::swap(first.arr, second.arr);
} |
…当您调用swap函数时,编译器会对您大喊大叫:
这与调用friend函数和将this对象作为参数传递有关。
解决此问题的方法是不使用friend关键字并重新定义swap函数:
1 2 3 4 5 6
| void swap(A& other) {
std::swap(size, other.size);
std::swap(arr, other.arr);
} |
这一次,您只需调用swap并传入other,就可以让编译器满意:
毕竟,您不需要使用friend函数交换2个对象。使swap成为具有一个other对象作为参数的成员函数同样有意义。
您已经可以访问this对象,因此将其作为参数传入在技术上是多余的。
- 你能分享复制错误的例子吗?
- @gmannickg dropbox.com/s/o1mitwcpxmawcot/example.cpp dropbox.com/s/jrjrn5dh1zez5vy/untitled.jpg.这是一个简化版本。每次用*this参数调用friend函数时似乎都会发生错误。
- @就像我说的,这是一个bug,对其他人来说可能很好。我只是想帮助一些和我有同样问题的人。我在Visual Studio 2012学习版和2013预览版中都尝试过,唯一让它消失的是我的修改
- 是的,绝对是个虫子。不过,这可能是对现有答案的评论,而不是答案,因为它无法回答实际问题。人们可以-1它。
- @gmannick它不适合所有图片和代码示例的注释。如果有人投反对票没关系,我敢肯定肯定有人也会遇到同样的问题;这篇文章中的信息可能正是他们需要的。
- 请注意,这只是IDE代码突出显示(intellisense)中的一个错误…它编译得很好,没有警告/错误。
- 如果尚未进行此操作(如果尚未修复),请在此处报告vs bug connect.microsoft.com/VisualStudio
- 在这篇文章发表一年之后,这个错误仍然没有被修正?!
- 我理解这种方法的动机可能只是为了解决IDE的问题,但是您在定义friend函数时给出了一个关于冗余的合理的论证。为什么这不是默认的实现方法?这仅仅是一个C++哲学的问题,还是偶然的,EDCOX1?0?除了类本身之外,其他人调用swap是一种常见的情况吗?
- @Villasv请参见stackoverflow.com/questions/5695548/&hellip;
当你处理C++ 11风格的分配器知道容器时,我想添加一个警告词。交换和分配有着微妙的不同语义。
为了具体起见,让我们考虑一个容器std::vector,其中A是某种状态分配器类型,我们将比较以下函数:
1 2 3 4 5 6 7 8 9 10
| void fs(std::vector<T, A> & a, std::vector<T, A> & b)
{
a.swap(b);
b.clear(); // not important what you do with b
}
void fm(std::vector<T, A> & a, std::vector<T, A> & b)
{
a = std::move(b);
} |
fs和fm功能的目的是使A具有b最初的状态。然而,有一个隐藏的问题:如果a.get_allocator() != b.get_allocator()会发生什么?答案是:视情况而定。让我们写一篇文章。
如果AT::propagate_on_container_move_assignment是std::true_type,那么fm用b.get_allocator()的值重新分配A的分配器,否则不重新分配,A继续使用原来的分配器。在这种情况下,由于A和b的存储不兼容,因此需要单独交换数据元素。
如果AT::propagate_on_container_swap是std::true_type,那么fs以预期的方式交换数据和分配器。
如果AT::propagate_on_container_swap是std::false_type,则需要进行动态检查。
- 如果是a.get_allocator() == b.get_allocator(),那么这两个容器使用兼容的存储,并以通常的方式交换。
- 但是,如果a.get_allocator() != b.get_allocator(),程序具有未定义的行为(参见[container.requirements.general/8]。
结果是,一旦容器开始支持有状态分配器,交换就成为C++ 11中的一个非平凡操作。这是一个有点"高级用例",但并非完全不可能,因为移动优化通常只有在类管理资源时才会变得有趣,而内存是最流行的资源之一。