首页 > 代码库 > 程序转化语意学

程序转化语意学

 

 

#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的语意学知识。

程序转化语意学