C++拷贝构造函数、无名临时对象以及NRV优化分析
来源:互联网 发布:c语言调用java接口 编辑:程序博客网 时间:2024/06/05 19:38
一:合成 Default Copy Constructor
那么什么时候编译器会为一个 class 生成一个 default copy constructor 呢?
和 default constructor 一样,C++ Standard 上说,如果 class 没有声明一个 copy constructor,就会有隐含的声明(implicitly declared)或隐含的定义(implicitly defined)出现,和以前一样,C++ Standard 把 copy constructor 区分为 trivial 和 nontrivial 两种,只有 nontrivial 的实体才会被合成于程序之中,决定一个 copy constructor 是否为 trivial 的标准在于 class 是否展现 “bitwise copy semantics”,即位逐次拷贝语义。如果没有展现,编译器将会为其合成 default copy constructor。
其实,bitwise copy = 浅拷贝,memberwise copy = 深拷贝 :)
什么时候一个 class 不展现出 “bitwise copy semantics” 呢,有四种情况:
- 当 class 内含一个 member object 而后者的 class 声明有一个 copy constructor 时
- 当 class 继承自一个 base class 而后者存在有一个 copy constructor 时
- 当 class 声明了一个或多个 virtual functions 时
- 当 class 派生自一个继承串链,其中有一个或多个 virtual base classes 时
无需合成的情况:
//简单的 bitwise copy 还不够//编译器必须明确地将 little_critter 的//virtual base class pointer/offset 初始化RedPanda little_red;Raccoon little_critter = little_red;
父类以子类内容做初始化操作情况:
ZooAnimal franny = yogi; //译注:这会发生切割(sliced)行为
当一个 base class object 以其 derived class 的 object 内容做初始化操作时,其 vptr 复制操作必须保证安全。父类的 vptr 不可以指向子类的 virtual table。因为父类声明一个对象需要调用自己的 virtual function,如果编译器不合成默认拷贝构造函数,那么父类的 vptr 会指向子类的 virtual table,再调用就会调用子类的 virtual function,这叫“炸毁”。
这种情况比较难理解,下面我来举个例子:
class base { public: base(int i) : int_base_(i) {} virtual void foo() { std::cout<<"base"<<std::endl; } virtual ~base() = default; protected: int int_base_; //data member also can be overrided }; class derived : public base { public: derived(int i) : int_(i), base(4) { } ~derived() = default; void foo() { std::cout<<"derived"<<std::endl; } private: int int_; }; void do_polymorphic(base& ptr) { ptr.foo(); } int main() { derived dr(5); base b = dr; do_polymorphic(b); return 0;}这段代码子类 override 了虚基类父类的 foo 方法,如果编译器没有为我们合成 default copy constructor 去为父类明确设定 vptr 指向父类自己的 virtual table ,而是按照 bitwise copy 的话,父类调用 foo 方法将会调用子类的 foo 方法,因为 bitwise copy 即浅拷贝的方式只是会将父类的 vptr 简单的指向子类的 virtual table(相当于指针赋值)。
但是输出结果是这样的:
处理 Virtual Base Class Subobject
//简单的 bitwise copy 还不够//编译器必须明确地将 little_critter 的//virtual base class pointer/offset 初始化RedPanda little_red;Raccoon little_critter = little_red;在这种情况下,为了完成正确的 little_critter 初值设定,编译器必须合成一个 copy constructor,安插一些码以设定 virtual base class pointer/offset 的初值(或者只是简单地确定它没有被抹消)。
二:程序转化语意学
关于临时对象的总结:
class base { public: base() { std::cout<<"base"<<std::endl; } base(int i) : int_base_(i) { std::cout<<"base"<<std::endl; } base(const base& othread) { std::cout<<"copy base"<<std::endl; } virtual void foo() const { std::cout<<"foo: base"<<std::endl; } virtual ~base() { std::cout<<"~base"<<std::endl; } protected: int int_base_; //data member also can be overrided };
1.使用一个临时对象来初始化一个新对象时,编译器一般会优化为直接使用临时对象的参数来创建新对象,实际上不会生成临时对象。
{ std::cout<<"x0 begin"<<std::endl; base x0 = base(5); //甚至 base x0(base(5)) 这种拷贝构造初始化也会被优化 std::cout<<"x0 end"<<std::endl; std::cout<<"main end"<<std::endl;}
2.使用一个临时对象来赋值一个对象时,临时对象创建完会直接被销毁掉。
{ base x0; std::cout<<"x0 begin"<<std::endl; x0 = base(5); std::cout<<"x0 end"<<std::endl; std::cout<<"main end"<<std::endl;}
3. 类对象作为函数参数按值传递时,使用临时对象作为函数使用的副本
// C++ 伪码,假如要调用 X xx; foo(xx);//编译器产生出来的临时对象X __temp0;//编译器对 copy ocnstructor 的调用__temp0.X::X( xx );//被调用的函数实际上为foo( __temp0 )测试如下:
void do_polymorphic(base ptr){ std::cout<<"fun begin"<<std::endl; ptr.foo(); std::cout<<"fun end"<<std::endl;}main(){ base b; std::cout<<"after declare"<<std::endl; do_polymorphic(b); std::cout<<"call fun end"<<std::endl; std::cout<<"main end"<<std::endl;}输出:
- 引用传递可以避免 __temp0 临时对象的产生,不会调用拷贝构造函数。
- __temp0 是在 main() 函数内,未调用 do_polymorphic() 函数前产生的,并且在该函数调用完毕后立即销毁,类似于上面的 2 。(用 GDB 可以调试查看)
返回值的初始化
X bar(){ X xx; //处理 xx ... return xx;}如何将 bar() 的返回值如何从局部对象 xx 中拷贝过来?解决方案是双阶段转化:
- 首先加上一个额外参数,类型是类的一个引用,这个参数用来放置被拷贝构造而得的返回值。
- 在 return 指令之前安插一个拷贝构造操作,以便将欲传回的对象的内容当作上述新增参数的初值。
//函数转换//以反应出 copy constructor 的应用//C++ 伪码void bar( X& __result ) //在这里加上额外参数{ X xx; //编译器所产生的 default constructor 操作 xx.X::X(); // ... 处理 xx //编译器所产生的 copy constructor 操作 __result.X::XX( xx ); return;}
//原语句X xx = bar();//编译器转化为以下两个指令句X xx;bar( xx );好的,上面这些就是返回值初始化的原理。
- 在程序员层面做优化,直接计算 __result。(其实就是类似 return X(); ,这种做法,返回的时候构造一次就可以了)。
- 编译器层面做优化,即 NRV(Name Return Value)优化。编译器可以在 return 指令传回相同的临时有名对象(即 xx 对象)的情况时,直接处理 __result,不生成对象xx。NRV 优化要求 class 必须具备拷贝构造函数。
base x0, x1;base x2 = add(x0, x1);如果编译器未开启NRV优化,会生成如下东东:
base __temp0; // 构造函数.add(__temp0, x0, x1);base (__temp0); // 拷贝构造函数.而开启NRV优化后,只会生成下列代码:
add(x2, x0, x1);所谓的NRV优化,即保存返回值的变量不再使用编译器内部生成的__temp0这样的东西,而是直接把 x2 直接作为返回变量。
书中说,NRV优化和拷贝构造函数是有关系的,只有定义了拷贝构造函数才会开启NRV优化,但现代编译器NRV优化的开启一般都与拷贝构造函数没有关系,下面一段话摘自网络,参考关于cfront的NRV优化,解释了为什么lippman在书中说有关的原因:
- “早期的 cfront需要一个开关来决定是否应该对代码实行NRV优化,这就是是否有客户(程序员)显式提供的拷贝构造函数:如 果客户没有显示提供拷贝构造函数,那么cfront认为客户对默认的逐位拷贝语义很满意,由于逐位拷贝本身就是很高效的,没必要再对其实施NRV优化;但 如果客户显式提供了拷贝构造函数,这说明客户由于某些原因(例如需要深拷贝等)摆脱了高效的逐位拷贝语义,其拷贝动作开销将增大,所以将应对其实施NRV 优化,其结果就是去掉并不必要的拷贝函数调用。”
- 需要说明的一点:NRV优化会导致原本预想中的调用“拷贝构造函数”变成调用别的“构造函数”,一旦这个时候,拷贝构造函数和别的构造函数提供的功能不同,就可能会出问题。
三:copy constuctor 要还是不要?
什么时候我们应该为 class 提供一个 explicit copy constructor 呢? 如果是编译器自动为你实施了最好的行为,那么就不需要自己定义 copy constructor,尤其是成员类型为 POD 类型的时候。不过,如果类的某个方法想要开启 NRV 优化,那倒是可以定义 copy constructor。甚至针对简单类型可以在 copy constructor 中直接用 memset 函数,但是memset 绝对不能用在 virtual class 中,因为别忘了 vptr 的存在。
四:成员初始化列表的队伍
- 当初始化一个 reference 成员时
- 当初始化一个 const 成员时
- 当调用一个基类的构造函数,而它拥有一组参数时
- 当调用一个成员类的构造函数,而它拥有一组参数时
- C++拷贝构造函数、无名临时对象以及NRV优化分析
- 临时对象与拷贝构造函数
- 临时对象与拷贝构造函数
- 返回临时对象时的拷贝构造函数问题
- C++中的临时对象(拷贝构造函数)(上)
- C++中的临时对象(拷贝构造函数)(下)
- C++中的临时对象(拷贝构造函数)(上)
- C++中的临时对象(拷贝构造函数)(下)
- 【题目】C++拷贝构造函数与C++临时对象
- C++中的临时对象(拷贝构造函数)
- [C++] NRV优化
- object构造、拷贝构造、析构、临时对象
- C 类对象的拷贝构造函数
- C++:对象切片及拷贝构造函数
- C++:对象切片及拷贝构造函数
- C++构造函数、拷贝构造函数、赋值运算符漫谈(三)——NRV
- C++构造函数、拷贝构造函数、赋值运算符漫谈(三)——NRV
- 无名临时对象
- Caused by: org.postgresql.util.PSQLException: ERROR: relation "item" does not exist
- Linux下mysql安装和启动配置(更新时间:2016-10-27)
- QMovie 使用方法gif
- bzoj1770-------(算法模板系列之gauss消元异或方程组)
- RxJava+Retrofit+OkHttp深入浅出-终极封装四(多文件下载之断点续传)
- C++拷贝构造函数、无名临时对象以及NRV优化分析
- construct-binary-tree-from-inorder-and-postorder-traversal
- codevs 2597_团伙_并查集
- java -version 显示版本和JAVA_HOME配置的不一样的原因以及解决
- MyBatis的SqlMapConfig.xml配置文件详解
- CentOS6.5网络设置
- 在ARM9(S3C2440)上实现ZigBee协议--基于CC2420芯片
- 为ant指定编译java源码的jdk版本
- 桌面小部件创建?