对于:
X foo(){
X xx;
return xx;
}
可能有这样的假设:
- 每次foo()被调用,就传回xx的值
- 如果类X定义了一个拷贝构造函数,那么当foo()被调用时,该拷贝构造函数也会被调用
那这两个假设对不对呢,下面我们来研究一下
显示初始化操作(explicit initialization)
已知有这样的定义:
X x0;
// 显示的以x0来初始化其类对象
void foo_bar(){
X x1(x0);
X x2 = x1;
X x3 = X(x0);
}
对于x1,x2,x3的定义,必要的程序转换有两个阶段:
- 重写每一个定义,其中的初始化操作会被剥除
- 类的拷贝构造函数调用操作会被安插进去
在明确的双阶段转换之后,foor_bar()可能是这样:
void foo_var(){
X x1; // 重新定义,剥除初始化操作
X x2;
X x3;
// 编译器安插X拷贝构造函数的调用操作
x1.X::X(x0); // 相当于调用X::X(const X& xx)
x2.X::X(x0);
x3.X::X(x0);
}
参数初始化(Argument Initialization)
C++标准提出:把一个类对象当作传递给一个函数或者作为一个函数的返回值,相当于以下形式的初始化操作:
X xx = arg;
因此,对于函数:
void foo(X x0);
的调用:
X xx;
foo(xx);
将会要求局部实体x0以memberwise
的方式将xx当作初值。
编译器的第1种实现方式是:
- 导入一个临时对象,并调用拷贝构造函数将它初始化,然后将该临时对象交给函数:
// 编译器产生的临时对象
X __temp0
// 编译器对拷贝构造函数的调用
__temp0.X::X(xx);
// 改写函数调用操作,以便使用上面的临时对象
foo(__temp0);
- 然而这样的转换只做了一半功夫而已。残留问题是foo的声明:临时对象先以类X的拷贝构造函数正确的设定了初值,然后再以bitwise方式拷贝到x0这个局部实体中。但是此时foo()的声明也因此必须被转化,形式参数从原来的类X对象转化为一个类X引用:
void foo(X & x0);
- 其中类X声明了一个析构函数,它会在foo()函数完成之后被调用,以析构临时对象
编译器的第2种实现方式是:
- 以拷贝构造的方式将实参直接构建在其应该的位置上,该位置视函数活动范围的不同记录于程序堆栈总。
- 在函数返回之前,局部对象(local object)的析构函数(如果有定义的话)会被调用
返回值初始化(Return Value Initialization)
对于:
X bar(){
X xx;
// ...
return xx;
}
问:bar()的返回值是怎么从局部对象xx中拷贝过来的?
答:双阶段转化
- 先加上一个额外参数,类型是类对象的一个引用,这个参数将用来放置被拷贝构造出来的返回值
- 在return指令之前安插一个拷贝构造函数的调用操作,以便将想传回的对象的内容当作上面新增参数的初值。
问:真正的返回值是什么?
答:最后一个转化操作会改写函数,使它不传回任何值。根据这样的算法,bar()转换如下:
void bar(X & __result){ // 加上一个额外参数
X xx;
//编译器产生的默认构造函数调用操作
xx.X::X();
// ... 处理xx
//编译器所产生的拷贝构造函数调用操作
__result.X::XX(xx);
return;
}
也就是说,对于:
X xx = bar();
将被转换成:
X xx;
bar(xx);
对于:
bar().memfunc();
将被转换成:
// 编译器产生的临时对象
X __temp0;
(bar(__temp0), __temp0).memfunc();
对于函数指针:
X (*pf)();
pf = bar;
将被转换成:
void (*pf)(X&)
pf = bar;
在用户层次做优化
对于一个类似Bar()这样的函数,定义一个“计算用”的构造函数。也就是说,程序员不再编写:
X bar(const T &y, const T &z){
X xx;
// ... 以y和z来处理xx
return xx;
}
这会要求xx被“memberwise”地被拷贝到编译器所产生的__result中:
程序员应该定义一个构造函数,直接计算xx的值:
X bar(const T &y, const T &z){
return X(y, z);
}
这样效率比较高:
void bar(X & __result, const T &y, const T &z){
__result.X::X(y, z);
return;
}
__result被直接计算出来,而不是由拷贝构造函数拷贝得到。不过这种方法用的不多。
在编译器层面做优化
X bar(){
X xx;
// ...
return xx;
}
在一个类似bar()这样的函数中,所有return指令返回相同的具名值(named value)xx,因此编译器可能自己优化,方法是采用_result参数取代name return value
void bar(X __result){
__result.X::X();
return;
}
这种优化技术也被称为NRV(named return value)优化
拷贝构造函数:要还是不要
对于:
class Point3d{
public:
Point3d(float x, float y, float z);
private:
float _x, _y, _z
}
这个类的设计者应该提供一些显式拷贝构造函数吗?
上面类的默认拷贝构造函数是trivial。他既没有任何的member(/base) class objects带有拷贝构造函数,也没有任何的虚基类或者虚函数。所以,默认情况下,一个Point3d类对象的memberwise
初始化操作会导致bitwise copy
。这样效率很高,但安全吗?
- 答案是yes。三个坐标成员是以数值来存储。
bitwise copy
既不会导致内存泄漏(memory leak),也不会产生address aliasing。因此它既快速又安全
address aliasing:同一地址可以直接或者间接的通过两个或者多个不同名字进行访问,这就是地址别名机制。
那么类的设计者应该提供一个显式拷贝构造函数吗?
- 答案是no,没有任何理由要你提供一个拷贝构造函数实体,因为编译器自动为你实施了最好的行为
那么如果你预见了这种类将需要大量的memberwise初始化操作(比如以传值方式返回对象)呢?
- 答案是yes,前提是使用的编译器提供NRV优化
加入,Point3d支持下面一组函数:
Point3d operator+(const Point3d&, const Point3d&);
Point3d operator-(const Point3d&, const Point3d&);
Point3d operator*(const Point3d&, int);
所有的这些函数都能良好的符号NRV模板:
{
Point result;
// ... 计算result
return result;
}
实现拷贝构造函数的最简单方法是这样:
Point3d::Point3d(const Point3d &rhs){
_x = rhs._x;
_y = rhs._y;
_z = rhs._z;
}
上面是可以的,但是使用C++库的memcpy会更有效率:
Point3d::Point3d(const Point3d & rhs){
memcpy(this, &rhs, sizeof(Point3d));
}
但是不管是memcpy还是memset,都只有在类不含任何由编译器产生的内部成员是才能有效运行。如果Point3d类声明一个或者一个以上的虚函数,或者内含有一个虚基类,那么使用上诉函数将会导致那些被编译器产生的内部成员的初值被改写。看个例子:
class Shape{
public:
// 这会改变内部的vptr
Shape(){ memset(this, 0, sizeof(Shape));}
virtual !Shape();
};
编译器为此构造函数扩张的内容是:
// 扩张后的构造函数
Shape::Shape(){
// vptr必须在用户代码执行之前先设定妥当
__vptr__shape = __vptr__shape ;
// memset会将vptr清0
memset(this, 0, sizeof(Shape));
}
总结
- 构造函数的使用,将迫使编译器多多少少的对用户代码做部分转化。尤其是当一个函数以传值方式返回一个类对象,而该类有一个拷贝构造函数(不管是明确定义的还是合成的),这将导致程序转换----不论在函数的定义还是使用上
- 此外编译器也将拷贝构造函数的调用操作优化,以一个额外的第一参数(数值被直接存在其中)取代NRV。