显示的初始化操作(Explicit Initialization)
已知有这样的定义:
X x0;
下面的三个定义,每一个都明显地以x0来初始化其class object:
void foo_bar()
{
X x1(x0); //定义了x1
X x2 = x0; //定义了x2
X x3 = X(x0); //定义了x3
}
必要的程序转化有两个阶段:
- 重写每一个定义,其中的初始化会被剥除。
- class的copy constructor调用操作会被安插进去。
在明确的双阶段转化之后,foo_bar可能看起来像这样:
//可能的程序转换
//C++ pseudo
void foo_bar()
{
X x1; //定义被重写,初始化操作被剥除
X x2; //定义被重写,初始化操作被剥除
X x3; //定义被重写,初始化操作被剥除
x1.X::X(x0);
x2.X::X(x0);
x3.X::X(x0);
//...
}
其中的:
x1.X::X(x0);
就表现出对以下的copy constructor的调用:
X::X(const X& xx);
参数的初始化(Argument Initialization)
C++ Standard说,把一个class object当做参数传递给一个函数(或者作为一个函数返回值),相当于以下形式的初始化操作:
X xx = arg;
其中xx代表形式参数(或返回值)而arg代表真正的参数值。因此,若已知这个函数:
void foo(X x0);
下面的调用方式:
X xx;
//...
foo(xx);
将会要求局部实例(local instance)x0以memberwise的方式将xx当做初值。在编译器实现技术上,有一种策略是导入所谓的临时性object,并调用copy constructor将它初始化,然后将此临时性object交给函数。例如将前一段程序代码转换如下:
//C++ pseudo
//编译器产生出来的临时对象
X _temp0;
//编译器调用copy constructor
_temp0.X::X(xx);
//重新改写函数的调用,以便使用上述的临时对象
foo(_temp0);
然而这样的转换只完成了一半功夫而已。临时对象先以class X的copy constructor正确的设定了初值,然后再以bitwise方式拷贝到x0这个局部实例中。因此foo()声明因而也必须被转换,形式参数必须从原来的一个class X改变成一个class X的reference:
void foo(X& x0);
其中class X声明的destructor,它会在foo()函数完成之后被调用,对付那个临时的object。
另一种实现方法是以“拷贝建构”(copy construct)的方式把实际参数直接建构在其应该的位置上,此位置视函数活动范围的不同,记录于程序堆栈中。在函数返回之前,局部对象(local object)的destructor(如果有定义)会被执行。Borland C++编译器就是这样实现,但它提供一个编译选项,用以指定前一种做法,以便和早期版本兼容。
返回值的初始化
已知下面的定义:
X bar()
{
X xx;
//处理xx
return xx;
}
如上bar()的返回值如何从局部对象xx拷贝过来?Stroustrup的解决做法是一个双阶段转化:
-
首先加上一个额外参数,类型是class object 的一个reference。这个参数用来放置被“拷贝建构(copy constructed)”而得到的返回值。
-
在return指令返回之前安插一个copy constructor调用操作,以便将欲传回之object的内容当做上述新增参数的初值。
真正的返回值是什么?最后一个转换操作会重新改写函数,使它不传回任何值。根据这样的做法,bar()转换如下:
//函数转换
//以反映出copy constructor的应用
//C++ pseudo
void
bar(X& __result) //加上一个额外参数
{
X xx;
//编译器产生的default constructor调用操作
xx.XX::XX();
//......处理xx
//编译器产生copy constructor调用操作
__result.XX::XX(xx);
return;
}
现在编译器必须转换每一个bar()调用操作,以反映其新定义。例如:
X xx = bar();
将被转换下列两个指令句:
X xx;
bar(xx);
而:
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之中。下面直接定义constructor,可以直接计算xx的值:
X bar(const T& y,const T& z)
{
return X(y,z);
}
于是当bar()的定义被转换后,效率会更高:
void bar(X& __result)
//上行是否应该是bar(X& __result,const T& y,const T& z)
{
__result.X::X(y,z);
return;
}
__result被直接计算出来,而不是经由copy constructor拷贝而得。
在编译器层面做优化(Optimization at the Compiler Level)
像bar()这样的函数,所有的return指令传回相同的具名数值(name value),因此编译器有可能自己做优化,方法是以result参数取代name return value。例如下面定义的bar():
X bar()
{
X xx;
//...处理xx
return xx;
}
编译器把其中的xx以__result取代:
void bar(X& __result)
{
//default constructor被调用
//C++ pseudo
__result.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 foo(double val)
{
test local;
local.array[0] = val;
local.array[100] = val;
return local;
}
main()
{
for(int cnt = 0; cnt < 10000000; cnt++)
{
test t = foo(double(cnt));
}
return 0;
}
上述程序不能实施NRV优化,因为test class缺少一个copy constructor。第二个版本加上一个inline copy constructor如下:
inline test::test(const test& t)
{
memcpy(this,&t,sizeof(test));
}
//别忘了在class test的声明中加一个member function如下:
//public:
//inline test(const test& t);
这个copy constructor激活了C++编译器的NRV优化。NRV优化的执行并不通过独立的优化工具完成。虽然NRV优化提供了重要的效率改善,但它却饱受批评。原因两点:
- 优化由编译器默默完成,而它是否真得被完成,并不十分清楚(因为很少有编译器会说明其实现程度,或是否实现)。
- 一旦函数变得比较复杂,优化也就变得比较难以实施。
某些程序员不喜欢应用程序被初始化,想象你已经摆好了你的copy constructor的阵势,使你的程序“以copying方式产出一个object时”,对称的调用destructor,例如:
void foo()
{
//这里希望有一个copy constructor
X xx = bar();
//...
//这里调用destructor
}
在此情况下,对称性被优化打破了:程序虽然比较快,却是错误的。例如你想在constructor或者destructor计数,那么会导致错误。
Copy constructor:要还是不要?
已知下面的3D坐标:
class Point3d
{
public:
Point3d(float x,float y,float z);
//...
private:
float _x,_y,_z;
};
这个class的设计者应该提供一个explicit copy constructor吗?
上述class的default copy constructor被视为trivial,它即没有任何member(或base)class objects带有copy constructor,也没有任何的virtual base class 或virtual function。所以,默认情况下,一个Point3d class object的“memberwise”初始化导致“bitwise copy”。这样做的效率高,也安全。bitwise既不会导致memory leak,也不会产生address aliasing。这个设计者一般不用提供explicit copy constructor,但是在预见class 需要大量的memberwise初始化操作时,例如以传值(by value)的方式传回object,那么就需要提供一个copy constructor的explicit inline函数实体就非常合理——在“你的编译器提供NRV优化”的前提下。
例如,Point3d支持下面的一组函数:
Point3d operator+(const Point3d&,const Point3d&);
Point3d operator-(const Point3d&,const Point3d&);
Point3d operator*(const Point3d&,int);
...
所有的那些函数都能良好的符合NRV template:
{
Point3d result;
//计算result
return result;
}
实现copy constructor最简单的方法:
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,都只有在”classes不含任何由编译器产生的内部members“时才能有效运行。如果Point3d class声明一个或一个以上的virtual functions,或内含一个virtual base class,那么使用上述函数将会导致那么”被编译器运行的内部members“的初值被改写。如下:
class Shape
{
public:
Shape(){memset(this,0,sizeof(Shape))}
virtual ~Shape();
//...
};
编译器为此constructor扩张内容看起来像:
//扩张后的constructor
//C++ pseudo
Shape::Shape()
{
//vptr必须在使用者的代码之前先设置妥当
_vptr__Shape = __vtbl__Shape;
//memset会将vptr清0
memset(this,0,sizeof(Shape));
}
若要正确使用memset和memcpy,需要掌握某些C++ Object Model语意学知识。
网上看了下别人的见解:Lippman在《深度探索C++》书中指出NRV的开启与关闭取决于是否有显式定义一个拷贝构造函数,我实在想不出有什么理由必须要有显示拷贝构造函数才能开启NRV优化,于是在vs2010中进行了测试,测试结果表明,在release版本中,不论是否定义了一个显式拷贝构造函数,NRV都会开启。由此可见vs2010并不以是否有一个显式拷贝构造函数来决定NRV优化的开启与否。但同时,立足于这一点,可以得出Lippman所说的以是否有一个显式定义的拷贝构造函数来决定是否开启NRV优化,应该指的是他自己领导实现的cfront编译器,而非泛指所有编译器。那么cfront又为什么要以是否定义有显示的拷贝构造函数来决定是否开启NRV优化呢?我猜测,他大概这样以为,当显式定义有拷贝构造函数的时候一般代表着要进行深拷贝,也就是说此时的拷贝构造函数将费时较长,在这样的情况下NRV优化才会有明显的效果。反之,不开启NRV优化也不是什么大的效率损失。
另外,有一点要注意的是,NRV优化,有可能带来程序员并不想要的结果,最明显的一个就是——当你的类依赖于构造函数或拷贝构造函数,甚至析构函数的调用次数的时候,想想那会发生什么。由此可见、Lippman的cfront对NRV优化抱有更谨慎的态度,而MS显然是更大胆。