复制构造函数和赋值运算符实现选择 -
Posted
技术标签:
【中文标题】复制构造函数和赋值运算符实现选择 -【英文标题】:Copy constructor and assignment operator implementation choices - 【发布时间】:2012-06-27 20:32:25 【问题描述】:我最近重新访问了这里看到的复制构造函数、赋值运算符、复制交换 idom: What is the copy-and-swap idiom? 和许多其他地方 -
上面的链接是一个很好的帖子 - 但我还有一些问题 - 这些问题在很多地方都得到了解答,在 *** 和许多其他网站上,但我没有看到很多一致性 -
1 - 您是否应该在复制构造函数中为深拷贝分配新内存的区域周围设置try
-catch
? (我已经看到了两种方式)
2 - 关于复制构造函数和赋值运算符的继承,什么时候应该调用基类函数,什么时候这些函数应该是虚拟的?
3 - std::copy
是在复制构造函数中复制内存的最佳方式吗?我见过memcpy
,也见过别人说memcpy
是地球上最糟糕的事情。
考虑下面的示例(感谢所有反馈),它提示了一些其他问题:
4 - 我们应该检查自我分配吗?如果是在哪里
5 - 题外话,但我已经看到交换用作:
std::copy(Other.Data,Other.Data + size,Data);
应该是:
std::copy(Other.Data,Other.Data + (size-1),Data);
如果交换从“第一个到最后一个”并且第 0 个元素是 Other.Data?
6 - 为什么注释掉的构造函数不起作用(我必须将大小更改为 mysize) - 假设这意味着无论我编写它们的顺序如何,构造函数总是首先调用分配元素?
7 - 我的实现中还有其他 cmets 吗?我知道代码没用,但我只是想说明一点。
class TBar
public:
//Swap Function
void swap(TBar &One, TBar &Two)
std::swap(One.b,Two.b);
std::swap(One.a,Two.a);
int a;
int *b;
TBar& operator=(TBar Other)
swap(Other,*this);
return (*this);
TBar() : a(0), b(new int) //We Always Allocate the int
TBar(TBar const &Other) : a(Other.a), b(new int)
std::copy(Other.b,Other.b,b);
*b = 22; //Just to have something
virtual ~TBar() delete b;
;
class TSuperFoo : public TBar
public:
int* Data;
int size;
//Swap Function for copy swap
void swap (TSuperFoo &One, TSuperFoo &Two)
std::swap(static_cast<TBar&>(One),static_cast<TBar&>(Two));
std::swap(One.Data,Two.Data);
std::swap(One.size,Two.size);
//Default Constructor
TSuperFoo(int mysize = 5) : TBar(), size(mysize), Data(new int[mysize])
//TSuperFoo(int mysize = 5) : TBar(), size(mysize), Data(new int[size]) *1
//Copy Constructor
TSuperFoo(TSuperFoo const &Other) : TBar(Other), size(Other.size), Data(new int[Other.size]) // I need [Other.size]! not sizw
std::copy(Other.Data,Other.Data + size,Data); // Should this be (size-1) if std::copy is First -> Last? *2
//Assignment Operator
TSuperFoo& operator=(TSuperFoo Other)
swap(Other,(*this));
return (*this);
~TSuperFoo() delete[] Data;
;
【问题讨论】:
【参考方案1】:如果分配内存,则需要确保在引发异常的情况下释放它。您可以使用显式的try
/catch
来执行此操作,也可以使用诸如std::unique_ptr
之类的智能指针来保存内存,然后当智能指针被堆栈展开销毁时,该内存将被自动删除。
您很少需要virtual
赋值运算符。在成员初始化列表中调用基类复制构造函数,如果您正在执行成员赋值,则首先在派生赋值运算符中调用基类赋值运算符 --- 如果您正在执行复制/交换,那么您不需要调用派生赋值运算符中的基类赋值,前提是正确实现了复制和交换。
std::copy
与对象一起工作,并且会正确调用复制构造函数。如果你有普通的 POD 对象,那么 memcpy
也可以工作。不过,在大多数情况下,我会选择 std::copy
--- 对于 POD,它应该在底层优化为 memcpy
,并且如果您稍后添加复制构造函数,它可以避免出现错误的可能性。
[更新问题的更新]
按照所写的复制/交换,无需检查自分配,而且确实没有办法这样做 --- 到您输入分配运算符时,other
是一个副本,你无法知道源对象是什么。这只是意味着自分配仍然会进行复制/交换。
std::copy
将一对迭代器 (first, first+size) 作为输入。这允许空范围,并且与标准库中每个基于范围的算法相同。
注释掉的构造函数不起作用,因为成员按照声明它们的顺序进行初始化,而不管成员初始化器列表中的顺序如何。因此,Data
总是首先被初始化。如果初始化依赖于size
,那么它将获得一个 duff 值,因为size
尚未初始化。如果您交换 size
和 data
的声明,则此构造函数将正常工作。好的编译器会警告成员初始化的顺序与声明的顺序不匹配。
【讨论】:
感谢 - 更新示例【参考方案2】:1 - 您是否应该在复制构造函数中为深拷贝分配新内存的区域进行 try-catch?
一般来说,您应该只在可以处理的情况下捕获异常。如果你有办法在本地处理内存不足的情况,那就抓住它;否则,放手。
如果构造失败,你当然不应该从构造函数中正常返回——这会使调用者得到一个无效的对象,并且无法知道它是无效的。
2 - 关于复制构造函数和赋值运算符的继承,什么时候应该调用基类函数,什么时候这些函数应该是虚拟的?
构造函数不能是虚函数,因为虚函数只能由对象调度,并且在创建它之前没有对象。通常,您也不会将赋值运算符设为虚拟;可复制和可赋值类通常被视为非多态“值”类型。
通常,您会从初始化列表中调用基类复制构造函数:
Derived(Derived const & other) : Base(other), <derived members>
如果您使用的是复制和交换习语,那么您的赋值运算符就不需要担心基类;这将由交换处理:
void swap(Derived & a, Derived & b)
using namespace std;
swap(static_cast<Base&>(a), static_cast<Base&>(b));
// and swap the derived class members too
Derived & Derived::operator=(Derived other)
swap(*this, other);
return *this;
3 -
std::copy
是在复制构造函数中复制内存的最佳方式吗?我见过memcopy
,也见过别人说memcopy
是地球上最糟糕的事情。
处理原始内存是相当不寻常的;通常您的类包含对象,并且通常无法通过简单地复制其内存来正确复制对象。您使用对象的复制构造函数或赋值运算符来复制对象,std::copy
将使用赋值运算符来复制对象数组(或更一般地说,是对象序列)。
如果你真的想要,你可以使用memcpy
来复制POD(普通旧数据)对象和数组;但是std::copy
更不容易出错(因为您不需要提供对象大小),更不易碎(因为如果您将对象更改为非 POD,它不会损坏)并且可能更快(因为对象大小和对齐在编译时是已知的)。
【讨论】:
谢谢 - 我用一个例子更新了这个问题并进行了一些进一步的讨论,请看一下 @MikeyG:这是相当多的问题;你最好分开问他们。简而言之:(4)如果您使用的是复制和交换,则不需要处理自我分配; (5)std::copy
(以及一般采用迭代器范围的函数)期望“结束”迭代器过去序列的结尾; (6) 成员总是按照它们在类定义中声明的顺序进行初始化,所以Data
在size
之前初始化。【参考方案3】:
try-catch
可以在您必须撤消某些操作时使用。否则,就让bad_alloc
传播给调用者。
调用基类的复制构造函数或赋值运算符是让处理其复制的标准方法。我从未见过虚拟赋值运算符的用例,所以我猜它们很少见。
std::copy
具有正确复制类对象的优点。 memcpy
可以处理的类型相当有限。
【讨论】:
【参考方案4】:-
如果你正在深度复制的构造函数可能会抛出一些东西
你可以处理,继续抓住它。我只是让记忆
不过,分配异常会传播。
复制构造函数(或任何构造函数)不能是虚拟的。包括一个
这些的基类初始化器。复制赋值运算符应该
委托给基类,即使它们是虚拟的。
memcpy()
对于在 C++ 中复制类类型来说太低级,并且可能导致未定义的行为。我认为std::copy
通常是更好的选择。
【讨论】:
以上是关于复制构造函数和赋值运算符实现选择 -的主要内容,如果未能解决你的问题,请参考以下文章