深度探索C++對象之四 --- Function語意學

深度探索C++對象模型 — Function語意學

C++支持三種類型的member functions: static、nonstatic和virtual。下面我們就來介紹下這三種member functions的調用方式。

nonstatic member function

nonstatic member function和nonmember function有相同的效率,其實編譯器內部也是把member function轉化爲nomember function的。
1. 首先修改函數原型,通過安插一個this指針,使class objec可以調用該函數
2. 然後通過該this指針來存取data member
3. 將member function重新寫成一個外部函數,對函數名稱進行mangling處理,之後在調用函數的地方也需要進行適當的修改

Name Mangling

編譯器爲了能夠保證data member或者member function都具有獨一無二的命名,對於每一個member都會做名稱的特殊處理(Name Mangling),比如

  1. 對於data member,通過加上類名

    class Bar {public: int ival;}
    class Foo:public Bar {public: int ival;};
    
    那麼在內部就會改寫成
    class Foo {
    public:
    int ival_3Bar;
    int ival_3Foo;
    }

    通過這種方式可以絕對清楚的指出到底處理的是哪一個ival。而funciton是有重載機制的,那麼就更加需要name mangling方法。

  2. 對於重載化的函數我們可以通過加入它的參數鏈表來進行名稱處理

class Point {
public:
    void x_5Point(float newX);
    float x_5Point();
}

在內部會改寫成

class Point {
public:
    void x_5PointFf(float newX);
    float x_5PointFv();
}

從上面可以看出通過將函數的參數也添加到名稱處理裏面去,形成獨一無二的函數名稱,能夠解決因爲函數重載導致的函數重名。另外對於不正確的調用,那麼在鏈接期間就會無法決議而失敗,這也就是“確保類型安全的鏈接行爲”,不過這種行爲只能捕捉函數標記,如果返回類型聲明錯誤,那麼是無法捕捉到的。

void print(const &Point3d) {}

這樣子聲明和調用就會鏈接出錯
void print(const Point3d) {}

但是如果這樣子聲明和調用是檢測不到的 
int print(const &Point3d) {}

這也就是爲什麼函數重載無法通過區分函數返回值就行判斷的。

static member function

static member function的主要特性就是它沒有this指針,以下的次要特性都是根源於主要特性:

  1. 首先static member function不能聲明爲const、volatile和virtual類型的
  2. 而且static member function獨立於class object存在,因此不能讀寫nonstatic data member。
  3. 如果取一個static member function的地址的話,得到的就是其在內存中的地址,而不是一個指向“class member function”的指針。
&Point::count()

得到的就是
unsigned int(*)()

而不是
unsigned int(Point::*)()

很久之前c++還沒有引入static member function的時候,對於這種不存取nonstatic data member的函數都是如下實現:

((Point3d*) 0)->count();

通過類型轉換成一個虛擬的class object,然後再調用count函數。
現在引入了static member function則是轉換成直接調用:

Point3d::count()

static member fuction因爲它的特性帶來了一個意想不到的好處,即static member function可以成爲一個callback函數

virtual member function

如果是調用一個virtual member function的話,

ptr->normalize()

那麼內部會被轉化成爲
(*ptr->vptr[1])(ptr)

我們之前提過,如果class含有虛函數的話,對於每個class object編譯器都會產生一個vptr指向class的vtable,然後通過索引值來找到調用的函數,這裏要注意後面括號裏的ptr參數傳入,這個即表示this指針的,表示實際要操作的class object。

另外通過對象來調用virtual function的原理並不相同

Point3d obj;
obj.normalize()

這裏通過class object來調用virtual function,只可能調用Point3d::normalize(), 那麼這類就不需要再使用vptr進行函數的確認,低效率!。直接調用實體的即可。因此“經由一個class object調用一個virtual function”,這種操作應該總是被編譯器像對待一般的nonstatic member function一樣加以決議。另外這樣子做的話,那麼virtual function的inline函數實體可以被擴展開來,提供極大的效率。

爲了支持多態的virtual function機制,在執行期間我們需要有“執行期類型判斷”,以此來確定point、reference指向的對象的實際類型,然後找到調用的函數的適當實體。那麼這些目標是如何實現的呢?

首先對於什麼樣子的class 需要保存一些類型信息給執行期使用。通過前面的學習我們可以知道如果一個class含有virtual function就需要這份額外的執行期信息。那麼什麼樣子的額外信息需要我們保存呢?

在執行期間,爲了能夠確定virtual function,我們需要知道對象的真實類型,其次我們還需要知道函數的實體位置。

單一繼承

爲了解決上面的問題,我們在virtual table第一個slot插入type info,之後對於每個class object都會安插一個vptr指向virtual table。該virtual table中從slot 1開始都是登記有virtual function的實體位置。編譯時期,編譯器能夠知道每個function的地址。在複雜的派生體系中,編譯器需要爲每一個類的vtable改寫合適的virtual function位置,大體有如下三種

  1. 通過從base class繼承而來的virtual function,derived class決定不改寫該function的話就直接使用base virtual function的函數實體
  2. 如果derived class決定改寫繼承而來的virtual funtion,那麼就會填上自己的函數實體
  3. 對於derived class新增的virtual function,編譯器需要增加vtable的slot,填上新增的virtual function函數實體。

如下圖所示:

單一繼承中每一個class都只有一個vitual table。每一個virtual function的索引值都是確定的,那麼編譯器直接通過索引值改寫virtual function的調用方式即可。

多重繼承

而在多重繼承中,每一個class object就不一定只有一個vptr了,其複雜度主要是在第二個及後繼的base class身上,以及“必須在執行期調整this指針”。

在多重繼承之下,derived class內涵n-1個額外的virtual table

我們可以查看如下圖所示的繼承關係

在上述圖中,Base1 subobject 和derived object的地址是一樣子的,編譯器在編譯期間改寫Base1 vpt的virtual function實體。唯一複雜的是Base2 subobject,如果通過Base1或者derived object來使用Base2,那麼編譯器需要修改this指針的offset,通過增加sizeof(Base1)來更新this指針以便正確的訪問到Base2 subobject,與此相同的如果使用的是Base2指針來訪問Base1或者derived,那麼這時候就需要減少sizeof(Base2)來修改offset。

如果是不需要計算offset的virtual function的直接就是在slot中找到函數實體,而如果是需要計算offset的,那麼就通過thunk技術來確定真正的virtual funtion函數實體位置。

thunk技術即通過一小段assembly代碼來跳轉到指定的位置執行

虛擬繼承

單一繼承中derived class中也只是有一個vptr指向vtable,但是在虛擬繼承中derived class有兩個vptr,一個是virtual base class自己的vptr指向base class的vtable;另外一個是自己的vptr,編譯時期需要改寫vptr以及vtable中的virtual function函數實體。具體的佈局如下圖所示:

單一繼承中base class和derived class object的起始地址是一樣子的,但是虛擬繼承中的virtual base class subobject和derived class object的地址已經不相同了,那麼在使用的時候編譯器就需要調整this指針的offset以指向正確的virtual base class subobject。

指向member function的指針

之前我們提到過取一個nonstatic data member的地址,得到的是該member在class 佈局中的offset+1,這個offset必須要結合特定的object對象地址纔有效。

而如果我們取一個nonstatic member function的地址的話,如果該函數是nonvirtual的話那麼得到的是它在內存中的真實地址,而這個地址也是需要綁定到某個class object的地址上才能夠調用。因爲nonstatic member function中需要存取nonstatic data member,而這需要通過this指針傳遞。

一個member function的指針可以如下聲明:

double (Point::* pmf)();

初始化的話可以:
double (Point::*coord)() = &Point::x;
或者
coord = &Point::y;

使用的話:
(origin.*coord)();
(ptr->*coord)();

static member functions的類型是函數指針,而不是“指向member function”的指針。

指向virtual member function的指針

對於member function和virtual member function分別取地址的話,得到的數值是不一樣的,member function取地址得到的是在內存中的實際地址,這個綁定到特定的class object的地址就可以調用。而取virtual member function的地址的話得到是它在vtable中的索引值。這兩種地址都是可以賦值給“指向member function之指針的”,那麼編譯器就需要有辦法區分當前指針是代表的實際內存地址還是vtable中的索引值?

(((int)pmf) & ~127) ? (*pmf)(ptr):
        (*ptr->vptr[(int)pmf](ptr);

這種實現技巧必須假設繼承體系中最多隻有128個virtual functions

多重繼承下,指向member function的指針

多重繼承情況下,有另外一種實現方式,通過提供如下一個結構體:

struct _mptr {
    int delta;
    int index;
    union {
        ptrtofunc faddr;
        int v_offset;
    };
};

index和faddr分別表示帶有virtual table索引值和nonvirtual member function地址(當index不指向virtual table時,設置爲-1)。
delta字段表示this指針的offset值,而v_offset字段放的是virtual base class的vptr位置。

Inline function

對於inline function,設計程序的時候沒有強制性的將任何函數都聲明爲inline類型的,編譯器會計算一些操作的次數來判斷當前函數是否需要inline function。

而對於inline function的形式參數,每一個形式參數都會被實際參數所取代,爲了防止“會帶來副作用的實際參數”,通常會有如下幾種情況:

  1. 如果是一個常量表達式,那麼直接用常量替換
  2. 如果是一個“帶副作用的實際參數”,那麼就需要引入臨時性對象
  3. 如果不是上述的情況那麼直接替換之

另外inline function如果有局部變量的話,爲了使得名稱唯一也是需要引入臨時性變量的,這會使得程序的體積變大。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章