这篇文章干啥呢? 不太好说… 大体来说,还是算程序转化语意吧
已知下面的程序片段:
#include "X.h"
X foo()
{
X xx;
// ...
return xx;
}
来吧,你看到这段程序会有什么假设?
我猜,可能会存在以下假设:
- 每次foo()被调用,就传回xx的值。
- 如果class X定义了一个 copy constructor,那么当
foo()
被调用时,保证该 copy constructor 也会被调用。
第一个假设的真实性,必须 class X 如何定义而定。
第二个假设的真实性,也部分的依赖于 class X 如何定义而定,但最主要的还是视你的C++编译器的优化层级而定。
如果在一个高质量的C++编译器中,上述的两点对于 class X的 nontrivial definetions 都不正确。
接下来我们详细的讨论一下具体原因。
显式的初始化操作
已知有这样的定义:
X x0;
下面的三个定义,每一个都明显地以x0来初始化其class object:
void foo_bar()
{
X x1(x0);
X x2=x0;
X x3=X(x0);
}
必要的程序转化有两个阶段:
- 重写每一个定义,其中的初始化操作会被剥除(这里所谓的定义指的是上述三行,而其实严谨的C++用词,”定义”是指”分配内存”的行为)
- class 的 copy constructor 调用操作会被安插进去。
所以上述代码,可能会被转化为如下代码:
void foo_bar()
{
X x1; //定义被重写,初始化操作被剥除(即此处不会有constructor调用)
X x2; //定义被重写,初始化操作被剥除(即此处不会有constructor调用)
X x3; //定义被重写,初始化操作被剥除(即此处不会有constructor调用)
//编译器安插 x copy constructor 的调用操作
x1.X::X(x0);
x2.X::X(x0);
x3.X::X(x0);
}
其中 x1.X::X(x0);
就表现出对 copy constructor 的调用: X::X(const X& xx);
注:此处拆分主要是为了解释对 copy constructor 的调用。
参数的初始化
当一个 class object 当做参数传给一个函数(或作为一个函数的返回值),相当于以下形式的初始化操作。
x xx=arg;
其中 xx 代表形式参数(或者返回值)而arg代表实际参数。
所以下面的这个函数:
void foo(X x0);
下面的调用方式:
X xx;
//...
foo(xx);
将会要求局部变量,x0 以 memberwise 的方式将 xx作为初值。在编译器实现技术上,有一种策略是导入所谓的临时性 object, 并调用 copy constructor 将它初始化,然后将此临时性object交给函数。
所以可能存在如下的转换:
//编译器产生出来的临时对象
X _temp0;
//编译器对 copy constructor 的调用
——temp0.X::X(xx);
//重新改写函数调用操作,以便使用上述的临时对象
foo(_temp0);
然而这样的转换只完成了一半而已,你会发现这跟之前没啥区别呀… 所以需要对foo()的声明做转换,形式参数必须由原来的 class X object 变成 class X reference:void foo(X &x0)
其中class X 声明了一个 destructor,它会在 foo()函数完成之后被调用,应用在临时性的object上。
还有一种实现方法是”copy constructor”的方式把实际参数直接构建在其应该出现的位置上,此位置视函数活动范围的不同,记录于程序堆栈中。在函数返回之前,局部变量的destructor会被执行。Borland C++编译器就是采用此方法,它也提供一个编译选项,用以指定前一种做法,以便和其早期版本兼容。
返回值的初始化
已知下面这个函数的定义:
X bar()
{
X xx;
//...
return xx;
}
这里编译器会如何把局部变量xx拷贝到返回值中呢?
cfront 编译器的做法是:
- 首先加上一个额外参数,类型是 class object 的一个 reference。这个参数将用来放置“copy constructor”而得的返回值。
- 在return指令之前安插一个 copy constructor 调用操作,以欲传回的object的内容作为上述新增参数的值。
改写之后的函数没有返回值。所以改写之后的代码可能如下:
//函数转换
//以反映出 copy constructor 的应用
void bar(X &_result)
{
X xx;
//编译器所产生的 default constructor 调用操作
xx.X::X();
//...
//编译器所产生的 copy constructor 调用操作
_result.X::xx(xx);
return ;
}
现在编译器必须转换每一个 bar()调用操作,以反映其新定义。
X xx=bar();
//将被转换为下列的语句
X xx;
bar(xx);
而bar().memfunc();
这样子的语句可能会被转换为:
X _temp0;
(bar(_temp0),temp0).memfunc(); //注意逗号运算符的作用(整个逗号表达式的值是以逗号分隔的列表中的最后一个表达式的值。)
同理,如果程序声明了一个函数指针,Like this:
X(*pf)();
pf=bar;
//被转换为:
void (*pf)(X&);
pf=bar;
使用者层面做优化
前面介绍了一些编译器的实现和优化手段,那么作为程序员我们能做的优化有哪些呢?
看这样两段代码:
X bar(const T &y,const T &z)
{
X xx;
//...以y和z来处理 xx
return xx;
}
X bar(const T &y,const T &z)
{
//...
return X(y,z);
}
使用前面的转换手段之后,可以看到后面的代码效率比较高,转换之后的代码如下:
void bar(X &result)
{
__result.X::X(y,z);
return;
}
这里的__result被直接计算出来,而不是经由 copy constructor 拷贝而得!这里只关心效率,而不是设计,所以应该根据你的使用场景来考虑到底怎么做。
Named Return Value Optimize
像前面这种函数
X bar(const T &y,const T &z)
{
X xx;
//...以y和z来处理 xx
return xx;
}
一些编译器有独特的优化手段,就是把它变成如下形式。
void bar(X &__result)
{
//default constructor被调用
__result.X::X();
//....直接处理__result
return ;
}
这种就是 Named Return Value Optimize(即所有的return都返回具有名称的对象)。在现代智能C++编译器中 NRVO是一个义不容辞的优化操作。同时随着现代编译器的不断迭代更新,我们不应该对这种程序转换行为作出假设。
相关参考资料: