【问题标题】:Copy constructor and assignment operator implementation choices -复制构造函数和赋值运算符实现选择 -
【发布时间】:2012-06-27 20:32:25
【问题描述】:

我最近重新访问了这里看到的复制构造函数、赋值运算符、复制交换 idom: What is the copy-and-swap idiom? 和许多其他地方 -

上面的链接是一个很好的帖子 - 但我还有一些问题 - 这些问题在很多地方都得到了解答,在 stackoverflow 和许多其他网站上,但我没有看到很多一致性 -

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;}

};

【问题讨论】:

    标签: c++ inheritance constructor assignment-operator deep-copy


    【解决方案1】:
    1. 如果分配内存,则需要确保在引发异常的情况下释放它。您可以使用显式的try/catch 来执行此操作,也可以使用诸如std::unique_ptr 之类的智能指针来保存内存,然后当智能指针被堆栈展开销毁时,该内存将被自动删除。

    2. 您很少需要virtual 赋值运算符。在成员初始化列表中调用基类复制构造函数,如果您正在执行成员赋值,则首先在派生赋值运算符中调用基类赋值运算符 --- 如果您正在执行复制/交换,那么您不需要调用派生赋值运算符中的基类赋值,前提是正确实现了复制和交换。

    3. std::copy 与对象一起工作,并且会正确调用复制构造函数。如果你有普通的 POD 对象,那么 memcpy 也可以工作。不过,在大多数情况下,我会选择 std::copy --- 对于 POD,它应该在底层优化为 memcpy,并且如果您稍后添加复制构造函数,它可以避免出现错误的可能性。

    [更新问题的更新]

    1. 按照所写的复制/交换,无需检查自分配,而且确实没有办法这样做 --- 到您输入分配运算符时,other 是一个副本,你无法知道源对象是什么。这只是意味着自分配仍然会进行复制/交换。

    2. std::copy 将一对迭代器 (first, first+size) 作为输入。这允许空范围,并且与标准库中每个基于范围的算法相同。

    3. 注释掉的构造函数不起作用,因为成员按照声明它们的顺序进行初始化,而不管成员初始化器列表中的顺序如何。因此,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】:
    1. try-catch 可以在您必须撤消某些操作时使用。否则,就让bad_alloc 传播给调用者。

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

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

    【讨论】:

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

      【讨论】:

        猜你喜欢
        • 2011-06-09
        • 2011-05-19
        • 2011-07-19
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2013-04-13
        相关资源
        最近更新 更多