C++對象模型學習——構造函數語意學(續)

3、程序轉化語義學(Program Transformation Semantics)

      對於下面的程序片斷:     ios

#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都不正確。優化

     一、顯式的初始化操做(Explicit Initialization)

      已知有這樣的定義:this

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 );
  // ...
}

     二、參數的初始化(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析

構。

       三、返回值的初始化(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;

       四、在使用者面作優化(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的設計是以效率考慮居多,而不是

以」支持抽象化「爲優先。

       五、在編譯器層面作優化(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優化後程序的正確性)。

       六、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的語意學知識!

 

4、成員們的初始化隊伍(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;
};
相關文章
相關標籤/搜索