#include "X.h" X foo() { X xx; // …… return xx; }
① 每次foo()被调用,就传回xx的值
② 如果class X定义了一个拷贝构造函数,那么当foo()被调用时,保证该拷贝构造函数也会被调用
第一个假设的真实性,必须视class X如何定义,第二个假设的真实性虽然也有部分必须视class X如何定义而定,但主要还是要看你的C++ 编译器所提供的进取性优化程度而定。你甚至可以假设在一个高品质的C++ 编译器中,上述两点对于class X的假设都是不正确的。
明确的初始化操作(Explicit Initialization)
下面有三个定义,每一个都明显地以x0 来初始化其class object:
X x0; void foo_bar() { // 定义了x1 X x1(x0); // 定义了x2 X x2 = x0; // 定义了x3 X x3 = X(X0); // …… }
必要的程序转化有两个阶段:
① 重写每一个定义,其中的初始化操作会被剥除(在严谨的C++用词中,“定义”是指“占用内存”的行为)
② class 的拷贝构造函数调用操作会被安插进去
经过上述来个阶段转化之后,foo_bar()可能看起来像这样:
// 可能的程序转换 // C++伪码 void foo_bar() { // 定义被重写,初始化操作被剥除 X x1; X x2; X x3; // 编译器安插class 拷贝构造函数的调用操作 // 拷贝构造函数:X::X(cosnt X& xx); x1.X::X(x0); x2.X::X(x0); x3.X::X(x0); // …… }
参数的初始化(Argument Initialization)
把一个class object 当做参数传给一个函数(或是作为一个函数的返回值),相当于以下形式的初始化操作:
X xx = arg;
其中xx 代表形式参数(或返回值)而arg 代表真正的参数值。因此,若已知这个函数:
void foo(X x0);
下面这样的调用方式:
X xx; // …… foo(xx);
将会要求局部实例x0 以memberwise 的方式将xx 当做初值,在编译器实现技术上,有一种策略是导入暂时性object,并调用拷贝构造函数将它初始化,然后将该暂时性object 交给函数。例如将前一段程序代码转换如下:
// C++伪码 // X xx会调用构造函数 // 编译器产生出来的暂时对象 x _temp0; // 编译器对拷贝构造函数的调用 _temp0.X::X(xx); // 重新改写函数调用操作,以便使用上述的暂时对象 foo(_temp0);
暂时性object 先以class X的拷贝构造函数正确地设定了初值,然后再以bitwise 方式拷贝到x0 这个局部实体中。foo() 的声明需要更改为以下的形式:
void foo(X& x0);
其中class X声明了一个析构函数,它会在foo() 函数完成之后被调用,对付那个暂时性的object。
返回值的初始化(Return Value Initialization)
已知下面这个函数定义:
X bar() { X xx; // 处理xx …… return xx; }
bar() 的返回值如何从局部对象xx 中拷贝过来?
① 首先加上一个额外参数,类型是class object 的一个引用。这个参数将用来放置被“拷贝建构”而得的返回值
② 在return 指令之前安插一个拷贝构造函数的调用操作,以便将欲传回的object 的内容当做上述新增参数的初值。
最后一个转化操作会重新改写函数,使它不传回任何值。根据这样的算法,bar() 转换如下:
// 函数转换 // 以反映出拷贝构造函数的应用 // C++伪码 void bar(X& _result) // 加上一个额外参数 { X xx; // 编译器所产生的缺省构造函数调用操作 xx.X::X(); // ……处理xx // 编译器所产生拷贝构造函数调用操作 _result.X::XX(xx); return; }
现在编译器必须转换第一个bar() 调用操作,以反映其新定义。例如:
X xx = bar();
将被转换为下列两个指令句:
// 注意,不必实行缺省构造函数 X xx; bar(xx);
而:
// 执行bar()所传回之X class object的memfunc() bar().memfunc();
可能被转化为:
// 编译器所产生的暂时对象 X _temp0; (bar(_temp0), _temp0).memfunc();
同样道理,如果程序声明了一个函数指针,像这样:
X (*pf)(); pf = bar;
它也必须被转化为:
void (*pf)(X &); pf = bar;
使用者层面优化(Optimization at the User Level)
“程序员优化”的观念:定义一个“计算用”的构造函数。换句话说程序员不再写:
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); }
于是当bar() 的定义被转换之后,效率会比较高:
// C++伪码 void bar(X& _result, const T& y, const T& z) { _result.X::X(y, z); return; }
_result 被直接计算出来,而不是经由拷贝构造函数拷贝而得。不过这种解决方法受到了某种批评,怕那些特殊计算用途的构造函数可能会大量扩散。
编译器层面优化(Optimization at the Compiler Level)
我们先看下面的例子:
X bar() { X xx; // ……处理xx return xx; }
编译器把其中的xx 以_result 取代:
void bar(X& _result) { // 缺省构造函数被调用 // C++伪码 _resutl.X::X(); // ……直接处理_result return; }
这样编译器优化操作,有时候被称为Named Return Value(NRV) 优化。NRV优化如今补视为是标准C++编译器的一个义不容辞的优化操作。你可以想想下面的代码:
class test { friend test foo(double); public: test() { memset(array, 0, 100*sizeof(double)); } private: double array[100]; };
同时要主考虑以下函数,它产生、修改、并传回一个test class object:
test foo(double val) { test local; local.array[0] = val; local.array[99] = val; return local; }
为个函数如果不使用NRV优化那么生成的代码大致是:
void foo(test& _result, double val) { test local; // 调用构造函数 local.test::test(); local.array[0] = val; local.array[99] = val; // 调用拷贝构造函数 _result.test::test(local); return; }
如果这个函数使用NRV优化,那么掭的代码大致是:
void foo(test& _result, double val) { // 调用构造函数 _result.test::test(); _result.array[0] = val; _result.array[99] = val; return; }
有一个main() 函数调用上述foo() 函数一千成次:
int main() { // 程序循环10000000次,每次产生一个test object; // 每个test object配置一个拥有100个double的数组; // 所有的元素都设初值为0,只有第0和第99元素以循环 // 计数器的值作为初值 for(int cnt = 0; cnt < 10000000; cnt++) { test t = foo(double(cnt)); } return 0; }
这个程序的第一个版本不能实施NRV优化,因为test class 缺少一个拷贝构造函数,第二个版本加上一个inline 拷贝构造函数如下:(这本书籍已经有一段时间了,所以有很多的地方编译器实现已经改变了,所以现在的编译器可能没有这个要求了。关于这里为什么要拷贝构造函数才能够调用NRV请查看博客)
inline test::test(const test& t) { memcpy(this, &t, sizeof(test)); }
虽然NRV优化提供了重要的效率改善,它还是饱受批评。其中一个原因是,优化由编译器默默完成,而它是否真的被完成,并不十分清楚。第二个原因是,一旦函数变得比较得复杂,优化也就变得比较难以施行。第三,某些程序员并不喜欢应用程序被优化,例如以下的代码:
void foo() { // 这里希望有一个拷贝构造函数 X xx = bar(); // …… // 这里调用析构函数 }
在此情况下,对称性被优化给打破了:程序虽然比较快,却是错误的。
请看下面的三个初始化操作在语意上是相等的:
X xx0(1024); X xx1 = x(1024); X xx2 = (X)1024;
但是在第二行和第三行中,语法明显地提供了两个步骤的初始化操作:
① 将一个暂时性的object 设以初值1024
② 将暂时性的object 以拷贝建构的方式作为explicit object 的初值。换句话说,xx0 是被单一的构造函数操作设定初值:
// C++伪码 xx0.X::X(1024);
而xx1 或xx2 却调用两个构造函数,产生一个暂时性object,并针对该暂时性object 调用class X 的析构函数:
// C++伪码 X _temp0; _temp0.X::X(1024); xx1.X::X(_temp0); _temp0.X::~X();
是否拷贝构造函数的剔除在“拷贝static object 和local object”时也应该成立?例如:
Thing outer() { // 可以不加考虑inner吗 Thing inner(outer); }
inner 应该人outer 中拷贝构造起来,或是inner 可以简单地被忽略?
一般而言,面对“以一个class object 作为另一个object 的初值”的情形,语言允许编译器有大量的自由发挥空间。其利益当然是导致机器码产生时有明显的效率提升。缺点则是你不能够案例地规划你的拷贝构造函数的副作用,必须视其执行而定。
拷贝构造函数要不要?
已知下面的3D 坐标点类:
class Point3d { public: Point3d(float x, float y, float z); // …… private: float_x, _y, _z; };
由于这个程序没有显式的定义一个拷贝构造函数,所以编译器会隐式的声明一个拷贝构造函数,并且数据成员的初始化使用“bitwise copy”(浅复制)技术。这样的效率很高,但安全吗?
答案是yes,因为三个坐标成员是以数值来储存,就是说没有指针或者类对象成员,这样“bitwise copy”既不会导致memory leak,也不会产生address aliasing,因此它既快速又安全。
那么,这个class 的设计者是否应该显式的声明一个拷贝构造函数呢?答案是NO,因为编译器自动为你实施了最好的行为。但是如果class 需要大量的memberwise 初始化操作,例如以传值的方式传回objects,那么你需要提供一个拷贝构造函数。
例如,Point3d支持下面一组函数:
Point3d operator+(const Pooint3d&, const Point3d&); Point3d operator-(const Pooint3d&, const Point3d&); Point3d operator*(const Pooint3d&, int); // ……
所有那些函数都能够良好地符合NRV template:
{ Point3d result; // 计算result return result; }
实现拷贝构造函数的最简单方法像这样:
Point3d::Point3d(const Point3d& rhs) { _x = rhs._x; _y = rhs._y; _z = rhs._z; }
这没问题,但使用C++ library 的memcpy() 会有更高的效率:
Point3d::Point3d(const Point3d& rhs) { memcpy(this, &rhs, sizeof(Point3d)); }
然而不管使用memcpy() 或memset(),都只有在“class 不含任何由编译器产生的内部members”时才能有效运行。如果Point3d class 声明一个或一个以上的virtual functions,或内含一个vritual base class,那么使用上述函数将会导致那些“被编译器产生的内部mmbers”的初值被改写。例如,已知下面声明:
class Shape { public: // 这会改变内部的vptr Shape() { memset(this, 0 , sizeof(Shape)); } virtual ~Shape(); // …… };
编译器为此构造函数扩张的内容看起来像是:
// 扩张后的构造函数 // C++伪码 Shape::Shape() { // vptr 必须在使用者的代码执行之前先设定妥当 _vpte_Shape = _vtbl_Shape; // memset会将vptr清为0 memset(this, 0, sizeof(Shape)); }
如你所见,若要正确使用memset() 和memcpy(),需得掌握某些C++ object Model的语意学知识。