Function
語意學 ( The Semantics of Function )
摘要:
nonstatic member function
nonstatic member function 實際上會被內化為 nonmember 的形式,步驟如下:
- 改寫函式的 signature 以安插一個額外的參數,用以提供一個存取管道,使 class object 得以將此函式喚起.額外參數就是 this.
- 將對 "nonstatic data member 的存取動作" 改經由 this 指標存取.
- 改寫成一個外部函式,將函式名稱經過 "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:
- 一個字串或數字,表示 class 型別.
- 一個指標指向某表格,表格中持有程式的 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
一般處理時,有兩個階段:
- 分析函式,若因某些問題(複雜度過高,建構問題…等)被判斷不可 inline, 則會轉為 static 函式,並在被編譯模組中產生對應的函式定義.
- 真正的 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
無法擴展開來.