程序转化语意学

 

 

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

时间: 2024-10-23 14:26:55

程序转化语意学的相关文章

C++对象模型——程序转化语意学(第二章)

2.3    程序转化语意学 (Program Transformation Semantics) 如下程序片段所示: #include "X.h" X foo() { X xx; return xx; } 看到这个代码,可能做出以下假设: 1.    每次foo()被调用,就传回xx的值 2.    如果 class X定义了一个copy constructor,那么当foo()被调用时,保证该copy constructor也会被调用. 第一个假设的真实性,必须视 class X如

深度探索C++对象模型-&gt;2.3 程序转化语意学

一. 1.显示的初始化操作: X x0; void foo_bar() { X x1(x0); X x2 = x0; X x3 = X(x0); } 会被转换成如下步骤(双阶段转化): 1 void foo_bar() 2 { 3 X x1; 4 X x2; 5 X x3; 6 7 x1.X::X(x1); 8 x2.X::X(x2); 9 x3.X::X(x3); 10 } 2.参数(形参)的初始化: 1 void foo(X x0);//声明 2 3 X xx; 4 foo(xx); 5 /

《深度探索C++对象模型》第二章 | 构造函数语意学

默认构造函数的构建操作 默认构造函数在需要的时候被编译器合成出来.这里"在需要的时候"指的是编译器需要的时候. 带有默认构造函数的成员对象 如果一个类没有任何构造函数,但是它包含一个成员对象,该成员对象拥有默认构造函数,那么这个类的隐式默认构造函数就是非平凡的,编译器需要为该类合成默认构造函数.为了避免合成出多个默认构造函数,编译器会把合成的默认构造函数.拷贝构造函数.析构函数和赋值拷贝操作符都以内联的方式完成.一个内联含有具有静态链接,不会被文件以外者看到.如果函数不适合做成内联,就

Inside The C++ Object Model(二)

============================================================================2-0. 关键字explicit被引入C++,提供给程序员一种方法,使他们能够制止"单一参数的constructor"被当做一个conversion运算符. ============================================================================2-1.Default Co

[转]《深度探索C++对象模型》读书笔记[一]

前 言 Stanley B.Lippman1.   任何对象模型都需要的三种转换风味: ü        与编译器息息相关的转换 ü        语言语义转换 ü        程序代码和对象模型的转换 2.   C++对象模型的两种解释 ü        语言中直接支持面向对象程序设计的部分 ü        对于各种支持的底层实现机制 3.   C++ class的完整virtual functions在编译时期就固定下来了,程序员没有办法在执行期动态增加或取代其中某一个.这使得虚拟函数调

构造函数语意学

C++参考手册告诉我们:default constructors -在需要的时候被编译器产生出来.关键字眼是:在需要的时候. 被谁需要? 做什么事? 当编译器需要它的时候(注意是编译器需要,而不是程序的需要),此外被合成出来的constructor只执行编译器所需要的行为(而不会执行程序所需要的行为,这个设计类的程序员负责). C++ standard 规定: 对于class X,如果没有任何user-declared constructor,那么会有一个default constructor被

让cpu运行程序(四)

如何编译汇编程序 这个汇编程序的编译对我们设计的CPU来说非常简单.按照我们设想的CPU中,程序存储器.数据存储器是分开的.因此,不用考虑程序和数据的混淆问题.此外,由于我们设想的指令系统是一条指令就占一个存储单元,所以标号的地址非常容易计算出来.如果我们确定了第一条指令存放的地址,那么从这条指令向后数数,就可以将标号确定下来. 对于变量在什么地方的问题,常常是一般作汇编程序设计的人较为迷惑的问题.产生迷惑的原因主要是我们自己不作变量地址分配.做为一个CPU 的设计者,不仅要对程序如何放入内存进

Web应用程序和Web网站

在牛腩接近尾声调试代码时,有时调试半天也调不出个什么效果,无奈之举,先和源码对比一下,发现我的web层中每个网页下面多个aspx.designer.cs 文件(设计器文件),网上说有这个文件说明你这个项目是web应用程序而不是WEB网站,而普通的WEB网站是aspx(网页信息)+cs(后台代码)的.这是什么情况?我都快哭了,难不成从开始我建立的项目就是错的,又回头看视频中最开始建立项目的阶段,后来证明我的假设是对的,疑问又来了,可是我的绝大部分功能也都可以实现的啊,那他们两个究竟有什么不同,相互

怎样写出一个递归程序

作为小白,我看到递归程序只是能看懂,但是自己写不出来,我知道要有一个临界条件(这个并不难找),但我不知道怎么演进,这让我十分头疼,因此找到了一篇个人认为写的不错的文章如下,根据我对递归的理解和疑问对原文做了一些标注,欢迎各位大佬,写下自己对递归的理解,本小白感激不尽. 如何写一个递归程序 总是听到大大们说递归递归的,自己写程序的时候却用不到递归.其中的原因,一个是害怕写递归,另一个就是不知道什么时候用递归.这篇文章就浅析一下,希望看完之后不再害怕递归,这就是本文最大的目的. 递归到底有什么意义?