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

时间:2012-06-26 13:47:49

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

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

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

1 - 您是否应该在我们为复制构造函数中的深层副本分配新内存的区域周围try - catch? (我已经看到了两种方式)

2 - 关于复制构造函数和赋值运算符的继承,何时应该调用基类函数,何时这些函数应该是虚函数?

3 - std::copy是复制构造函数中复制内存的最佳方法吗?我在memcpy看过它,看到其他人说memcpy是地球上最糟糕的事情。


考虑以下示例(感谢所有反馈),它提示了一些其他问题:

4 - 我们应该检查自我分配吗?如果是这样的话

5 - 关闭主题问题,但我看过swapped用作: std::copy(Other.Data,Other.Data + size,Data); 它应该是: std::copy(Other.Data,Other.Data + (size-1),Data); 如果swap从'First到Last'而第0个元素是Other.Data?

6 - 为什么注释掉的构造函数不起作用(我必须将大小更改为mysize) - 假设这意味着无论我编写它们的顺序如何,构造函数将始终首先调用分配元素?

7 - 对我的实施还有其他评论吗?我知道代码没用,但我只想说明一点。

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

};

4 个答案:

答案 0 :(得分:4)

  1. 如果分配内存,则需要确保在抛出异常时释放内存。您可以使用显式try / catch执行此操作,也可以使用智能指针(如std::unique_ptr)来保存内存,然后在智能指针被销毁时自动删除内存通过堆栈展开。

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

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

  4. [更新更新问题]

    1. 使用copy / swap编写时,无需检查自我赋值,实际上没有办法 - 当你输入赋值运算符时other复制,你无法知道源对象是什么。这只意味着自我分配仍然会进行复制/交换。

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

    3. 注释掉的构造函数不起作用,因为成员按声明的顺序初始化,而不管成员初始化列表中的顺序如何。因此,Data始终首先被初始化。如果初始化取决于size,那么它将获得一个duff值,因为size尚未初始化。如果你交换sizedata的声明,那么这个构造函数将正常工作。好的编译器会警告成员初始化的顺序与声明的顺序不匹配。

答案 1 :(得分:3)

  

1 - 您是否应该在我们为复制构造函数中的深层副本分配新内存的区域周围尝试?

一般情况下,如果可以处理,则应该只捕获异常。如果你有办法在本地处理内存不足的情况,那么赶上它;否则,就放手吧。

如果构造失败,你肯定不会从构造函数中正常返回 - 这会使调用者无效,并且无法知道它是无效的。

  

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就不会破坏)并且可能更快(因为对象大小和对齐在编译时是已知的。)

答案 2 :(得分:1)

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

答案 3 :(得分:1)

    当您必须撤消某些内容时,可以使用
  1. try-catch。否则,只需让bad_alloc传播给调用者。

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

  3. std::copy的优点是可以正确复制类对象。 memcpy相对于它可以处理的类型而言非常有限。