构造函数语义学——Copy Constructor 篇
本文主要介绍《深度探索 C++对象模型》之《构造函数语义学》中的 Copy Constructor
构造函数的调用时机
首先需要明确,构造函数何时会被调用呢?cppreference 中已经有了足够详细地说明:
凡在对象从同类型的另一对象(以直接初始化或复制初始化)初始化时,调用复制构造函数(除非重载决议选择了更好的匹配或其调用被消除),情况包括:
初始化:T a = b; 或 T a(b);,其中 b 类型为 T;
函数实参传递:f(a);,其中 a 类型为 T 而 f 为 Ret f(T t);
函数返回:在如 T f() 这样的函数内部的 return a;,其中 a 类型为 T,它没有移动构造函数。
编译器合成 copy constructor 的条件
在之前《构造函数语义学——Default Constructor 篇》一文中,我们分析了编译器产生 default constructor 的条件,以及编译器所产生的 default constructor 的类型(trivial & non-trivial);对于构造函数来说,其原理也是大致类似的,只是具体的细节条件不同,此文中就不再给出具体的证明,读过前一篇博文的读者也应该能够自己分析,此文只给出具体的条件
编译器隐式声明&定义 copy constructor 的条件
隐式声明的复制构造函数
若不对类类型(struct、class 或 union)提供任何用户定义的复制构造函数,则编译器始终会声明一个复制构造函数,作为其类的非 explicit 的 inline public 成员。
与 default constructor 类似,只要没有任何 user_declared 的 copy constructor,那么编译器就会为我们自动声明一个 copy constructor(这一点与《深度探索 C++对象模型》中所述不同)
隐式定义的复制构造函数
若隐式声明的复制构造函数未被弃置,则当其被 ODR 式使用时,它为编译器所定义(即生成并编译函数体)。对于 union 类型,隐式定义的复制构造函数(如同以 std::memmove)复制其对象表示。对于非联合类类型(class 与 struct),该构造函数用直接初始化,按照初始化顺序,对对象的各基类和非静态成员进行完整的逐成员复制。
trivial copy constructor 的条件
编译器自动合成的 copy constructor 也是分为 trivial 和 non-trivial 的
对于 trivial copy constructor 的条件,cppreference 中也给出了详细的说明:
当下列各项全部为真时,类 T 的复制构造函数为平凡的:
它不是用户提供的(即它是隐式定义或预置的),且若它被预置,则其签名与隐式定义的相同;
T 没有虚成员函数;
T 没有虚基类;
为 T 的每个直接基类选择的复制构造函数都是平凡的;
为 T 的每个类类型(或类类型数组)的非静态成员选择的复制构造函数都是平凡的;
而在《深度探索 C++对象模型》中有一句话“决定一个copy constructor是否为trivial的标准在于class是否展现出所谓的bitwise copy semantics”
;即如果一个 class 展现出了 bitwise copy semantics,那么编译器为其合成的 copy constructor 就是 trivial 的
换言之,如果不满足 bitwise copy semantics,那么编译器合成的 copy constructor 就是 non-trivial 的。何时一个 class 不表现出 bitwise copy semantics 呢?书中给了四个条件(略有修改):
- 当 class 内含一个 member object,而后者的 class 中的有一个 copy constructor(而后者 class 的 copy constructor 必须是 non-trivial 的)
- 当 class 继承自一个 base class,而这个 base class 存在一个 copy constructor(该 base class 的 copy constructor 必须是 non-trivial 的)
- class 声明了 virtual function
- class 派生自一个继承链,而该继承链中存在一个或多个 virtual base class
其实这个四个条件相当于 cppreference 中提到的成为 trivial copy constructor 的相反条件
编译器合成的 copy constructor 的行为
trivial copy constructor 的行为
关于 trivial copy constructor 的行为,cppreference 也有提到:
非联合类的平凡复制构造函数,效果为复制实参的每个标量子对象(递归地包含子对象的子对象,以此类推),且不进行其他动作。不过不需要复制填充字节,甚至只要其值相同,每个复制的子对象的对象表示也不必相同。
这句话的意思是说,如果编译器合成的出来 copy constructor 是 trivial 的,它展现出这种行为:逐个字节的拷贝所有内容
举个例子:
class A { private: int _a; }; int main() { A a; A aa = a; return 0; }
其中 A aa = a;这一句,会调用编译器产生的 trivial copy constructor,该 trivial copy constructor 会一个字节一个字节的把 a 中的成员变量的值拷贝到 aa 对应的成员变量中去
这似乎看起来挺好的呀,也正是我们所需要的结果,但是,如果 class A 中的成员变量是一根指针,那么问题就大了:
#include <iostream> using namespace std; class A { public: int *p; }; int main() { A a; int val = 1; a.p = &val; A aa = a; cout << a.p << endl; cout << aa.p << endl; *(aa.p) = 2; cout << *(a.p) << endl; cout << *(aa.p) << endl; } // 上述程序的输出为 0x7ffc5d760414 0x7ffc5d760414 2 2
也就是说,在编译器自动为我们合成的 trivial copy constructor 的行为中,复制了 a 的指针给了 aa(浅拷贝),也就是说 a 和 aa 中的指针 p 指向了相同的地址!!!
在这种含有指针的情况下,编译器产生的 trivial copy constructor 的行为便不是我们所希望的,我们必须手动显示的定义一个符合我们需求的 copy constructor 来完成对指针的拷贝
non-trivial copy constructor 的行为
cppreference 中已经说了:
对于非联合类类型(class 与 struct),该构造函数用直接初始化,按照初始化顺序,对对象的各基类和非静态成员进行完整的逐成员复制。
non-trivial copy constructor 一个很重要的行为是:确保 vptr 的准确设定。(因为只要包含虚机制,那么编译器自动合成的 copy constructor 就不可能是 trivial 的)
上面一点,书中已经说的足够清楚,此文不再赘述
总结
- copy constructor 在特定条件下,编译器也会为我们自动合成
- 编译器合成的 copy constructor 也是分为 trivial 和 non-trivial 的
- 要时刻牢记 trivial copy constructor 的条件与行为
- 当成员变量涉及指针时,最好的做法就是显式提供自定义的 copy constructor 来满足需求