在C ++的三规则中,为什么operator =不能调用copy ctor?

时间:2013-10-11 19:07:52

标签: c++ copy-constructor

以下“最小”示例应显示rule of 3 (and a half)的使用。

#include <algorithm>
#include <iostream>

class C
{
    std::string* str;
public:
    C()
        : str(new std::string("default constructed"))
    {
        std::cout << "std ctor called" << std::endl;
    }
    C(std::string* _str)
        : str(_str) 
    {
        std::cout << "string ctor called, "
            << "address:" << str << std::endl;
    }
    // copy ctor: does a hard copy of the string
    C(const C& other)
        : str(new std::string(*(other.str)))
    {
        std::cout << "copy ctor called" << std::endl;
    }

    friend void swap(C& c1, C& c2) {
        using std::swap;
        swap(c1.str, c2.str); 
    }

    const C& operator=(C src) // rule of 3.5
    {
        using std::swap;
        swap(*this, src);
        std::cout << "operator= called" << std::endl;
        return *this;
    }

    C get_new() {
        return C(str);
    }
    void print_address() { std::cout << str << std::endl; }
};

int main()
{
    C a, b;
    a = b.get_new();
    a.print_address();
    return 0;
}

像这样编译(g ++版本:4.7.1):

g++ -Wall test.cpp -o test

现在,应该怎么办?我假设行a = b.get_new();会制作一个硬拷贝,即分配一个新字符串。原因:operator=()采用其参数(在此设计模式中为典型值),每个值调用一个副本ctor,它将进行深层复制。真的发生了什么?

std ctor called
std ctor called
string ctor called, address:0x433d0b0
operator= called
0x433d0b0

副本ctor 从不被调用,因此,副本是软的 - 两个指针都是相同的。为什么没有调用副本?

3 个答案:

答案 0 :(得分:4)

副本正在被删除。

没有副本,因为b.get_new();正在最终成为C参数的位置构建其“临时”operator=对象。编译器能够管理这个,因为所有内容都在一个翻译单元中,所以它有足够的信息来进行这样的转换。

您可以使用标记-fno-elide-constructors消除clang和gcc中的构造省略,然后输出将如下:

std ctor called
std ctor called
string ctor called, address:0x1b42070
copy ctor called
copy ctor called
operator= called
0x1b420f0

返回值优化消除了第一个副本。使用RVO,该函数构造最终直接返回到返回值的位置的对象。

我不确定第二份副本是否有特殊名称。这是get_new()的返回值到operator= ()的参数的副本。

正如我之前所说,将两个副本同时删除会导致get_new()将对象直接构建到operator= ()参数的空间中。


请注意,两个指针都相同,如:

std ctor called
std ctor called
string ctor called, address:0xc340d0
operator= called
0xc340d0

本身并不表示错误,这不会导致双重释放;由于副本已被删除,因此没有该对象的额外副本保留对分配的字符串的所有权,因此不会有额外的免费。

但是,您的代码确实包含与规则三无关的错误:get_new()正在传递指向对象自己的str成员的指针,以及它创建的显式对象(在“string ctor”行在输出中调用地址:0xc340d0“取得原始对象(str)已管理的b对象的所有权。这意味着bget_new()内创建的对象都试图管理相同的字符串,这将导致双重释放(如果实现了析构函数)。

要查看此更改,请更改默认构造函数以显示其创建的str

C()
    : str(new std::string("default constructed"))
{
    std::cout << "std ctor called. Address: " << str << std::endl;
}

现在输出结果如下:

std ctor called. Address: 0x1cdf010
std ctor called. Address: 0x1cdf070
string ctor called, address:0x1cdf070
operator= called
0x1cdf070

所以打印的最后两个指针没有问题。问题在于打印第二个和第三个指针。修复get_new()

C get_new() {
    return C(new std::string(*str));
}

将输出更改为:

std ctor called. Address: 0xec3010
std ctor called. Address: 0xec3070
string ctor called, address:0xec30d0
operator= called
0xec30d0

并解决双重释放的任何潜在问题。

答案 1 :(得分:3)

允许C ++在返回类实例的函数中优化复制构造。

get_new中发生的事情是,直接返回从_str成员新构造的对象,然后将其用作赋值的来源。这称为“返回值优化”(RVO)。

请注意,虽然编译器可以自由地优化复制结构,但仍需要检查是否可以合法地调用复制结构。例如,如果代替成员函数,你有一个非友元函数返回和实例,并且复制构造函数是私有的,那么即使在使函数可访问后,你也会得到编译器错误,副本可能最终被优化掉了。

答案 2 :(得分:1)

为什么您希望使用复制ctor并不完全清楚。 get_new()函数在返回值时不会创建C对象的新副本。这是一个名为Return Value Optimization的优化,任何C ++编译器都会实现它。