我遇到了一个关于C++中类构造的问题。我定义了一个类obj
,这个类中有两个成员和各种构造函数,其中包括默认构造函数、拷贝构造函数和移动构造函数。每个构造函数的函数体中都会输出一个字符串,表示这个构造函数被执行了。下面是类的定义:
class obj{
public:
string *e;
string *n;
obj():e(new string("123")),n(new string("456")){ //默认构造
cout<<"default"<<endl;
}
obj(string *s1, string *s2):e(s1),n(s2){ //非默认构造
cout<<"non default"<<endl;
}
obj(obj *t):e(t->e), n(t->n){ //用指针构造
cout<<"pointer"<<endl;
};
obj(obj& t):e(t.e), n(t.n){ //拷贝构造
cout<<"copy"<<endl;
};
obj(const obj& t):e(t.e), n(t.n){ //拷贝构造
cout<<"const copy"<<endl;
};
obj(obj &&t) noexcept :e(t.e), n(t.n) { //移动构造
cout<<"move"<<endl;;
}
obj(const obj &&t) noexcept :e(t.e), n(t.n) { //移动构造
cout<<"const move"<<endl;;
}
~obj(){
delete e;
delete n;
}
};
然后我在主函数中构造了一个名为o3
的obj
对象,构造参数是一个匿名的obj
对象,我想看看构造o3
时会调用哪个构造函数。代码如下:
int main(){
obj o3(obj(new string("qqq"), new string("zzz"))); //这属于什么构造???
cout<<*o3.e<<endl;
return 0;
}
为了防止编译器优化,我给g++指定了-o0
选项。最后执行结果如下:
non default
qqq
根据程序的执行结果来看,匿名类构造时调用了类的第二个构造函数,而o3
没有调用任何一个构造函数。但是程序最终打印出了o3
的成员,说明o3
被成功构造了。这令我百思不得其解,o3
是如何被构造出来的,调用了哪个构造函数?
程序是用gcc 7.4.0编译的,在ubuntu下执行。
Copy elision
====================
gcc 7.4.0 默认的标注是 gnu++14 ,基础标准是 c++14 。
C++ 14 里(C++14 draft n4140),定义了在若干情况下,编译器(作为一项优化)可以选择不调用(本应该被调用的)拷贝/移动构造函数,称作 copy elision 。
其中一种情况是:
这正是你现在的情况,一个临时对象被被拷贝/移动至一个同类型的变量。此时可以直接在目标对象中构造“临时”对象。
copy elision 可能是唯一一个标准特许的可能改变程序行为的优化。
copy elision 是可选的,gcc 可以使用
-fno-elide-constructors
阻止这一优化。加上之后,可以看到调用了 move constructor 。也因为其实可选的,即使发生 copy elision ,obj 类仍然需要一个可访问的 move constructor 。如果你把 move constructor 改成 private ,编译将不通过。
============================
C++17 (C++17 draft n4659)以后,情况发生了变化。 prvalue 跟 temporary object 的定义发生了一些变化。
obj(.....)
是一个 prvalue ,prvalue 不再是一个对象。prvalue 在必要的时候,可以生成一个临时对象,叫做 materialize 。
但是,在你的这个程序里,并不需要这个 prvalue 生成临时对象。
关于初始化,有如下规定:
用 prvalue 初始化同类型的对象的时候,不会发生 copy/move ,而是直接构造目标对象。
所以,在 C++17 里(可以用 -std=c++17 启用),即使加上
-fno-elide-constructors
也不会调用 copy/move constructor ,因为这已经不是一个编译器优化,而是语言本身在这里不需要 copy/move 。同时,即使将所有 copy/move constructor 全变成 private ,这个程序也可以正确通过编译,因为这里不需要它们。注:C++17 中同样存在 copy elision ,但是已经不再包含上述情形。