關於《深度探索C++對象模型》有一段話看不懂?

煩請看過並能看懂《深度探索C++對象模型》的聰明朋友來看一下,先看一張圖片【書中第223頁】:

其中我看不懂的是,為什麼對於類構造函數來說,可以通過傳遞一個多餘的參數來判定要不要調用基類的構造函數【這樣可以避免基類的重複構造】,但對於類賦值函數【operator=】來說,就不能通過這種方式避免給基類重複賦值呢?跟「取函數地址操作合法與否」有什麼關係呢?


上圖是這章書中各類的繼承關係。

書的 223 頁是在 5.3 節,是分析在 virtual 繼承時,複製函數(也可以叫賦值函數 operation =)應該如何處理。之前的 5.2 節是分析在 virtual 繼承時,構造函數應該如何處理。

按順序,應該先理解好 5.2 節的對象構造,再來理解 5.3 節的對象複製。這一系列看似複雜的問題,都是因為有 virtual 繼承。假如沒有這個特性,對象構造和複製就會簡單得多。為什麼需要 virtual 繼承呢?見圖中,Vertext3d 同時繼承 Point3d 和 Vertex,這樣就間接繼承了 Point 兩次,這種繼承關係就是菱形繼承。假如按照普通的繼承方式,Point 的數據就在 Vertex3d 出現兩次。而使用 virtual 繼承,Point 的數據只會在 Vertex3d 中出現一次。

-------------------------------------------

5.2 節,對象構造

virtual 繼承的語意是讓 Point 在 Vertex3d 中只出現一次。既然如此,Vertex3d 的構造函數就只能調用 Point 的構造函數一次。原則上,Vertex3d 的複製函數(operation =) 也應該只調用 Point 的複製函數一次。

在沒有 virtual 繼承的時候,Vertext3d、Point3d、Vertex 的構造函數大致這樣實現(為簡化描述,省略掉 this 指針,原書中代碼包含 this 指針):

Vertext3d::Vertex3d() {
Point3d::Point3d();
Vertex::Vertex();
// 接下來配置虛函數表,其它數據賦值
}

Point3d::Point3d() {
Point::Point();
// 接下來配置虛函數表,其它數據賦值
}

Vertex::Vertex() {
Point::Point();
// 接下來配置虛函數表,其它數據賦值
}

這樣做會間接調用了 Point 的構造函數兩次。但在 virtual 繼承下,這種方式行不通了,會違反了 C++ 的語意。

為正確實現 virtual 繼承,編譯器使用了一個小花招,將 Point::Point() 直接放在 Vertext3d::Vertex3d 中調用。變成:

Vertext3d::Vertex3d() {
Point::Point();
Point3d::Point3d();
Vertex::Vertex();
// 接下來配置虛函數表,其它數據賦值
}

Point3d::Point3d() {
// 接下來配置虛函數表,其它數據賦值
}

Vertex::Vertex() {
// 接下來配置虛函數表,其它數據賦值
}

這樣做的話,Vertex3d v3d; 這句代碼的語意就正確了。但 Point3d p3d; 就沒有調用 Point::Point(),語意錯誤。所以就為各個構造函數添加一個額外參數,變成:

Vertext3d::Vertex3d(bool __most_derived) {
if (__most_derived) {
Point::Point();
}
Point3d::Point3d(false);
Vertex::Vertex(false);
// 接下來配置虛函數表,其它數據賦值
}

Point3d::Point3d(bool __most_derived) {
if (__most_derived) {
Point::Point();
}
// 接下來配置虛函數表,其它數據賦值
}

Vertex::Vertex(bool __most_derived) {
if (__most_derived) {
Point::Point();
}
// 接下來配置虛函數表,其它數據賦值
}

Vertex3d v3d; 編譯成調用 Vertext3d::Vertex3d(true); 同樣 Point3d p3d; 也編譯成調用 Point3d:: Point3d(true)。這就是題目中說的:

對於類構造函數來說,可以通過傳遞一個多餘的參數來判定要不要調用基類的構造函數,這樣可以避免基類的重複構造。

另外 C++ 的一個設計原則是運行速度,一個特性盡量做到零消耗。添加了 __most_derived 額外參數後,構造函數就多了一個多餘的 if 判斷。書中就提到,有些編譯器編譯時,將構造函數分裂成兩個。一個調用 Point::Point, 另一個不調用 Point::Point(也可能不設置虛函數表),這樣運行時就少了一個 if 判斷。只是編譯出來的目標代碼會大一些。

-------------------------------------------

5.3 節,對象複製

上文提到,Point 只在 Vertex3d 中出現一次,因此原則上,Vertex3d 的複製函數(operation =) 也應該只調用 Point 的複製函數一次。沿用對象構造的思路,那是不是可以在複製函數中,添加一個額外的參數呢?假如可以,一切情況就跟對象構造相同了。

很不幸,編譯時不能添加額外參數。223 頁提到,需要支持成員函數指針。也就是書中原文:

「取 copy assignment operator 的地址" 應該是合法的。

什麼意思呢?比如:

class Point3d {
public:
Point3d operator = (const Point3d rhs); // 1
Point3d doSomething(const Point3d rhs); // 2
};

函數 1 是複製函數(或者叫賦值函數),函數 2 是普通函數。它們的指針類型是應該相同的,都為 Point3d (Point3d::*)(const Point3d), 因此下面語句應該是合法的。

typedef Point3d (Point3d::*pmfPoint3d)(const Point3d);
pmfPoint3d pmf = Point3d::operator=;
(x.*pmf)(x);

pmf = Point3d::doSomething
(x.*pmf)(x);

假如編譯器為 operator = 添加了額外的參數,operator = 的類型,就跟 doSomething 不相同了。上述代碼就編譯不過了。

那為什麼 operator = 支持成員函數指針,而構造函數不需要支持呢?因為構造函數沒有被調用時,對象根本就不存在。C++ 標準規定,不能取構造函數和構釋函數的地址。也就沒有指向構造函數的指針。

因此對象複製函數不能添加額外參數。那應該怎麼做呢?

  1. 可以將對象的 operator =,在編譯時分裂成兩個。這樣符合語意,可以只調用 Point::Point 一次,速度也快。但是編譯出來的代碼體積變大。

  2. 放寬要求。雖然原則上只應該調用基類的複製函數一次。但就算調用多幾次,也沒有什麼大問題。

在 224 頁,方框中就有這段話。

我們並沒有規定那些代表 virtual base class 的 subobjects 是否該被「隱含定義(implicitly defined)的 copy assignment operator「 指派(賦值,assign)內容一次以上。(C++ Standard, Section 12.8)

-------------------------------------------

題外話

對象構造和對象複製,有很多類似的地方,但也需要注意它們的區別。《深度探索 C++ 對象模型》很多章節,都是先講述對象構造怎麼怎麼樣,再講述對象複製時怎麼樣。平時寫代碼也是,對象構造、複製、釋放,都需要特別注意。另外這本書有些術語似乎跟大陸的其術語不太一致,看的時候注意。比如書中常說的對象複製、對象拷貝,經常是指賦值。而我們大陸這邊,說對象複製,通常聯想到複製構造函數。英文就沒有歧義,一個是 copy assignment operator, 一個是 copy constructor。

C++ 不斷為舊特性打補丁,virtual 繼承這特性已入歧途。Point 的數據會重複是因為使用了多重繼承,假如一開始就只有單繼承,並添加介面,就沒有之後的一系列問題。這些看似複雜的問題,是自己弄出來的。《深度探索 C++ 對象模型》這本書中,一旦涉及到 virtual 繼承,編譯器實現時就會有很多小花招,那些章節看起來也比較難懂。實際工程中不應該使用多繼承,而只使用「單繼承 + 介面」,介面概念在 C++ 中實現為沒有數據的純虛類。這樣關於 virtual 一系列的章節,實際工程中沒有那麼重要。


說白了這個東西就是,你自己寫的構造函數,有辦法通過多寫一個參數的重載來決定怎麼構造,而operator=沒法多寫一個參數所以不行

struct dont_initialize_base_class {};

struct vbase
{
int _a;
vbase(int a) :_a(a) {}
};

struct d1 : virtual vbase
{
int b;
d1(int a) : vbase(a), b(a) {}
d1(int a, dont_initialize_base_class): b(a){}
};

struct d2 : virtual vbase
{
int c;
d2(int a) : vbase(a), c(a) {}
d2(int a, dont_initialize_base_class): c(a){}
};

struct derived : d1, d2
{
derived(int a) : d1(a), d2(a, dont_initialize_base_class{}) {}
};

就是這個意思


你提到的圖呢?


看完這本我認為比較糟糕的書後,有如下兩點體會:

1.多態是用Virtual Function Table實現的

2.怎麼實現的以及其它問題都依賴於具體實現,我哪知道...


推薦閱讀:

輪子哥可以分享一下曾經是怎樣帶學生的嗎?
cout 和 cin 的底層實現是怎樣的?
C++中if(a!=b)和if(a^b)哪個效率更高?
如何設計一個真正高性能的spin_lock?
使用C++的痛苦來自哪裡?

TAG:C | 編譯器 | 深度探索C對象模型書籍 |