复制构造函数和赋值运算符实现选择 -

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 尚未初始化。如果您交换 sizedata 的声明,则此构造函数将正常工作。好的编译器会警告成员初始化的顺序与声明的顺序不匹配。

【讨论】:

感谢 - 更新示例【参考方案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) 成员总是按照它们在类定义中声明的顺序进行初始化,所以Datasize之前初始化。【参考方案3】:

    try-catch 可以在您必须撤消某些操作时使用。否则,就让bad_alloc 传播给调用者。

    调用基类的复制构造函数或赋值运算符是让处理其复制的标准方法。我从未见过虚拟赋值运算符的用例,所以我猜它们很少见。

    std::copy 具有正确复制类对象的优点。 memcpy 可以处理的类型相当有限。

【讨论】:

【参考方案4】:
    如果你正在深度复制的构造函数可能会抛出一些东西 你可以处理,继续抓住它。我只是让记忆 不过,分配异常会传播。 复制构造函数(或任何构造函数)不能是虚拟的。包括一个 这些的基类初始化器。复制赋值运算符应该 委托给基类,即使它们是虚拟的。 memcpy() 对于在 C++ 中复制类类型来说太低级,并且可能导致未定义的行为。我认为std::copy 通常是更好的选择。

【讨论】:

以上是关于复制构造函数和赋值运算符实现选择 -的主要内容,如果未能解决你的问题,请参考以下文章

克隆() vs 复制构造函数 vs 工厂方法?

如何在 Python 中创建多个构造函数? [复制]

我可以将复制构造函数设为私有并仍然使用默认实现吗

设计模式-Prototype(通过复制构造函数实现自我复制)-(创建型模式)

关于复制构造函数的几个问题

C++——构造函数析构函数以及复制构造函数