关于c ++:在赋值运算符中使用复制构造函数

Using copy constructor in assignment operator

在赋值运算符中使用复制构造函数是否违反样式指南?即。:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
const Obj & Obj::operator=(const Obj & source)
{
    if (this == &source)
    {
        return *this;
    }

    // deep copy using copy-constructor
    Obj * copy = new Obj(source);

    // deallocate memory
    this->~Obj();

    // modify object
    *this = *copy;

    return *copy;
}

假设复制构造函数对对象执行深度复制。

编辑:

正如评论家指出的那样,我的代码是极其错误的。

至于总体概念性问题:正如whozcraig所建议的,复制/交换习语似乎是解决问题的方法:什么是复制和交换习语?


下面简单介绍一下示例中的复制/交换习惯用法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
#include

class Obj
{
    int *p;
    void swap(Obj& left, Obj& right);

public:
    Obj(int x = 0) : p(new int(x)) {}
    Obj(const Obj& s);
    Obj& operator = (const Obj& s);
    ~Obj() { delete p; }
};

Obj::Obj(const Obj& source) : p(new int(*source.p))
{}

void Obj::swap(Obj& left, Obj& right)
{
    std::swap(left.p, right.p);
}

Obj & Obj::operator=(const Obj & source)
{
    Obj temp(source);
    swap(*this, temp);
    return *this;
}

int main()
{
    Obj o1(5);
    Obj o2(o1);
    Obj o3(10);
    o1 = o3;
}

为了了解它是如何工作的,我特意创建了一个成员,它是指向动态分配内存的指针(如果没有用户定义的复制构造函数和分配运算符,这将是有问题的)。

如果您关注赋值操作符,它会调用Obj复制构造函数来构造临时对象。然后,特定于Objswap被称为交换单个成员。现在,魔法出现在temp对象中,在swap被调用之后。

当调用temp的析构函数时,它将根据this以前的指针值调用delete,但已与temp指针交换。所以当temp超出作用域时,它清除了"旧"指针分配的内存。

另外,请注意,在赋值期间,如果new在创建临时对象时抛出异常,则赋值将在this的任何成员发生更改之前抛出异常。这样可以防止对象中的成员因意外更改而损坏。

现在,前面给出了一个使用常用的"共享代码"方法复制分配的答案。以下是此方法的完整示例,并解释了其存在问题的原因:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
class Obj
{
    int *p;
    void CopyMe(const Obj& source);

public:
    Obj(int x = 0) : p(new int(x)) {}
    Obj(const Obj& s);
    Obj& operator = (const Obj& s);
    ~Obj() { delete p; }
};

void Obj::CopyMe(const Obj& source)
{
    delete p;
    p = new int(*source.p);
}

Obj::Obj(const Obj& source) : p(0)
{
   CopyMe(source);
}

Obj & Obj::operator=(const Obj & source)
{
    if ( this != &source )
        CopyMe(source);
    return *this;
}

所以你会说"这是怎么回事?"好吧,问题是CopyMe的第一个功能是称为delete p;。接下来你要问的问题是"那又是什么?这不是我们应该做的吗,删除旧的记忆?

问题在于,后续调用new可能会失败。所以我们所做的就是在我们知道新数据可用之前销毁我们的数据。如果new现在抛出一个异常,我们就把对象弄乱了。

是的,您可以通过创建一个临时指针,分配来轻松地修复它,最后,将临时指针分配给p。但很多时候,这可以被遗忘,而上面这样的代码永远保留在代码库中,即使它有潜在的腐败缺陷。

举例来说,这里是CopyMe的修复:

1
2
3
4
5
6
void Obj::CopyMe(const Obj& source)  
{
    int *pTemp = new int(*source.p);
    delete p;
    p = pTemp;
}

但同样,您将看到大量使用"共享代码"方法的代码,这些方法有这个潜在的bug,并且没有提到异常问题。


你想做的事行不通。您似乎认为赋值运算符返回的对象变成了新的"this",但事实并非如此。您没有修改对象,只是将其销毁。

1
2
3
4
5
6
Obj a;
Obj b;
a = b; // a has been destroyed
       // and you've leaked a new Obj.
// At the end of the scope, it will try to destroy `a` again, which
// is undefined behavior.

使用placement new可以实现赋值运算符,但它是一个脆弱的解决方案,一般不建议:

1
2
3
4
5
6
7
8
9
10
11
12
13
const Obj & Obj::operator=(const Obj & source)
{
    if (this == &source)
    {
        return *this;
    }

    this->~Obj();

    new (this) Obj(source);

    return *this;
}

如果构造过程中可能出现异常,这将导致问题,并且可能导致派生类出现问题。


在复制构造函数和assigmnet运算符之间共享代码是完全有意义的,因为它们通常执行相同的操作(将对象作为参数属性传递给它)。

个人而言,我经常通过巧妙地对赋值运算符进行编码,然后从复制构造函数调用它来实现这一点:

1
2
3
4
5
6
7
8
9
10
11
12
13
Obj::Obj(const Obj & source)
{
    Obj::operator=( source );
}

const Obj& Obj::operator=(const Obj& source)
{
    if (this != &source)
    {
        // copy source attribtes to this.
    }    
    return *this;
}

如果您正确地编写operator=,它就会工作。如注释所示,可以建议在C++中使用两个复制函数:复制构造函数和=运算符重载:可能的公共函数吗?

无论如何,您在两个函数之间共享代码的想法是好的,但是您实现它的方式却不好。一定要工作。它有很多问题,不能按你的意思去做。它以递归方式调用运算符。此外,您不应该像以前那样明确地调用析构函数(this->~Obj();)。