C++对象模型剖析(三)一一构造函数语义学(二)

构造函数语义学(二)

程序转化语意学 Program Transformation

看看书上的例子

#include "X.h"
X foo() {
    X xx;
    // ....
    return xx;
}

一个人可能会做出一下假设

  • 每次 foo() 调用,就传回 xx 的值
  • 如果 class X 定义了一个 copy constructor, 那么当 foo() 被调用时,保证该 copy constructor 也会被调用。

第一个假设的真实性,必须视 class X 如何定义而定。第二个假设的真实性,虽然也部分地必须视 class X 如何定义而定,但最主要的还是视你的C++编译器所提供的进取优化层级(degree of aggressive optimization)而定。一个人甚至可以假设在一个高质量的C++编译器中,上述两点对于 class X 的 nontrivial definisions 都不正确。

  • 显式的初始化操作 Explicit Lnitiation

    看下面的定义

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

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

    • **重写每一个定义,其中的初始化操作会被剥除。**在C++用词中,定义 是指 “占用内存” 的行为。
    • class 的 copy constructor 调用操作会被安插进去。

    所以,经过编译器重定义后的代码是这样的

    void foo_bar() {
        // 定义被重写,初始化操作被剥离
        X x1;
        X x2;
        X x3;
        
        // 编译器安插 X copy constructor 的调用操作
        // X::X( const X& x);
        x1.X::X(x0);
        x2.X::X(x0);
        x3.X::X(x0);
    }
    
  • 参数的初始化 Argument Initialization

    C++ Standard(Section 8.5)说,把一个 class object 当作参数传给一个 函数或是作为一个函数的返回值,相当于这样子的操作

    X xx = arg;
    // 其中xx代表形式参数
    // arg 是真正的参数
    

    这句话的意思就是,在我们将一个参数传递给一个函数或者将一个值从函数中返回时,编译器并不会直接将这个参数传递给函数或直接返回,它会先构建一个临时 class object,将参数或将要返回的值拷贝早这个临时的 class object 中,然后再对这个 class object 进行操作

    X xx;
    void foo(X xx) { ... }
    void foo(xx);
    
    // 编译器会将上面的 foo 调用进行改写
    X __temp0;
    __temp0.X::X(xx); 	// copy constructor
    // 重新改写函数调用操作,以便使用上面的临时对象
    foo(__temp0);
    

    但是上面的调用任然存在问题,传递参数的时候会发生 bitwise 拷贝(我们要尽可能地避免 bitwise copy)

    void foo(const X&);
    

    另一种实现方法是 **以“拷贝构建”(copy constructor)的方式把实际参数直接建构再其应该的位置上,**此位置视函数活动范围的不同,记录于程序堆栈之中。在函数返回之前,局部对象(local object)的 destructor 会被执行。

  • 返回值的初始化 Return Value Initialization

    在 Stroustrup 在 cfront 中 通过一个双阶段转化,将一个函数内的临时对象进行返回。

    • 首先加上一个额外的参数,类型是 class object 的一个 reference。这个参数将用来放置被 “拷贝构建(copy constructed)” 而得的返回值。
    • 在 return 指令之前安插一个 copy constructor 调用操作,以便将将要传回的 object 的内容当作上述新增参数的初值。
    // 原版
    X bar() {
        X xx;
        // handle xx
        return xx;
    }
    
    // 经过编译器重写的 bar()
    viod 
    bar ( X& __result ) // 加上了一个额外的参数
    {
        X xx;
        
        // 编译器产生的 default constructor 调用操作
        xx.X::X();
        
        // ...处理 xx
        
        // 编译器产生的 copy constructor 调用操作
        __result.X::X(xx);
        return;
    }
    
    // 在函数外的调用也需要发生改变
    X xx = bar();
    // 将定义与初始化的操作分离了
    X xx;	// 注意:这里是编译器改的,并不会调用 default constructor
    bar(xx);
    
    // 看看下面的这个转变
    bar().memfunc();
    // 编译器产生一个临时对象进行适配
    X __temp0;
    (bar(__temp0), __temp0).memfunc();
    
  • 在使用者层面做优化(Optimization at the User Level)

    提示:这一点十分抽象,请大家理性吸收

    这里作者在书上提出了一个观念:**定义一个 计算用 的 constructor。**看看例子。

    // 一般实现
    X bar(cosnt T& y, const T& z) 
    {
        X xx;
        // 通过y和z来计算 xx
        return xx;
    }
    
    // 作者在书中的写法
    X bar(const T& y, const T& z)
    {
        return X(y, z);		// 逆天,直接在 constructor 中进行计算
    }
    

    不可否认的是上面的做法确实效率要高一点,但是我并不推荐,作者在书中也说了,这种理念受到批评。

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

    还是这段代码

    X bar()
    {
        X xx;
        // 。。。处理xx
        return xx;
    }
    
    // 编译器做的优化
    void bar(X& __result)
    {
        // default constructor 调用
        // C++伪码
        __result.X::X();
        
        // ......直接处理__result
        return;
    }
    

    这样的编译器优化操作,有时候被称为 Named Retrun 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[99] = val;
        
        return local;
    }
    
    int main()
    {
        for (int cnt = 0; cnt < 100000000; ++cnt)
            	test t = foo(double(cnt));
        return 0;
    }
    

    上面这段代码并不能激活编译器的 NRV 优化,我们需要给它加上一个 copy constructor。

    inline test::test(const test& t)
    {
        memcpy(this, &t, sizeof(test));
    }
    
    0544df056744e9e113ab99ab1ef9253

    这个是书上测试的结果,-O是gcc自带的一种优化策略。

    NRV 优化的两个缺点:

    • 这种优化由编译器默默完成,而它是否真的被完成,并不清楚
    • 一旦函数变得复杂,优化也会变得比较难以执行。在 cfront 中,只有当所有 named return 指令句发生于函数的 top level ,优化才会施行。如果导入 “a nested local block with a return statement”, cfront 就会将优化关闭。

    看到一篇关于NRV的文章,可以看看

    理解NRV优化-CSDN博客

    总结上面的文章的观点就是:NRV优化会将开销较大的拷贝构造替换成别的构造函数,如果一个 class 中没有定义 copy construcotor 那么编译器就认为 通过 bitwise copy 可以达到程序员的要求(因为 bitwise copy 原本就是比较高效的),所以如果一个函数中没有显式定义出 copy constructor 那么编译器将不会进行 NRV优化。

    总结:一般而言,面对 “以一个 class object 作为另一个 class object 的初值” 的情形,语言允许编译器有大量的自由发挥的空间。其好处是能够在机器产生机器码的时候有明显的效率的提升。缺点是程序员并不能够安全地规划 copy construcotr 的副作用,必须视其执行而定。

    我们什么时候需要定义 copy costructor

    先看第一个例子

    class Point3d {
    public:
        Point3d( float x, float y, float z );
    private:
        float _x, _y, _z;
    };
    

    这种时候,就没有必要设置一个 copy cosntructor,这种情况下使用 bitwise copy 既高效又安全。

    下面看看两种不同的 copy cosntructor

    // 1
    Point3d::Point3d( const Point3d &rhs)
    {
        _x = rhs._x;
        _y = rhs._y;
        _z = rhs._z;
    }
    
    // 2
    Point3d::Point3d(const Point3d &rhs)
    {
        memcpy(this, &rhs, sizeof(rhs));
    }
    

    需要注意的是第二种情况,在 class 中有定义 virtual function 或 class 有 virtual base class 的时候不能这么使用,因为那样会导致编译器在使用者代码执行前设置的值被更改(比如:vptr)

    class Shape {
    public:
        Shape() 
        {
            memset(this, 0. sizeof(Shape));
        }
        
        virtual ~Shape();
    }
    
    // 编译的时候,编译器会为构造函数进行扩张
    Shape::Shape()
    {
        __vptr__Shpae = _vtbl__shape;
        memset( this, 0, sizeof(Shape) );
    }
    
  • 23
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值