以下“最小”示例应显示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 从不被调用,因此,副本是软的 - 两个指针都是相同的。为什么没有调用副本?
答案 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
对象的所有权。这意味着b
和get_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 ++编译器都会实现它。