避免在复制构造函数和 operator= 中重复相同的代码

Posted

技术标签:

【中文标题】避免在复制构造函数和 operator= 中重复相同的代码【英文标题】:Avoid repeating the same code in copy constructor and operator= 【发布时间】:2021-06-13 00:45:26 【问题描述】:

在 C++ 中,当类包含动态分配的数据时,显式定义复制构造函数、operator= 和析构函数通常是合理的。但是这些特殊方法的活动是重叠的。更具体地说,operator= 通常首先进行一些破坏,然后进行类似于复制构造函数中的处理。

我的问题是如何以最好的方式编写此代码,而无需重复相同的代码行,也无需处理器做不必要的工作(如不必要的复制)。

我通常会得到两种帮助方法。一种用于建设,一种用于破坏。第一个是从复制构造函数和 operator= 调用的。第二个由析构函数和运算符 = 使用。

示例代码如下:

    template <class T>
    class MyClass
    
        private:
        // Data members
        int count;
        T* data; // Some of them are dynamicly allocated
        void construct(const MyClass& myClass)
        
            // Code which does deep copy
            this->count = myClass.count;
            data = new T[count];
            try
            
                for (int i = 0; i < count; i++)
                    data[i] = myClass.data[i];
            
            catch (...)
            
                delete[] data;
                throw;
            
        
        void destruct()
        
            // Dealocate all dynamicly allocated data members
            delete[] data;
        
        public: MyClass(int count) : count(count)
        
            data = new T[count];
        
        MyClass(const MyClass& myClass)
        
            construct(myClass);
        
        MyClass& operator = (const MyClass& myClass)
        
            if (this != &myClass)
            
                destruct();
                construct(myClass);
            
            return *this;
        
        ~MyClass()
        
            destruct();
        
    ;

这是否正确? 以这种方式拆分代码是一个好习惯吗?

【问题讨论】:

+1 因为这个问题有助于提高我的认识。看起来像是我在阅读答案之前会写的东西。 嗯,我很少在两者中重复代码,因为它们都做完全不同的事情:一个初始化,一个分配...... 其课程设计的“深拷贝”性质导致重复。 @PlasmaHH 这取决于。考虑一个简单的字符串或向量类,使用深拷贝语义。 (重复代码的数量是否足以证明额外的功能是另一个问题。如果它只是一个简单的new,那么单独的功能可能不值得费心。) This is what I would have done assignclearswap 完成所有工作。 【参考方案1】:

一个初始评论:operator= 确实不是开始于 破坏,但通过构建。否则,它将离开 如果构造通过 a 终止,则对象处于无效状态 例外。因此,您的代码不正确。 (注意 测试自我分配的必要性通常表明 赋值运算符正确。)

处理这个问题的经典解决方案是交换成语:you 添加成员函数swap:

void MyClass:swap( MyClass& other )

    std::swap( count, other.count );
    std::swap( data, other.data );

保证不扔。 (在这里,它只是交换一个 int 和一个指针,两者都不能抛出。)然后你 将赋值运算符实现为:

MyClass& MyClass<T>::operator=( MyClass const& other )

    MyClass tmp( other );
    swap( tmp );
    return *this;

这很简单直接,但任何解决方案都可以 所有可能失败的操作都在你开始之前完成 更改数据是可以接受的。对于像您这样的简单案例 代码,例如:

MyClass& MyClass<T>::operator=( MyClass const& other )

    T* newData = cloneData( other.data, other.count );
    delete data;
    count = other.count;
    data = newData;
    return *this;

(cloneData 是一个成员函数,它完成了大部分工作 你的 construct 会,但会返回指针,但不会 修改this中的任何内容。

编辑:

与您最初的问题没有直接关系,但通常在 在这种情况下,您确实想要在new T[count] cloneData(或construct,或其他)。这构建了所有 T 的默认构造函数,然后分配它们。 这样做的惯用方式是这样的:

T*
MyClass<T>::cloneData( T const* other, int count )

    //  ATTENTION! the type is a lie, at least for the moment!
    T* results = static_cast<T*>( operator new( count * sizeof(T) ) );
    int i = 0;
    try 
        while ( i != count ) 
            new (results + i) T( other[i] );
            ++ i;
        
     catch (...) 
        while ( i != 0 ) 
            -- i;
            results[i].~T();
        
        throw;
    
    return results;

大多数情况下,这将使用单独的(私人)经理来完成 类:

//  Inside MyClass, private:
struct Data

    T* data;
    int count;
    Data( int count )
        : data( static_cast<T*>( operator new( count * sizeof(T) ) )
        , count( 0 )
    
    
    ~Data()
    
        while ( count != 0 ) 
            -- count;
            (data + count)->~T();
        
    
    void swap( Data& other )
    
        std::swap( data, other.data );
        std::swap( count, other.count );
    
;
Data data;

//  Copy constructor
MyClass( MyClass const& other )
    : data( other.data.count )

    while ( data.count != other.data.count ) 
        new (data.data + data.count) T( other.date[data.count] );
        ++ data.count;
    

(当然还有赋值的交换习语)。这允许 多个计数/数据对,没有任何丢失异常的风险 安全。

【讨论】:

这是一项革命性的价值不止一个 + - “请注意,测试自赋值的必要性通常表明赋值运算符不正确。” @James Kanze:一种情况(同事遇到)是您的分配操作员必须对其一个资源执行 memcpy。在那种情况下,自我分配成为必要,不是吗? @Dilip 或者他可以做memmove,或者(可能)std::copy,或者可能只是 POD 结构之间的分配。我想不出memcpy 是“必需”的任何情况,甚至想不出我会在C++ 中使用它的地方。 (实际上,如果传递给它的两个地址相同,memcpy 将起作用,但从形式上讲,这是未定义的行为。)【参考方案2】:

我认为这没有任何内在问题,只要您确保不声明构造或破坏虚拟。

您可能对 Effective C++ (Scott Meyers) 中的第 2 章感兴趣,该章完全致力于构造函数、复制运算符和析构函数。

至于您的代码未按应有的方式处理的异常,请考虑更有效的 C++ (Scott Meyers) 中的第 10 条和第 11 条。

【讨论】:

除非它不是异常安全的。如果 construct 中的 new 抛出(或复制抛出),则对象处于不连贯状态,在这种状态下破坏它会导致未定义的行为。 @JamesKanze 当然你是对的,但问题是关于避免代码重复的技术,我认为这种技术没有固有的问题。 它没有固有的问题,除了它不起作用。异常安全不是可选功能;如果程序要正确,这是必不可少的。【参考方案3】:

通过首先复制右侧然后与之交换来实现分配。通过这种方式,您还可以获得异常安全,这是您上面的代码未提供的。否则,当 destruct() 成功后,construct() 失败时,您最终可能会导致容器损坏,因为成员指针引用了一些已释放的数据,并且在销毁时将再次释放,从而导致未定义的行为。

foo&
foo::operator=(foo const& rhs)

   using std::swap;
   foo tmp(rhs);
   swap(*this, tmp);
   return *this;

【讨论】:

如果构造失败,为什么以旧的(不再需要的)内容结束,而不是空容器?恕我直言,在空容器中复制失败会更干净。具体来说,空容器很容易在后面的代码中检测到;一个容器,其中包含它不应该再拥有的内容,以后更难检测到。 假设您想在出现此类故障时终止程序,无论哪种方式都可以正常工作,但是您仍然需要设置data = nullptr(原始代码无法做到这一点)。 @ToolmakerSteve 这取决于你想要什么。交换习语确保了完整的事务完整性;你要么成功,要么什么都没有改变。在大多数情况下,单个对象不需要完整的事务完整性;您必须保证的只是对象可以在失败的情况下被破坏。 (试图保证除了不变之外的任何实际状态可能不是很有用。) thx,是的,我刚刚意识到我在考虑一种不同的情况,最终结果是一个空指针。我明白现在在说什么;正如所写,最终结果是一个有内容但内容不完整的对象。 @juanchopanza 即使您实际上并不需要强保证,交换习语通常也是获得最低保证的最简单和最便宜的方法。

以上是关于避免在复制构造函数和 operator= 中重复相同的代码的主要内容,如果未能解决你的问题,请参考以下文章

根据 operator= 实现复制构造函数

Map::operator[] 总是需要一个默认的构造函数[重复]

复制构造函数和赋值运算符

在 C++ 中复制构造函数?

在 C++ 中编写复制构造函数和赋值运算符的清单

第10课 面向对象的增强(default/deleteoverride/final)