對於下面的程序片斷: 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都不正確。優化
已知有這樣的定義: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 ); // ... }
把一個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析
構。
已知下面這個函數定義:
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;
「程序員優化「:定義一個」計算用「的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的設計是以效率考慮居多,而不是
以」支持抽象化「爲優先。
在像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優化後程序的正確性)。
已知下面的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; };