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

Avoid repeating the same code in copy constructor and operator=

提问人:John Bumper 提问时间:7/10/2013 最后编辑:David GJohn Bumper 更新时间:7/13/2013 访问量:2374

问:

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

我的问题是,如何在不重复相同代码行的情况下以最佳方式编写此代码,并且不需要处理器执行不必要的工作(例如不必要的复制)。

我通常最终会得到两种帮助方法。一个用于建设,一个用于破坏。第一个是从 copy constructor 和 operator= 调用的。第二个由 destructor 和 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();
        }
    };

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

C++ 析构函数 复制构造函数赋 值运算符 代码共享

评论

0赞 ToolmakerSteve 7/10/2013
+1,因为这个问题有助于提高我的意识。在阅读答案之前,它看起来像我会写的东西。
0赞 PlasmaHH 7/10/2013
嗯,我很少在两者中重复代码,因为它们都做完全不同的事情:一个初始化,一个赋值......
0赞 ToolmakerSteve 7/10/2013
正是他的类设计的“深度复制”性质导致了重复。
0赞 James Kanze 7/10/2013
@PlasmaHH 视情况而定。想想一个简单的字符串或向量类,使用深度复制语义。(重复代码的数量是否足以证明其他函数的合理性是一个不同的问题。如果只是一个简单的,可能不值得为单独的功能而烦恼。new
1赞 Mooing Duck 7/13/2013
这就是我本来会做的,做所有的工作。assignclearswap

答:

0赞 doomster 7/10/2013 #1

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

foo&
foo::operator=(foo const& rhs)
{
   using std::swap;
   foo tmp(rhs);
   swap(*this, tmp);
   return *this;
}

评论

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

我看不出这有什么固有的问题,只要你确保不声明构造或破坏虚拟。

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

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

评论

1赞 James Kanze 7/10/2013
除了它不是例外安全。如果 in 抛出(或复制抛出),则对象处于不连贯状态,在这种状态下,破坏它将导致未定义的行为。newconstruct
0赞 Stefano Falasca 7/10/2013
@JamesKanze 你当然是对的,但问题是关于避免代码重复的技术,我认为这种技术没有固有的问题。
0赞 James Kanze 7/10/2013
它没有固有的问题,只是它不起作用。异常安全不是可选功能;如果程序要正确,这是必不可少的。
7赞 James Kanze 7/10/2013 #3

一个初步评论:不是以 破坏,但通过建造。否则,它将离开 如果构造通过 例外。因此,您的代码不正确。(请注意, 测试自我分配的必要性通常表明 赋值运算符不正确operator=

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

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

(其中 是执行大部分操作的成员函数 您的返回,但返回指针,并且不返回 修改 中的任何内容。cloneDataconstructthis

编辑:

与您最初的问题没有直接关系,但通常,在 在这种情况下,你不想做一个 in(或 ,或其他什么)。这将构造所有 ,然后分配它们。 这样做的惯用方式是这样的:new T[count]cloneDataconstructT

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

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

评论

0赞 SChepurin 7/10/2013
这是革命性的东西,价值超过一个+-“请注意,测试自赋值的必要性通常表明赋值运算符不正确。
0赞 ForeverLearning 7/11/2013
@James Kanze:一个同事遇到的一个案例是,你的分配操作员必须对其资源之一进行memcpy。在那种情况下,自我分配就成为一种必要,不是吗?
0赞 James Kanze 7/11/2013
@Dilip 或者他可以做,或者(也许)或者可能只是在 POD 结构之间分配。我想不出任何“必需”的情况,甚至我想不出我会在 C++ 中使用它的地方。(在实践中,如果传递给它的两个地址相同,则将起作用,但正式地说,这是未定义的行为。memmovestd::copymemcpymemcpy