2.3 程序转化语义学(Program Transformation)

本文探讨了C++中显示初始化、参数初始化及返回值初始化的概念,深入解析了编译器层面的优化策略,如NRV优化,及其对效率的影响。同时,讨论了在不同场景下,是否提供显式拷贝构造函数的考量。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

显示的初始化操作(Explicit Initialization)

已知有这样的定义:

X x0;

下面的三个定义,每一个都明显地以x0来初始化其class object:

void foo_bar()
{
	X x1(x0);		//定义了x1
	X x2 = x0;		//定义了x2
	X x3 = X(x0);	//定义了x3
}

必要的程序转化有两个阶段:

  1. 重写每一个定义,其中的初始化会被剥除。
  2. 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的解决做法是一个双阶段转化:

  1. 首先加上一个额外参数,类型是class object 的一个reference。这个参数用来放置被“拷贝建构(copy constructed)”而得到的返回值。

  2. 在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优化提供了重要的效率改善,但它却饱受批评。原因两点:

  1. 优化由编译器默默完成,而它是否真得被完成,并不十分清楚(因为很少有编译器会说明其实现程度,或是否实现)。
  2. 一旦函数变得比较复杂,优化也就变得比较难以实施。

某些程序员不喜欢应用程序被初始化,想象你已经摆好了你的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显然是更大胆。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值