码迷,mamicode.com
首页 > 其他好文 > 详细

程序转化语意学

时间:2014-12-23 22:39:02      阅读:254      评论:0      收藏:0      [点我收藏+]

标签:

 

 

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

程序转化语意学

标签:

原文地址:http://www.cnblogs.com/xiaoheike/p/4181310.html

(0)
(0)
   
举报
评论 一句话评论(0
登录后才能评论!
© 2014 mamicode.com 版权所有  联系我们:gaon5@hotmail.com
迷上了代码!