C++对象模型学习——构造函数语意学(续)

三、程序转化语义学(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++编译器所提供的进取优化层级

(degree of aggressive optimization)而定。甚至可以假设在高质量C++编译中,上述两点对

于class X的nontrivial definitions都不正确。

     1、显式的初始化操作(Explicit Initialization)

      已知有这样的定义:

X x0;

       下面三个定义,每一个都明显地以x0来初始化其class object:

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

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

        1)重写每一个定义,其中的初始化操作会被剥除。(这里的“定义”是指“占用内存”的行

为。)

        2)class的copy constructor调用操作会被安插进去。

        例如上面的例子,在明确的双阶段转化之后,foo_bar()可能看起来像这样:

// 可能的转换
// C++伪码
void foo_bar()
{
  X x1; // 定义被重写,初始化操作被剥除
  X x2; // 定义被重写,初始化操作被剥除
  X x3; // 定义被重写,初始化操作被剥除
  
  // 编译器安插X copy construction的调用操作
  x1.X::X( x0 ); // 表现出对copy constructor X::X( const X& xx )的调用
  x2.X::X( X0 );
  x3.X::X( X0 );
  // ...
}

     2、参数的初始化(Argument Initialization)

      把一个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 );

       而这种方法必须改变foo()声明,形式参数必须从原先的一个class X object改变为一个class

X reference,如下:

void foo( X& x0 );

       而临时性的object也会被class X的destructor析构掉。

       另一种实现方式是以“拷贝建构”(copy construct)的方式把实际参数直接建构在其应该的位

置上,此位置视函数活动范围的不同,记录于程序堆栈中。并在程序返回之前用destructor析

构。

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

       已知下面这个函数定义:

X bar()
{
  X xx;
  // 处理 xx...
  return xx;
}

       关于bar()的返回值如何从局部对象xx中拷贝过来,Stroustrup在cfront中的解决做法是一个

双阶转化:       

       1)首先加上一个额外参数,类型是class object的一个reference。这个参数将用来放置被

“拷贝建构(copy constructed)”而得的返回值。

       2)在return指令之前安插一个copy constructor调用操作,以便将欲传回之object的内容当

做上述新增参数的初值。

        bar()按上述算法转换如下:

// 函数转换
// 以反映出copy constructor的应用
// C++伪码
void bar( X& _result ) // 加上一个额外的参数
{
  X xx;
  
  // 编译器所产生的default constructor调用操作
  xx.X::X();
  
  // 编译器所产生的copy constructor调用操作
  _result.X::X( xx );
  
  return;
}

        现在编译器必须转换每一个bar()调用操作,以反映其新定义。如下:

X xx = bar();

         将被转换为下列两个指令句:

// 注意,不必施行default constructor
X xx;
bar( xx );

        而:

bar().memfunc(); // 执行bar()所传回之X class object的memfunc()

        可能被转化为:

// 编译器所产生的临时性对象
X _temp0;
( bar( _temp0 ), _temp0 ).memfunc();

         同理,如果程序声明了一个函数指针,像这样:

X ( *pf )();
pf = bar;

         它也必须被转化为:

void ( *pf )( X& );
pf = bar;

       4、在使用者面做优化(Optimization at the User Level)

       “程序员优化“:定义一个”计算用“的constructor。即:

// 程序员不再写下面的函数
X bar( const T &y, const T &z )
{
  X xx;
  // ...以y和z来处理xx
  return xx; // 这会要求xx被”memberwise“地拷贝到编译器所产生的_result之中
}


// 可以定义另一个constructor,直接结算xx的值
X bar( const T &y, const T &z )
{
  return X( y, z );
}

// 这样转换后可以效率比较高
// C++伪码
void bar( X &_result, const T &y, const T &z )
{
  _result.X::X( y, z );
  return;
}

     可以看到_result直接被计算出来,而不是经由copy constructor拷贝而得。这种做法可能导致

特殊计算用途的constructor大量扩散。在这个层面上,class的设计是以效率考虑居多,而不是

以”支持抽象化“为优先。

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

       在像bar()这样的函数中,所有的return指令传回相同的具名数值,因此编译器可能自己做优

化,方法是以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)优化。考虑如下代

码:

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()函数1000万次
int main()
{
  for( int cnt = 0; cnt < 10000000; cnt++ )
  {
    test t = foo( double( cnt ) );
  }

  return 0;
}

// 整个程序的意义是:重复循环10000000次,每次产生一个test object;
// 每隔test object配置一个拥有100个double的数组:所有的元素都设初值
// 为0,只有#0和#99元素以循环计数器的值作为初值

     上面这个版本不能实施NRV优化,因为test class缺少一个copy constructor。第二版加上一

个inline copy constructor如下:

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

      得到的程序如下:

#include <iostream>
#include <memory.h>

class test
{
  friend test foo( double );
  
  public:
    test(){ memset( array, 0, 100 * sizeof( double ) ); }
    inline test( const test &t ){ memcpy( this, &t, sizeof( test ) ); }

  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()函数1000万次
int main()
{
  for( int cnt = 0; cnt < 10000000; cnt++ )
  {
    test t = foo( double( cnt ) );
  }

  return 0;
}

    下面是各种版本的运行性能分析(通过gprof和time程序)结果:

    1)未实施NRV(源程序为copyCtr4.cc)

    

    2)实施NRV(源程序为copyCtr5.cc)

    3)实施NRV+-O1(源程序为copyCtr5.cc)

         同样也可以从编译器生成的汇编代码看到:

// 未实施NRV优化的main函数
main:
.LFB975:
	...
	jmp	.L5
.L6:
        ...
	call	_Z3food
	subl	$4, %esp
	addl	$1, -812(%ebp)
.L5:
	cmpl	$9999999, -812(%ebp)
	jle	.L6
	movl	$0, %eax
	movl	-4(%ebp), %ecx
        ...

// 实施NRV优化的main函数
main:
.LFB978:
        ...
	jmp	.L5
.L6:
        ...
	call	_Z3food
	subl	$4, %esp
	addl	$1, -812(%ebp)
.L5:
	cmpl	$9999999, -812(%ebp)
	jle	.L6
	movl	$0, %eax
	movl	-4(%ebp), %ecx
	...

// 实施NRV优化和-O1优化的main函数
main:
.LFB1031:
	.loc 1 29 0
	.cfi_startproc
.LVL3:
	subl	$800, %esp
	.cfi_def_cfa_offset 804
	.loc 1 29 0
	movl	$10000000, %eax
.LVL4:
	.p2align 4,,7
	.p2align 3
.L24:
.LBB37:
	.loc 1 30 0 discriminator 2
	subl	$1, %eax
	jne	.L24
.LBE37:
	.loc 1 36 0
	xorl	%eax, %eax
	addl	$800, %esp
	.cfi_def_cfa_offset 4
.LVL5:
	ret

      结果是明显的,NRV并没有太多明显的改善(通过main函数对比看出,可能gcc自动实施了

NRV优化,导致两者main函数相同),而且随着编译器技术的进步,在-O1优化面

前,单独的NRV优化更多成了后辈程序员们对历史的一种纪念(只是从结果看到的效率出发,

并没有验证-O1优化后程序的正确性)。

       6、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 objects带有copy constructor,也没任何的virtual base class或virtual function。所

以,默认情况下,一个Point3d class object的“memberwise”初始化操作会导致“bitwise copy”。

          这样做效率很高,而且很安全,因为三个坐标成员是以数值来存储的。bitwise copy既不

会导致memory leak,也不会产生address aliasing。

          对于这个class,没必要提供一个explicit copy constructor,因为编译器自动实施了最好的

行为。而如果预见class需要大量的memberwise初始化操作,则提供一个copy constructor的

explicit inline函数实例就是非常合理的了——在“编译器提供NRV优化”的前提下。

          实现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(),都只有在“classes不含任何由编译器产生的内部

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

 

四、成员们的初始化队伍(Member Initialization List)

      在下列情况下,为了让程序顺利通过编译,必须使用member initialization list:

      1)当初始化一个reference member时;

      2)当初始化一个const member时;

      3)当调用一个base class的constructor,而它拥有一组参数时;

      4)当调用一个member class的constructor,而它拥有一组参数时;

      在这种情况下,程序可以被正确编译并执行,但是效率不高。例如:

class Word
{
  public:
    Word(){ _name = 0; _cnt = 0; }
  
  private:
    String _name;
    int _cnt;
}

         在这里,Word constructor会先产生一个临时性的String object,然后将它初始化,之后以

一个assignment运算符将临时object指定给_name,随后再摧毁那个临时性object。可能扩张结

果如下:

// C++伪码
Word::Word( /*this pointer goes here*/ )
{
  // 调用String的default constructor
  _name.String::String();

  // 产生临时性对象
  String temp = String( 0 );

  // "memberwise"地拷贝_name
  _name.String::operator=( temp );

  // 摧毁临时性对象
  temp.String::~String();
  
  _cnt = 0;
}

        下面是一个明显更有效率的实现方法:

// 较佳的方式
Word::Word : _name( 0 )
{
  _cnt = 0;
}

         它会被扩张成这样子:

// C++伪码
Word::Word( /*this pointer goes here*/ )
{
  // 调用String( int )constructor
  _name.String( 0 );
  _cnt = 0;
}

         陷阱最有可能发生在这种形式的template code中:

template< class type >
foo< type >::foo( type t )
{
  // 可能是(也可能不是)个好主意
  // 视type的真正类型而定
  _t = t;
}

         这会导致member初始化权在member initialization list中完成,甚至一个行为良好的

member,如_cnt:

// 坚持此种编码风格
Word::Word() : _cnt( 0 ), _name( 0 )
{
}

         那么member initialization list中到底发生了什么?

         编译器会一一操作initialization list,以适当顺序在constructor之内安插初始化操作,并且

在任何explicit user code之前。例如,先前的Word constructor被扩充为:

// C++伪码
Word::Word( /*this pointer goes here*/ )
{
  _name.String::String( 0 );
  _cnt = 0;
}

          实际上:list中的项目顺序是由class中的members声明顺序决定的,不是由initialization

list中的排序顺序决定的。本例的Word class 中,_name被声明于_cnt之前,所以它的初始化也

比较早。

           “初始化顺序”和“initialization list中项目的排列顺序”之间的外观错乱,会导致意想不到的

危险:

#include <stdio.h>

class X
{
  public:
    int i;
    int j;
  
  public:
    X( int val ) : j( val ), i( j ){ } // 有陷阱的写法
};

class Y
{
  public:
    int i;
    int j;
  
  public:
    Y( int val ) : j( val ){ i = j; } // 修改后的写法(建议写法)
};

int main()
{
  X x( 3 );
  Y y( 5 );

  printf( "x.i = %d  x.j = %d\n", x.i, x.j );
  printf( "y.i = %d  y.j = %d\n", y.i, y.j );

  return 0;
}

       可以看到x.i果然不是所期望的3。initialization list的项目被放在explicit code之前。

       可以像下面这样,调用一个member function以设定一个member的初值:

// X::xfoo()被调用
X::X( int val ) : i( xfoo( val ) ), j( val )
{}

         其中xfoo()是X的一个member function。但我们不知道foo()对X object的依赖性有多高,如

果把xfoo()放在constructor体内,那么对于“到底哪一个member在xfoo()执行时间被设立初值”这

件事,就可以确保不会发生模棱两可的情况了。

          Member function的使用是合法的,这是因为和此object相关的this指针已经被建构妥当,

而当constructor大约被扩充为:

// C++伪码:constructor被扩充后的结果
X::X( /*this pointer, */ int val )
{
  i = this->xfoo( val );
  j = val;
}

          最后,如果一个derived class member function被调用,其返回值被当做base class

constructor的一个参数,将会如何:

// 调用FooBar::fval()
class FooBar : public X
{
  public:
    int fval(){ return _fval; }
    FooBar( int val ) : _fval( val ), X( fval() ){  }  
                       // fval()作为base class constructor的参数
  
  private:
    int _fval;
}

        它的可能扩张结果:

// C++伪码
FooBar::FooBar( /* this pointer goes here */ )
{
  X::X( this, this->fval() ); //的确不是一个好主意
  _fval = val;
};

 

转载于:https://my.oschina.net/u/2537915/blog/706603

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值