筆記:深度探索C++物件模型 第四章

Function
語意學 ( The Semantics of Function )

摘要:

nonstatic member function

virtual member function

static member function

virtual member function(單一繼承)

多重繼承

函式的效能

Point-to-member functions

inline function

 

nonstatic member function

nonstatic member function 實際上會被內化為 nonmember 的形式,步驟如下:

  1. 改寫函式的 signature 以安插一個額外的參數,用以提供一個存取管道,使 class object 得以將此函式喚起.額外參數就是 this.
  2. 將對 "nonstatic data member 的存取動作" 改經由 this 指標存取.
  3. 改寫成一個外部函式,將函式名稱經過 "mangling" 處理,以成為一個獨一無二的語彙.

名稱的特殊處理 (Name mangling)

一般而言, member 的名稱後面會被加上 class 名稱. 若你宣告 extern "C", 就會壓抑 nonmember
functions 的 "mangling" 效果. 此 mangling 的動作, 各家 compiler 實作方式不同.

 

virtual member functions

若 ptr->normalize(); 

則會被轉化為

(*ptr->vptr[1])(ptr);

其中:

  • vptr 表示由 compiler 產生的指標,指向 virtual table, 其名稱也會被 "mangled", 因為在一個複雜的衍生體系中,可能存在多個
    vptrs.
  • 1 是 virtual tabe slot 的索引值.
  • 第二個 ptr 表示 this 指標.

若宣告為 inline, 則會被 compiler 當作一般 nonstatic member function 一樣地決議,提供極大的效率利益.

 

static member function

一般建議把 static data member 宣告為 nonpublic, 並提供一個或多個 member functions 來存取之.

主要特性: 沒有 this 指標.

次要特性: 他不能直接存取其 class 中的 nonstatic members; 他不能被宣告為 const, volatile, 或 virtual;
他不需要經由 class object 才被喚起.

如果取一個 static member function 的位址, 將獲得其在記憶體中的位置,其位址的型別並不是一個 "指向 class member
function 的指標", 而是一個 "nonmember 函式指標".即

&Point3D::object_count();

會得到

 unsigned int (*)();

而非

 unsigned int (Point3D:*)();

差不多等同於 nonmember function.

p.s. object_count 原型宣告為

unsigned int
Point3D::
object_count() {
return _object_count;
}

 

virtual member function(單一繼承)

單一繼承一般是在每個多型的 class object 身上增加 2 個 member:

  1. 一個字串或數字,表示 class 型別.
  2. 一個指標指向某表格,表格中持有程式的 virtual functions 的執行時期位址.為了找到函式位址,每個 virtual function
    被指派一個表格索引值.

這些工作都由 compiler 完成. 執行時期要做的只是在特定的 virtual table slot 中啟動 virtual function.

圖解. 若 Point3D 繼承 Point2D 繼承 Point, 那麼個別的 virtual table 就可能是

於是當

Point *ptr;
ptr=new Point3D();
ptr->z();

compiler 可以把該呼叫轉化為

(*ptr->vptr[4])(ptr);

 

多重繼承

在多重繼承中支援 virtual function,其複雜度圍繞在第二個及後繼的 base class 上,以及"必須在執行時期調整 this
指標"上.

即後繼的 class 會有多個 virtual table.

將後繼的物件位址指定給一個 base1 指標或 base2 指標時, virtual table 就要視指標的型態作切換,以免呼叫到錯誤的函數.

效率若依照原始 c++ 模型,會變的不好,但這方面各家 compiler 會利用 thunk 或 address points 策略來改善.

虛擬繼承下的 virtual functions

實作上,同樣要調整 this 指標,很複雜,效率也不一定較好,建議不要在一個 virtual base class 中宣告 nonstatic data
members.

 

函式的效能

inline > (nonmember friend=static member=nonstatic member) > virtual
member > virtual member(多重繼承) > virtual member(虛擬繼承)

virtual member 在層數越多的狀況下,其執行時間也成正比增加.

 

Point-to-Member functions

double (Point::*pmf)();
double (Point::*coord)()=&Point::x; //初始
coord=&Point::y; //或是這樣初始

於是可以

(origin.*coord)();

(ptr->*coord)();

這樣用.

實際上則會轉化為

(coord)(&origin);

(coord)(ptr);

支援"指向 virtual member functions"的指標

考慮如下片段(假設 z 為 virtual function)

float (Point::*pmf)()=&Point::z;
Point *ptr=new Point3D;
ptr->z(); //ok
(ptr->*pmf)(); //仍然 ok

compiler 實作上,必須定義 pmf, 使他能持有兩種數值,並且其數值能區分其意義.

多重繼承的狀況:

stroustrup 利用 union 來處理

struct __mptr {
int delta;
int index; //處理 virtual table 索引,不指時為 -1
union {
ptrfunc faddr;
int v_offset; //處理 nonvirtual member function
};
};

於是

(ptr->*pmf)();

會變成

(pmf.index<0)?
(*pmf.faddr)(ptr):
(*ptr->vptr[pmf.index](ptr));

Microsoft 以 vcall thunk 來作檢查,避免浪費檢查的時間,但副作用是,當傳遞一個不變值的指標給 member function 時,需要產生暫時性的物件.

效率:同樣地,不牽涉到"虛擬"+"多重"情況的,效率較佳.

 

inline function

一般處理時,有兩個階段:

  1. 分析函式,若因某些問題(複雜度過高,建構問題…等)被判斷不可 inline, 則會轉為 static 函式,並在被編譯模組中產生對應的函式定義.
  2. 真正的 inline function 擴展動作,是在呼叫的那一點上,這會帶來參數的求值動作及暫時性物件的管理.

通常需進入 assembler 中,才能得知是否真實現了 inline

形式參數擴展的情況大致如下:

inline int min(int i, int j) {
return i<j?i:j;
}
int
main() {
int minval;
int val1=1024,val2=2048;
minval=min(val1,val2);		//minval=val1<val2?val1:val2;
minval=min(1024,2048);		//minval=1024;
minval=min(foo(),bar()+1);	//int t1,t2;
//minval=(t1=foo()),(t2=bar()+1),t1<t2?t1:t2;

 

區域變數的情況

inline int
min(int i, int j) {
int minval=i<j?i:j;
return minval;
}
int local_var;
int minval;
...
minval=min(val1,val2);

則可能會代換為

int local_var;
int minval;
int __min_lv_minval;
minval=(__min_lv_minval=val1<val2?val1:val2),__min_ln_minval;

inline 函式中的區域變數再加上有副作用的參數,可能會導致大量暫時性物件的產生.並且使得程式大小暴增.避免過於複雜的 inline 函式,以免 compiler
無法擴展開來.