构造函数语义学(二)
程序转化语意学 Program Transformation
看看书上的例子
#include "X.h"
X foo() {
X xx;
// ....
return xx;
}
一个人可能会做出一下假设
- 每次 foo() 调用,就传回 xx 的值
- 如果 class X 定义了一个 copy constructor, 那么当 foo() 被调用时,保证该 copy constructor 也会被调用。
第一个假设的真实性,必须视 class X 如何定义而定。第二个假设的真实性,虽然也部分地必须视 class X 如何定义而定,但最主要的还是视你的C++编译器所提供的进取优化层级(degree of aggressive optimization)而定。一个人甚至可以假设在一个高质量的C++编译器中,上述两点对于 class X 的 nontrivial definisions 都不正确。
-
显式的初始化操作 Explicit Lnitiation
看下面的定义
X x0; void foo_bar() { X x1(x0); // 定义了 x1 X x2 = x0; // 定义了 x2 X x3 = X(x0); // 定义了 x3 }
必要的程序转化有两个阶段:
- **重写每一个定义,其中的初始化操作会被剥除。**在C++用词中,定义 是指 “占用内存” 的行为。
- class 的 copy constructor 调用操作会被安插进去。
所以,经过编译器重定义后的代码是这样的
void foo_bar() { // 定义被重写,初始化操作被剥离 X x1; X x2; X x3; // 编译器安插 X copy constructor 的调用操作 // X::X( const X& x); x1.X::X(x0); x2.X::X(x0); x3.X::X(x0); }
-
参数的初始化 Argument Initialization
C++ Standard(Section 8.5)说,把一个 class object 当作参数传给一个 函数或是作为一个函数的返回值,相当于这样子的操作
X xx = arg; // 其中xx代表形式参数 // arg 是真正的参数
这句话的意思就是,在我们将一个参数传递给一个函数或者将一个值从函数中返回时,编译器并不会直接将这个参数传递给函数或直接返回,它会先构建一个临时 class object,将参数或将要返回的值拷贝早这个临时的 class object 中,然后再对这个 class object 进行操作
X xx; void foo(X xx) { ... } void foo(xx); // 编译器会将上面的 foo 调用进行改写 X __temp0; __temp0.X::X(xx); // copy constructor // 重新改写函数调用操作,以便使用上面的临时对象 foo(__temp0);
但是上面的调用任然存在问题,传递参数的时候会发生 bitwise 拷贝(我们要尽可能地避免 bitwise copy)
void foo(const X&);
另一种实现方法是 **以“拷贝构建”(copy constructor)的方式把实际参数直接建构再其应该的位置上,**此位置视函数活动范围的不同,记录于程序堆栈之中。在函数返回之前,局部对象(local object)的 destructor 会被执行。
-
返回值的初始化 Return Value Initialization
在 Stroustrup 在 cfront 中 通过一个双阶段转化,将一个函数内的临时对象进行返回。
- 首先加上一个额外的参数,类型是 class object 的一个 reference。这个参数将用来放置被 “拷贝构建(copy constructed)” 而得的返回值。
- 在 return 指令之前安插一个 copy constructor 调用操作,以便将将要传回的 object 的内容当作上述新增参数的初值。
// 原版 X bar() { X xx; // handle xx return xx; } // 经过编译器重写的 bar() viod bar ( X& __result ) // 加上了一个额外的参数 { X xx; // 编译器产生的 default constructor 调用操作 xx.X::X(); // ...处理 xx // 编译器产生的 copy constructor 调用操作 __result.X::X(xx); return; } // 在函数外的调用也需要发生改变 X xx = bar(); // 将定义与初始化的操作分离了 X xx; // 注意:这里是编译器改的,并不会调用 default constructor bar(xx); // 看看下面的这个转变 bar().memfunc(); // 编译器产生一个临时对象进行适配 X __temp0; (bar(__temp0), __temp0).memfunc();
-
在使用者层面做优化(Optimization at the User Level)
提示:这一点十分抽象,请大家理性吸收
这里作者在书上提出了一个观念:**定义一个 计算用 的 constructor。**看看例子。
// 一般实现 X bar(cosnt T& y, const T& z) { X xx; // 通过y和z来计算 xx return xx; } // 作者在书中的写法 X bar(const T& y, const T& z) { return X(y, z); // 逆天,直接在 constructor 中进行计算 }
不可否认的是上面的做法确实效率要高一点,但是我并不推荐,作者在书中也说了,这种理念受到批评。
-
在编译器层面上做优化(Optimization at the Compiler Level)
还是这段代码
X bar() { X xx; // 。。。处理xx return xx; } // 编译器做的优化 void bar(X& __result) { // default constructor 调用 // C++伪码 __result.X::X(); // ......直接处理__result return; }
这样的编译器优化操作,有时候被称为 Named Retrun Value(NRV)优化。NRV 优化如今被视为标准c++编译器的一个不可缺少的优化操作。
class test { friend test foo(double); public: test() { memset(array, 0, 100*sizeof(double)); } private: double array[100]; }; test foo(double val) { test local; local.array[0] = val; local.array[99] = val; return local; } int main() { for (int cnt = 0; cnt < 100000000; ++cnt) test t = foo(double(cnt)); return 0; }
上面这段代码并不能激活编译器的 NRV 优化,我们需要给它加上一个 copy constructor。
inline test::test(const test& t) { memcpy(this, &t, sizeof(test)); }
这个是书上测试的结果,-O是gcc自带的一种优化策略。
NRV 优化的两个缺点:
- 这种优化由编译器默默完成,而它是否真的被完成,并不清楚
- 一旦函数变得复杂,优化也会变得比较难以执行。在 cfront 中,只有当所有 named return 指令句发生于函数的 top level ,优化才会施行。如果导入 “a nested local block with a return statement”, cfront 就会将优化关闭。
看到一篇关于NRV的文章,可以看看
总结上面的文章的观点就是:NRV优化会将开销较大的拷贝构造替换成别的构造函数,如果一个 class 中没有定义 copy construcotor 那么编译器就认为 通过 bitwise copy 可以达到程序员的要求(因为 bitwise copy 原本就是比较高效的),所以如果一个函数中没有显式定义出 copy constructor 那么编译器将不会进行 NRV优化。
总结:一般而言,面对 “以一个 class object 作为另一个 class object 的初值” 的情形,语言允许编译器有大量的自由发挥的空间。其好处是能够在机器产生机器码的时候有明显的效率的提升。缺点是程序员并不能够安全地规划 copy construcotr 的副作用,必须视其执行而定。
我们什么时候需要定义 copy costructor
先看第一个例子
class Point3d { public: Point3d( float x, float y, float z ); private: float _x, _y, _z; };
这种时候,就没有必要设置一个 copy cosntructor,这种情况下使用 bitwise copy 既高效又安全。
下面看看两种不同的 copy cosntructor
// 1 Point3d::Point3d( const Point3d &rhs) { _x = rhs._x; _y = rhs._y; _z = rhs._z; } // 2 Point3d::Point3d(const Point3d &rhs) { memcpy(this, &rhs, sizeof(rhs)); }
需要注意的是第二种情况,在 class 中有定义 virtual function 或 class 有 virtual base class 的时候不能这么使用,因为那样会导致编译器在使用者代码执行前设置的值被更改(比如:vptr)
class Shape { public: Shape() { memset(this, 0. sizeof(Shape)); } virtual ~Shape(); } // 编译的时候,编译器会为构造函数进行扩张 Shape::Shape() { __vptr__Shpae = _vtbl__shape; memset( this, 0, sizeof(Shape) ); }