天天看點

c++成員函數指針揭秘1 前言2 文法3 與其它語言機制的混合使用4 實作5 語言限制與陷阱

原帖位址  http://blog.csdn.net/xlie/article/details/3031966#_Toc133650388

目 錄

 1 前言

2 文法

3 與其它語言機制的混合使用

3.1 繼承

3.2 虛函數

3.2 多繼承

4 實作

4.1 Microsoft的實作

4.1.1 内部表示

4.1.2 Vcall_addr實作

4.1.3 This指針調整

4.1.4 結論

4.2 GCC的實作

4.2.1 内部表示

4.2.2 實作機制

5 語言限制與陷阱

5.1 例子

5.2 static_cast幹了些什麼

5.3 預設的轉換

5.4 教訓

5.5 如何避開陷阱

1 前言

C++語言支援指向成員函數的指針這一語言機制。就像許多其它C++語言機制一樣,它也是一把雙刃劍,用得好,能夠提高程式的靈活性、可擴充性等等,但是也存在一些不易發現的陷阱,我們在使用它的時候需要格外注意, 尤其是在我們把它和c++其它的語言機制合起來使用的時候更是要倍加當心。

關鍵字:成員函數指針,繼承,虛函數,this指針調整,static_cast

2 文法

C++成員函數指針(pointer to member function)的用法和C語言的函數指針有些相似.

下面的代碼說明了成員函數指針的一般用法:

class ClassName {public: int foo(int); }

int (ClassName::*pmf)(int) = &ClassNmae::foo;

ClassName  c;   //.*的用法,經由對象調用

(c.*pmf)(5);      // A

ClassName *pc = &c;  //->*的用法,經由對象指針調用

(Pc->*pmf)(6);   // B

使用typedef可以讓代碼變得略微好看一點:

typedef int (ClassName::*PMF)(int);

PMF pmf = &ClassName::foo;

注意擷取一個成員函數指針的文法要求很嚴格:

1)        不能使用括号:例如&(ClassName::foo)不對。

2)        必須有限定符:例如&foo不對。即使在類ClassName的作用域内也不行。

3)        必須使用取位址符号:例如直接寫ClassName::foo不行。(雖然普通函數指針可以這樣)

是以,必須要這樣寫:&ClassName::foo。

C++成員函數的調用需要至少3個要素:this指針,函數參數(也許為空),函數位址。上面的調用中,->*和.*運算符之前的對象/指針提供了this(和真正使用this并不完全一緻,後面會讨論),參數在括号内提供,pmf則提供了函數位址。

注意這裡成員函數指針已經開始顯示它“異類”的天性了。上面代碼中注釋A和B處兩個表達式,産生了一個在C++裡面沒有類型(type)的“東西”(這是C++語言裡面唯一的例外,其它任何東西都是有類型的),這就是.*和->*運算符:

(c.*pmf)

(Pc->*pmf)

這兩個運算符求值生成的“東西”我們隻知道可以把它拿來當函數調用一樣使喚,别的什麼也不能幹,甚至都不能把它存在某個地方。就因為這個原因,Andrei Alexandrescu 在他那本著名的《Modern c++ design》裡面就說,成員函數指針和這兩個操作符号是“curiously half-baked concept in c++”。(5.9節)

C++裡面引入了“引用”(reference)的概念,可是卻不存在“成員函數的引用”,這也是一個特殊的地方。(當然,我們可以使用“成員函數指針”的引用,呵呵)

3 與其它語言機制的混合使用

C++是一種Multi-Paradigm的語言,各種語言機制混合使用也是平常的事。這裡我們隻提幾種會影響到成員函數指針實作和運作的語言機制。

3.1 繼承

根據C++語言規定,成員函數指針具有contravariance特性,就是說,基類的成員函數指針可以指派給繼承類的成員函數指針,C++語言提供了預設的轉換方式,但是反過來不行。

3.2 虛函數

首先要說明,指向虛拟成員函數(virtual function member)的指針也能正确表現出虛拟函數的特性。舉例說明如下:

    class B { public: virtual int foo(int) {return 0; } };

    class D : public B { public: virtual int foo(int) {  return 0; } };

     int (B::*pmf)(int) = &B::foo;

     D d;

     B* pb = &d;

     (d.*pmf)(0);     //這裡執行D::foo

     (pb->*pmf)(0);   //這裡執行D::foo,多态

C++借由虛函數提供了運作時多态特性,虛函數的實作和普通函數有很大的不同。一般編譯器都是采用大家都熟悉的v-table (virtual function table)的方式。所有的虛函數位址存在一個函數表裡面,類對象中存儲該函數表的首位址(vptr_point)。運作時根據this指針、虛函數索引和虛函數表指針找到函數調用位址。

因為這些不同,是以成員函數指針碰上虛函數的時候,也需要作特殊的處理,才能正确表現出所期望的虛拟性質。

3.2 多繼承

這裡扯上多繼承,是因為多繼承的存在導緻了成員函數指針的實作的複雜性。這是因為編譯器有時候需要進行”this”指針調整。

舉例說明如下:

class B1{};

class B2{};

class D: public B1, public B2{}

假設上面三個對象都不涉及到虛函數,D在記憶體中的典型布局如下圖所示(如果有虛函數則多一個vptr指針,差别不大):

現在假設我們經由D對象調用B2的函數,

D d;

d.fun_of_b2(); 

這裡傳給fun_of_b2的this指針不能是&d, 而應該對&d加上一個偏移,得到D内含的B2子對象的首位址處。

成員函數指針的實作必須考慮這種情況。

多繼承總是不那麼受歡迎。不過即使是單繼承,上面的情況也會出現。考慮下面的例子:

class B{};  //non-virtual class

class D :public B{}; //virtual class

假設B是一個普通的類,沒有虛拟成員函數。而D加上了虛拟成員函數。那麼D的典型記憶體布局如下圖所示:

因為D引入了vptr指針,而一般的實作都将vptr放在對象的開頭,這就導緻經由D對象通路B的成員函數的時候,仍然需要進行this指針的調整。

D d;

d.fun_of_b();  //this 指針也需要調整,否則fun_of_b的行為就會異常

4 實作

從上面一節我們可以看到,編譯器要實作成員函數指針,有幾個問題是繞不過去的:

1)    函數是不是虛拟函數,這個涉及到虛函數表(__vtbl)的通路。

2)    函數運作時,需不需要調整this指針,如何調整。這個涉及到C++對象的記憶體布局。

事實上,成員函數指針必須記住這兩個資訊。為什麼要記住是否為虛函數就不用解釋了。但是this指針調整為什麼要記住呢?因為在.*和->*運算符求值時必須用到。 考慮上面那個多繼承的例子:

int (D::*pmf)(int) = &B2::foo_of_b2;  //A

D d;                          

(d.*pmf)(0);                      //B

看看上面的代碼,其實我們在A處知道需要進行this指針調整,也知道該怎麼調整。但是這時候this還沒出世呢,還不到調整的時候。到了B處終于有了This指針了,可是又不知道該怎樣調整了。是以pmf必須記住調整方式,到了B處調用的時候,再來進行調整。

4.1 Microsoft的實作

4.1.1 内部表示

Microsoft VC的實作采用的是Microsoft一貫使用的Thunk技術(不知道這個名字怎麼來的,不過有趣的是把它反過來拼寫就變成了大牛Knuth的名字,呵呵)。

對于Mircosoft來說,成員函數指針實際上分兩種,一種需要調節this指針,一種不需要調節this指針。

先厘清楚那些情況下成員函數指針需要調整this指針,那些情況下不需要。回憶上一節讨論的c++對象記憶體布局的說明,我們可以得出結論如下:

如果一個類對象obj含有一些子對象subobj,這些子對象的首位址&subobj和對象自己的首位址&obj不等的話,就有可能需要調整this指針。因為我們有可能把subobj的函數當成obj自己的函數來使用。

根據這個原則,可以知道下列情況不需要調整this指針:

1)  繼承樹最頂層的類。

2)  單繼承,若所有類都不含有虛拟函數,那麼該繼承樹上所有類都不需要調整this指針。

3)  單繼承,若最頂層的類含有虛函數,那麼該繼承樹上所有類都不需要調整this指針。

下列情況可能進行this指針調整:

1)  多繼承

2)  單繼承,最頂的base class不含virtual function,但繼承類含虛函數。那麼這些繼承類可能需要進行this指針調整。

Microsoft把這兩種情況分得很清楚。是以成員函數的内部表示大緻分下面兩種:

struct pmf_type1{

      void* vcall_addr;

};

struct pmf_type2{

      void* vcall_addr;

      int  delta;  //調整this指針用

};

       這兩種表示導緻成員函數指針的大小可能不一樣,pmf_type1大小為4,pmf_type2大小為8。有興趣的話可以寫一段代碼測試一下。

4.1.2 Vcall_addr實作

上面兩個結構中出現了vcall_addr, 它就是Microsoft 的Thunk技術核心所在。簡單的說,vcall_addr是一個指針,這個指針隐藏了它所指的函數是虛拟函數還是普通函數的差別。事實上,若它所指的成員函數是一個普通成員函數,那麼這個位址也就是這個成員函數的函數位址。若是虛拟成員函數,那麼這個指針指向一小段代碼,這段代碼會根據this指針和虛函數索引值尋找出真正的函數位址,然後跳轉(注意是跳轉jmp,而不是函數調用call)到真實的函數位址處執行。

看一個例子。

//源代碼

class  C

{

public:

     int nv_fun1(int) {return 0;}

     virtual int v_fun(int) {return 0;}

     virtual int v_fun_2(int) {return 0;}

};

void foo(C *c)

{

     int (C::*pmf)(int);

     pmf = &C::nv_fun1;

     (c->*pmf)(0x12345678);

     pmf = &C::v_fun;

     (c->*pmf)(0x87654321);

     pmf = &C::v_fun_2;

     (c->*pmf)(0x87654321);

}

; foo的彙編代碼,release版本,部分地方進行了優化

:00401000 56                      push esi

:00401001 8B742408                mov esi, dword ptr [esp+08]

; pmf = &C::nv_fun1;

; (c->*pmf)(0x12345678);

:00401005 6878563412              push 12345678

:0040100A 8BCE                    mov ecx, esi ;this

:0040100C E81F000000              call 00401030

; pmf = &C::v_fun;

; (c->*pmf)(0x87654321);

:00401011 6821436587              push 87654321

:00401016 8BCE                    mov ecx, esi  ;this

:00401018 E803070000              call 00401720

; pmf = &C::v_fun_2;

;    (c->*pmf)(0x87654321);

:0040101D 6821436587              push 87654321

:00401022 8BCE                    mov ecx, esi  ;this

:00401024 E807070000              call 00401730

:00401029 5E                      pop esi

:0040102A C3                      ret

:00401030 33C0    ; 函數實作       xor eax, eax    

:00401032 C20400                  ret 0004

:00401720 8B01    ; vcall           mov eax, dword ptr [ecx]

:00401722 FF20                    jmp dword ptr [eax]

:00401730 8B01    ; vcall          mov eax, dword ptr [ecx]

:00401732 FF6004                  jmp [eax+04]

從上面的彙編代碼可以看出vcall_addr的用法。00401030, 00401720, 00401730都是vcall_addr的值,其實也就是pmf的值。在調用的地方,我們不能分别出是不是虛函數,所看到的都是一個函數位址。但是在vcall_addr被當成函數位址調用後,進入vcall_addr,就有差別了。00401720, 00401730是兩個虛函數的vcall,他們都是先根據this指針,計算出函數位址,然後jmp到真正的函數位址。00401030是C::nv_fun1的真實位址。

       Microsoft的這種實作需要對一個類的每個用到了的虛函數,都分别産生這樣的一段代碼。這就像一個template函數:

       template <int index>

     void vcall(void* this)

    {

        jmp this->vptr[index]; //pseudo asm code

    }

每種不同的index都要産生一個執行個體。

Microsoft就是采用這樣的方式實作了虛成員函數指針的調用。

4.1.3 This指針調整

不過還有一個this調整的問題,我們還沒有解決。上面的例子為了簡化,我們故意避開了this指針調整。不過有了上面的基礎,我們再讨論this指針調整就容易了。

首先我們需要構造一個需要進行this指針調整的情況。回憶這節開頭,我們讨論了哪些情況下需要進行this指針調整。我們用一個單繼承的例子來進行說明。這次我們避開virtual/non-virtual function的問題暫不考慮。

class B {

public:

     B():m_b(0x13572468){}

     int b_fun(int)     {

         std::cout<<'B'<<std::endl;

         return 0;

     }

private:

     int m_b;

};

class D : public B {

public:

     D():m_d(0x24681357){}

     virtual int foo(int)   {

         std::cout<<'D'<<std::endl; 

         return 0;

     }

private:

     int m_d;

};

// 注意這個例子中virtual的使用

void test_this_adjust(D *pd, int (D::*pmf)(int))

{

     (pd->*pmf)(0x12345678);

}

:00401000   mov eax, dword ptr [esp+04] ; this入參

:00401004   mov ecx, dword ptr [esp+0C] ; delta入參

:00401008   push 12345678 ;參數入棧

:0040100D   add ecx, eax ; this = ecx= this+delta

:0040100F   call [esp+0C] ; vcall_addr入參

:00401013   ret

void test_main(D *pd)

{

     test_this_adjust(pd, &D::foo);

     test_this_adjust(pd, &B::b_fun);

}

; test_this_adjust(pd, &D::foo);

:00401020  xor ecx, ecx

:00401022  push esi

:00401023  mov esi, dword ptr [esp+08] ; pd, this指針

:00401027  mov eax, 004016A0 ; D::foo vcall位址

:0040102C  push ecx ; push delat = 0, ecx=0

:0040102D  push eax ; push vcall_addr

:0040102E  push esi  ; push this

:0040102F  call 00401000 ; call test_this_adjust

; test_this_adjust(pd, &B::b_fun);

:00401034  mov ecx, 00000004 ;和上面的調用不同了

:00401039  mov eax, 00401050 ; B::b_fun位址

:0040103E  push ecx ; push delta = 4, exc=4

:0040103F  push eax ; push vcall_addr, B::b_fun位址

:00401040  push esi ; push this

:00401041  call 00401000  ; call test_this_adjust

:00401046  add esp, 00000018

:00401049  pop esi

:0040104A  ret

注意這裡和上面一個例子的差別:

在調用test_this_adjust(pd, &D::foo)的時候,實際上傳入了3個參數,調用相當于

         test_this_adjust(pd, vcall_address_of_foo, delta(=0));

調用test_this_adjust(pd, &B::b_fun)的時候,也是3個參數

         test_this_adjust(pd, vcall_address_of_b_fun, delta(=4));

兩個調用有個明顯的不同,就是delta的值。這個delta,為我們後來調整this指針提供了幫助。

再看看test_this_adjust函數的彙編代碼,和上一個例子的不同,也就是多了一句代碼:

:0040100D   add ecx, eax ; this = ecx= this+delta

這就是對this指針作必要的調整。

4.1.4 結論

       Microsoft根據情況選用下面的結構表示成員函數指針,使用Thunk技術(vcall_addr)實作虛拟函數/非虛拟函數的自适應,在必要的時候進行this指針調整(使用delta)。

struct pmf_type1{

     void* vcall_addr;

};

struct pmf_type2{

     void* vcall_addr;

     int  delta;  //調整this指針用

};

4.2 GCC的實作

        GCC對于成員函數指針的實作和Microsoft的方式有很大的不同。

4.2.1 内部表示

GCC對于成員函數指針統一使用類似下面的結構進行表示:

struct

{

    void* __pfn;  //函數位址,或者是虛拟函數的index

    long __delta; // offset, 用來進行this指針調整

};

4.2.2 實作機制

先來看看GCC是如何區分普通成員函數和虛拟成員函數的。

不管是普通成員函數,還是虛拟成員函數,資訊都記錄在__pfn裡面。這裡有個小小的技巧。我們知道一般來說因為對齊的關系,函數位址都至少是4位元組對齊的。這就意味這一個函數的位址,最低位兩個bit總是0。(就算沒有這個對齊限制,編譯器也可以這樣實作。) GCC充分利用了這兩個bit。如果是普通的函數,__pfn記錄該函數的真實位址,最低位兩個bit就是全0,如果是虛拟成員函數,最後兩個bit不是0,剩下的30bit就是虛拟成員函數在函數表中的索引值。

使用的時候,GCC先取出最低位兩個bit看看是不是0,若是0就拿這個位址直接進行函數調用。若不是0,就取出前面30位包含的虛拟函數索引,通過計算得到真正的函數位址,再進行函數調用。

GCC和Microsoft對這個問題最大的不同就是GCC總是動态計算出函數位址,而且每次調用都要判斷是否為虛拟函數,開銷自然要比Microsoft的實作要大一些。這也差不多可以算成一種時間換空間的做法。

在this指針調整方面,GCC和Mircrosoft的做法是一樣的。不過GCC在任何情況下都會帶上__delta這個變量,如果不需要調整,__delta=0。

這樣GCC的實作比起Microsoft來說要稍簡單一些。在所有場合其實作方式都是一樣的。而且這樣的實作也帶來多一些靈活性。這一點下面“陷阱”一節再進行說明。

GCC在不同的平台其實作細節可能略有不同,我們來看一個基于Intel平台的典型實作:

//source code

int test_fun(Base *pb, int (Base::*pmf)(int))

{

     return (pb->*pmf)(4);

}

//assembly

8048478:    push   %ebp

 8048479:  mov    %esp,%ebp

 804847b:  sub    $0x18,%esp

 804847e:  mov    0xc(%ebp),%eax   ;__pfn, 入參

 8048481:  mov    0x10(%ebp),%edx  ;__delta, 入參

 8048484:  mov    %eax,0xfffffff8(%ebp)  ; __pfn

 8048487:  mov    %edx,0xfffffffc(%ebp)  ; __delta

 804848a:  sub    $0x8,%esp           ;

 804848d:  mov    0xfffffff8(%ebp),%eax ; __pfn

 8048490:  and    $0x1,%eax             ; __test last 2 bits, 判斷是否為虛拟函數

 8048493:  test   %al,%al

 8048495:  je     80484b6 <_Z8test_funP4BaseMS_FiiE+0x3e> ;不是虛函數就跳到 non-virtual fun處

  ; virtual fun,是虛拟函數,計算函數位址

 8048497:  mov    0xfffffffc(%ebp),%eax ;__delta

 804849a:  mov    0x8(%ebp),%ecx  ;get pb, 入參

 804849d:  add    %eax,%ecx       ;ecx = this=pb+__delta

 804849f:  mov    0xfffffff8(%ebp),%eax ;eax=__pfn

 80484a2:  shr    $0x2,%eax             ;eax=__pfn>>2 (fun index)

 80484a5:  lea    0x0(,%eax,4),%edx     ;edx=eax * 4

 80484ac:  mov    (%ecx),%eax           ;eax=vtble

 80484ae:  mov    (%eax,%edx,1),%edx    ;edx為函數位址

 80484b1:  mov    %edx,0xfffffff4(%ebp)   ;存起來

 80484b4:  jmp    80484bc <_Z8test_funP4BaseMS_FiiE+0x44>

 ; non-virtual fun,不是虛拟函數,直接取出函數位址

 80484b6:  mov    0xfffffff8(%ebp),%eax ;__pfn, fun addr

 80484b9:  mov    %eax,0xfffffff4(%ebp) ;__pfn, fun addr

                ; common invoking

                ; 0xfffffff4(%ebp) contains fun address

 80484bc:  push   $0x4                 ;push parameters

 80484be:  mov    0xfffffffc(%ebp),%eax   ; delta

 80484c1:  add    0x8(%ebp),%eax        ; this = pb+delta, this指針調整

 80484c4:  push   %eax                  ; this

 80484c5:  call   *0xfffffff4(%ebp)     ;invoke

 80484c8:  add    $0x10,%esp

 80484cb:  leave 

 80484cc:  ret   

 80484cd:  nop     

5 語言限制與陷阱

       按照C++語言的規定,對于成員函數指針的使用,有如下限制:

       不允許繼承類的成員函數指針指派給基類成員函數指針。

       如果我們一定要反其道而行,則存在this指針調整的陷阱,需要注意。這一節我們通過兩個例子,說明為什麼這樣操作是危險的。

5.1 例子

先看一個單繼承的例子。

class B {

public:

         B():m_b(0x13572468){}

      int b_fun(int) {  //A

                   std::cout<<'B'<<std::endl;

                   return 0;

         }

private:

         int m_b;

};

class D : public B {

public:

         D():m_d(0x24681357){}

         virtual int foo(int) {     // B

                   std::cout<<'D'<<std::endl; 

                   return 0;

         }

private:

         int m_d;

};

void test_consistent(B* pb, int (B::*pmf)(int))

{

         (pb->*pmf)(0x12345678);

}

void test_main(D *pd)

{

         typedef int (B::*B_PMF)(int);

         //test_consistent(pd, &D::foo);  error!

         test_consistent(pd, static_cast<B_PMF>(&D::foo));

     // crash in MSVC

}

int main()

{

         D d;

         test_main(&d);

         return 0;

}

這句話在Microsoft Visual C++6.0下面一運作就crash。 表面上看我們傳的指針是D的指針,函數也是D的函數。但實際上不是那麼簡單。函數調用的時候,pd指派給pb,編譯器會進行this指針調整,pb指向pd内部B的子對象。這樣到了test_consistent函數内部的時候,就是用D::B對象調用D::foo函數,this指針不對,是以就crash了。

       上面這個問題,GCC能正确的進行處理。其實錯誤的原因不在于pb=pd指針指派的時候,編譯器将指針進行了調整,而在于在test_consistent内,成員函數指針被調用的時候,應該将this指針再調整回去!這個問題又是由static_cast的行為不适當引起的。

       static_cast<B_PMF>(&D::foo)

這裡的static_cast, 是将D的成員函數指針強制轉換為給B的成員函數指針。因為它是D的函數,雖然會經由B的指針或者對象調用,但是調用時this指針應該根據B的位址調整成D的首位址。是以經過static_cast之後,這個成員函數指針應該為{__pfn,  __delta= -4 }。(B被包含在D内部,是以這裡是-4!) GCC正确的執行了這個cast,并且每次使用成員函數指針調用時都進行this指針調整, 是以沒有問題。可是Microsoft的實作在這個地方卻無能為力,為什麼呢?就算static_cast正确,在test_consistent裡面根本就不會進行this指針調整! 因為它使用的其實是struct{void *vcall_address;}這個結構,根本不知道要進行this指針調整。

Microsoft在這裡要做的是将一個struct pmf_type2類型的對象,通過static_cast轉換成一個struct pmf_type1的對象。這種轉換根本不能成功,因為struct pmf_type1要少一個成員delta.這樣的轉換會丢失資訊。

當然我們不能怪Microsoft,C++語言本來就規定了不能這樣用。不過Microsoft可以做得更好一點,至少可以不允許這樣的static_cast。(這樣的用法, VC2005能夠給出一個告警, 提示有可能産生不正确的代碼!)

我們可以很簡單的解決這個問題,在上面的代碼中A處,把注釋掉的virtual打開,也可以把B處的virtual注釋掉,使得所有地方都無需進行this調整,問題也就不再出現了。

這個例子可能有些牽強,我們把上面的代碼稍做修改,再舉一個涉及到多繼承的例子。

class B {

public:

         B():m_b(0x13572468){}

         virtual int b_fun(int)   {

                   std::cout<<"B "<<std::hex<<m_b<<std::endl;

                   return 0;

         }

private:

         int m_b;

};

class B2 {

public:

         B2():m_b2(0x24681357){}

         int b2_fun(int)   {

                   std::cout<<"B2 "<<std::hex<<m_b2<<std::endl;

                   return 0;

         }

private:

         int m_b2;

};

class D :public B , public B2

{

public:

         D():m_d(0x24681357){}

         int foo(int)

         {

                   std::cout<<"D "<<std::hex<<m_d<<std::endl; 

                   return 0;

         }

private:

         int m_d;

};

void test_consistent(B* pb, int (B::*pmf)(int))

{

         (pb->*pmf)(0x12345678);

}

void test_main(D *pd)

{

         typedef int (B::*B_PMF)(int);

         //test_consistent(pd, &B2::b2_fun);                    //A

         //test_consistent(pd, static_cast<B_PMF>(&B2::b2_fun));  // B

         typedef int (D::*D_PMF)(int);                        // C

         D_PMF pmf = &B2::b2_fun;               // D

         test_consistent(pd, static_cast<B_PMF>(pmf)); // E 結果錯誤!

}

int main()

{

         D d;

         test_main(&d);

         return 0;

}

先用Microsoft Visual C++進行測試。這段代碼執行結果是錯誤的。(沒有crash,比crash更糟)。先看注釋A處,文法錯誤,VC給出了正确的編譯錯誤。

B處,進行static_cast, VC也能給出正确的編譯錯誤,說int (B2::*)(int)類型不能轉換成int (B::*)(int)類型。這也很好。

這樣都不行,我們就繞一下,來個“智取”。先将int (B2::*)(int)轉換為int (D::*)(int)。這個轉換是C++标志規定必須實作的,屬于基類成員函數指針指派給繼承類成員函數指針。然後再進一步使用static_cast轉換成int (B::*)(int)類型。編譯錯誤沒有了。可是執行結果不正确!原因和上一個例子一樣,this指針不能正确的進行調整。這裡D類是需要進行this指針調整的,而B類,B2類都不需要調整,在test_consistent中調用函數指針的時候,不會進行this指針調整,是以出現了錯誤。

這個例子,GCC表現也相當好。這都歸根于GCC采用一緻的成員函數指針的表示和實作!

在Microsoft新釋出的Visual C++2005中, 上面的問題仍然存在。(再重複一下, 這不怪Microsoft, C++标準本來就不允許這樣用。)

5.2 static_cast幹了些什麼

       GCC裡面,不同類型的成員函數指針使用static_cast進行轉換,就是計算出合适的__delta值。

       VC裡面,使用static_cast進行轉換,做了什麼?

5.3 預設的轉換

       C++規定編譯器必須提供一個從基類成員函數指針到繼承類成員函數指針的預設轉換。這個轉換,最關鍵的地方,其實也是this指針調整。

5.4 教訓

從上面的例子,我們得到如下教訓:

1)  static_cast不能随便用。

2)  一般情況下不要将繼承類的成員函數指針指派給基類成員函數指針。不同編譯器可能有不同的表現。這可能導緻潛在的可移植性問題。

5.5 如何避開陷阱

現在我們明白了将C++運作時多态特性和C++成員函數指針合起來使用的時候,可能有些不夠自然的地方,而且存在上面所描述的陷阱。這些陷阱都是因為this指針調整引起的。是以要避開這個陷阱,就要避開this指針調整,是以需要注意:

1)  不要使用static_cast将繼承類的成員函數指針指派給基類成員函數指針,如果一定要使用,首先确定沒有問題。(這條可能會限制代碼的可擴充性。)

2)  如果一定要使用static_cast, 注意不要使用多繼承。

3)  如果一定要使用多繼承的話,不要把一個基類的成員函數指針指派給另一個基類的函數指針。

4)  單繼承要麼全部不使用虛函數,要麼全部使用虛函數。不要使用非虛基類,卻讓子類包含虛函數。

最後,用Herb Sutter的話結個尾(如果我沒記錯的話):do what you know,and know what you do!

參考書目

1)    Modern C++ design, Andrei Alexandrescu

2)    Inside the C++ Object model, Stanley B. lippman

3)    C++ Common Knowledge: Essential Intermediate Programming, Stephen C. Dewhurst

4)    The C++ Programming Language (special edition), Bjarne Stroustrup,