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如何定义而定,第二个假设的真实性,虽然也有部分必须视 class X如何定义而定,但最主要的还是视C++的编译器所提供的进取性优化程度(degree of aggressive optimization)而定,甚至可以假设在一个高品质的C++编译器中,上述两点对于 class X的nontrivial definitions都不正确。

明确的初始化操作 (Explicit Initialization)

    已知有这样的定义:
X x0;
//下面有三个定义,每一个都明显地以x0来初始化其 class object:
void foo_bar() {
    X x1(x0);
    X x2 = x0;
    X x3 = X(x0);
}
    必要的 程序转化有两个阶段
    1.    重写每一个定义,其中的初始化操作会被剥除。
    2.    class 的copy constructor调用操作会被插入。

    例如,在显式的双阶段转化之后,foo_bar()可能看起来像这样:
//    可能的程序转换
//    C++伪码
void foo_bar() {
    X x1;    //定义被重写,初始化操作被剥除
    X x2;    //定义被重写,初始化操作被剥除
    X x3;    //定义被重写,初始化操作被剥除
    //    编译器插入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);

参数的初始化 (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++伪码
// 编译器产生出来的暂时对象
X __temp0;
// 编译器对copy constructor的调用
__temp0.X::X(xx);    
// 重新改写函数调用操作,以便使用上述的暂时对象
foo(__temp0);
    注意: 在C++中作用域运算符::的优先级最高,__temp0.X::X(xx)相当于__temp0.(X::X(xx))
    然而这样的转换只做了一半功夫,问题出在foo()的声明。暂时性object先以 class X的copy constructor正确地设定了初值,然后再以bitwise方式拷贝到x0这个局部实体中。foo()的声明因而也必须被转化,形式参数必须从原先的一个 class X object改变为一个 class X reference,如这样:
void foo(X &x0);
    其中 class X声明了一个destructor,它会在foo()函数完成之后被调用,对付那个暂时性的object。
    另一种实现方法是以"拷贝建构"(copy construct)的方式把实际参数直接建构在其应该的位置上,该位置视函数活动范围的不同记录与程序堆栈中。在函数返回之前,局部对象(local object)的destructor(如果有定义的话)会被执行,Borland C++编译器就是使用此法,但它也提供一个编译选项,用以指定前一种做法,以便和其早期版本兼容。

返回值的初始化 (Return Value Initialization)

    已知下面这个函数定义
X bar() {
    X xx;
    // 处理xx...
    return xx;
}
    可能会问 bar()的返回值如何从局部对象xx中拷贝过来?Stroustrup在cfront中的解决办法是一个双阶段转化:
    1.    首先加上一个额外参数,类型是 class object的一个reference。这个参数将用来放置被"拷贝建构(copy constructed)"而得到的返回值
    2.    在 return 指令之前插入一个copy constructor调用操作,以便将欲传回的objace的内容当做上述新增参数的初值。

    真正的返回值是什么?最后一个转化操作会重新改写函数,使它不传回任何值,根据这样的算法,bar()转换如下:
// 函数转换以反映出copy constructor的应用
// C++伪代码
void bar(X &__result)    // 加上一个额外参数
{
    X xx;
    // 编译器所产生的default constructor调用操作
    xx.X::X();
    // ...处理xx
    // 编译器所产生的copy constructor调用操作
    __result.X::XX(xx);
    return;
}
    现在编译器必须转换每一个bar()调用操作,以反映其新定义。例如:
X xx = bar();
    将被转换为下列两个指令句:
// 注意,不必施行default constructor
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)

    Jonathan Shopiro提出 "程序员优化"的观念:定义一个"计算用"的constructor,换句话说程序员不再写:
X bar(const T &y, const T &z) {
    X xx;
    // ...以y和z来处理xx
    return xx;
}
    那会要求xx被"memberwise"地拷贝到编译器所产生的__result中,Jonathan定义另一个constructor,可以直接计算xx的值:
X bar(const T &y, const T &z) {
    return X(y, z);
}
    于是当bar()定义被转换之后,效率会比较高:
// C++伪代码
void bar(X &__result)
{
    __result.X::X(y, z);
    return;
}
    __result被直接计算出来,而不是经由copy constructor拷贝得到。不过这种解决方法受到了某种批评,担心那些特殊计算用途的constructor可能会大量扩散。在这个层次上, class 的设计是以效率考虑居多,而不是以"支持抽象化"为优先。

在编译器层面做优化 (Optimization at the Compiler Level)

    在一个如bar()这样的函数中,所有的 return 指令返回相同的具名数值(named value),因此 编译器可能自己优化,方法是以result参数取代named return value。例如下面的
bar()定义:
X bar() {
    X xx;
    // ...处理xx
    return xx;
}
    编译器把其中的xx以及__result取代:
void bar(X &__result) {
    // default constructor 被调用
    // C++伪代码
    __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 class object:
test foo(double val) {
    test local;
    
    local.array[0] = val;
    local.array[99] = val;

    return local;
}
    有一个main()函数调用上述foo()函数一千万次:
main()
{
    for (int cnt = 0; cnt < 10000000; cnt++) {
        test t = foo(double(cnt));
    }
    return 0;
}
// 整个程序的意义是重复循环1000万次,每次产生一个test object
// 每个test object配置一个拥有100个double的数组,所有的元素都设初值为0
// 只有#0和#9元素以循环计数器的值作为初值
    这个程序的第一个版本不能实施NRV优化,因为test class 缺少一个copy constructor。第二个版本加上一个online 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优化提供了重要的效率改善,它还是饱受批评,其中一个原因是,优化由编译器默默完成,而它是否真的被完成,并不十分清楚。第二个原因是,一旦函数变得比较复杂,优化也就变得比较难以施行。
    上述两个批评主要关心的是编译器可能在实施优化时失败。第三个批评则是从相反的方向出发,某些程序员真的不喜欢应用程序被优化。想象已经摆好了copy constructor的阵势,使程序"以copying 方式产生一个object时",对称地调用destructor,例如:
void foo() {
    // 这里希望有一个copy constructor
    X xx = bar();
    // ...
    // 这里调用destructor
}
    在此情况下,对称性被优化给打破:程序虽然比较快,确实错误的。难道编译器因为抑制copy constructor的调用而在这里出错吗?也就是说,难道copy constructor在"object是经由copy而完成其初始化"的情况下,一定要被调用吗?
    这样的需求在许多程序中可能会被征以严格的"效率税",例如,虽然下面三个初始化操作在语意上相等:
X xx0(1024);
X xx1 = X(1024);
X xx2 = (X)1024;
    但是在第二行和第三行中,语法明显地提供了两个步骤的初始化操作:
    1.    将一个暂时性的object设以初值1024
    2.    将暂时性的object以拷贝建构的方式作为explicit object的初值
    换句话说,xx()是被单一的constructor操作设定初值:
// C++伪代码
xx0.X::X(1024);
    而xx1或xx2却调用两个constructor,产生一个暂时性object,并针对该暂时性objet调用 class X的destructor:
// C++伪代码
X __temp0;
__temp0.X::X(1024);
xx1.X::X(__temp0);
__temp0.X::~X();
    一般而言, 面对"以一个 class object作为另一个 class object的初值"的情况,语言允许编译器有大量的自由发挥空间,其好处是导致码产生时有明显的效率提升,缺点是不能够安全地规划copy constructor的副作用,必须视其执行而定。

Copy Constructor:要还是不要?

    已知下面的3D坐标点类:
class Point3d {
public:
    Point3d(float x, float y, float z);
private:
    float _x, _y, _z;
};
    上述 class 的 default copy constructor被视为trivial。它既没有任何member(或base) class object带有的copy constructor,也没有任何的 virtual base class 或 virtual function,所以 默认情况下,一个Point3d class object的"memberwise"初始化操作会导致"bitwise copy",这样的效率很高,但是安全吗?
    答案是 yes,三个坐标成员是以数值来储存的,bitwise copy既不会导致memory leak,也不会产生address aliasing,因此它既快速又安全。
    那么,这个 class 的设计者应该提供一个 explicit copy constructor吗?答案是no,没有理由提供一个copy constructor函数实体,因为编译器自动实施了最好的行为。比较难以回答的是,是否预见 class 需要大量的memberwise初始化操作,例如以传值(by value)的方式传回objects?如果答案是yes,那么提供一个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(),都只有在"class不含任何由编译器产生的内部members"时才能有效地运行。 如果Point3d class 声明一个或一个以上的 virtual functions,或内含一个 virtual base class ,那么使用上述函数将会导致那些"被编译器产生的内部members"的初值被改写。例如,已知下面声明:
class Shape {
public:
    // 这会改变内部的vptr
    Shape() {
        memset(this, 0, sizeof(Shape));
    }
    virtual ~Shape();
};
    编译器为此constructor扩张的内容看起来像是:
// 扩张后的constructor
// C++伪代码
Shape::Shape() {
    // vptr必须在使用者的代码执行之前先设定妥当
    __vptr__Shape = __vtbl__Shape;
    // memset会将vptr清为0
    memset(this, 0, sizeof(Shape));
};
    如上所述,如要正确使用memset()和memcpy(),需要掌握某些C++ Object Model的语意学知识。

总结

    copy constructor的应用,迫使编译器多多少少对程序代码做部分转化,尤其 是当一个函数以传值(by value)的方式传回一个 class object,而该 class 有一个copy constructor(不论是显式定义的,或是合成的)时,这将导致深奥的程序转化——不论在函数的定义或使用上。此外 编译器也将copy constructor的调用操作优化,以一个额外的第一参数(数值被直接存放其中)取代NRV。程序员如果了解那些转换,以及copy constructor优化后的可能状态,就比较能够控制程序的执行效率。



  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值