六、Inheritance and Object-Oriented Design
在 C++ 中,「inheritence」可以是 single inheritance 或 multiple inheritance,而每個 inheritance link 可以是 public, protected 或是 private,也可以是 virtual 或 non-virtual。
接著是成員函式的各個選項:virtual? non-virtual? pure virtual?
預設參數值與 virtual function 有什麼交互影響?繼承如何影響 C++ 的名稱搜尋規則?設計選項有哪些?如果 class 的行為需要修改,virtual function 是最佳選擇嗎?

32、Make sure public inheritance models "is-a."
如果令 class D 以 public 形式 inherit class B,這便是告訴 C++ compiler 說:
每個型別為 D 的物件同時也是一個型別為 B 的物件,而反之並不成立。
也就是說「B 物件可派上用場的任保地方,D 物件一樣可以派上用場」,因為每一個 D 物件就是一種 B 物件。

C++ 對於「public inheritance」嚴格奉行上述見解,考慮以下例子:
class Person { ... };
class Student : public Person { ... };

void eat(const Person& p);
void study(const Student& s);

Person P;
Student s;

eat(p);  // 正確
eat(s);  // 正確
study(p);  // 錯誤
study(s);  // 正確

在 C++ 領域中,任何 function 如果期望獲得一個型別為 Person (或 Pointer-to-Person 或 reference-to-Person) 的引數,都也願意接受一個 Student (或 Pointer-to-Student 或 reference-to-Student) 物件。
這個論點只對 public inheritance 才成立,private inheritance 的意義與此完全不同,詳見條款 39,至於 protected inheritance ,這至今仍令人困惑。

public inheritance 和 is-a 之間的等價關係聽起來或許簡單,考慮 Penguin 與 Bird 的例子:
class Bird {
  public:
    virtual void fly();  // 鳥可以飛
    ...
};

class Penguin : public Bird {  // 企鵝是一種鳥
    ...
};

以這個 inheritance 來說,企鵝卻可以飛?
謹慎一些思考,便會認清一個事實,並不是所有的鳥都是可以飛的:
class Bird {
  public:
    ...
};

class FlyingBird : Bird {
  public:
    virtual void fly();  // 飛鳥可以飛
    ...
};

class Penguin : public Bird {  // 企鵝是一種鳥
    ...
};

或是另一派的處理方式:所有的鳥都會飛,企鵝也是鳥,但企鵝就是不會飛。
為 Penguin 重新定義 fly() function,令產生一個執行期錯誤:
class Penguin : public Bird {
  public:
    virtual void fly() { error("Attempt to make a penguin fly!"); }
    ...
};

條款 18 說過:好的介面可以防止無效的程式碼通過編譯,因此應該寧可採取「在編譯期就拒絕讓企鵝飛行」的設計:
class Bird {
  public:
    ...  // 不宣告 fly() function
};

class Penguin : public Bird {
    ...  // 不宣告 fly() function
};

類似的直覺誤導,可能也會發生在其它情況,如幾何:
正方形是一個矩形,所以 class Square 應該以 public inheritance 形式繼承 class Rectangle?

這個直覺反應,將會在實作時,產生困擾,舉個例子來說: Rectangle 可以任改變長、寬,而 Square inherit Rectangle ,所以也可以呼叫 Rectangle 的 function 而改變長寬,但呼叫之後,長寬改變,就不再是個合法的 Square。

public inheritance 主張能夠施行於 base class 物件身上的每件事情,也可施行在 derived class 物件身上,所以以 class Square 來 inherit class Rectangle 的關係顯然並不正確,雖然 compile 會過。

is-a 並非是唯一存在於 class 之間的關係,另外兩個常見的關係是 has-a 和 is-implemented-in-terms-of ,將在條款 38 39 討論。


「public inheritance」意味 "is-a",適用於 base class 身上的每件事情,也一定適用於 derived class 身上。



33、Avoid hiding inherited names.
在一般程式碼中:
int x;  // global variable
void someFunc() {
    double x;  // local variable
    std::cin >> x;
}

這個讀取數據的述敘句指的是 local variable x,而不是 global variable x,因為內層作用域的名稱會遮蔽外圍作用域的名稱。
當 compiler 處於 someFunc 作用域並遭遇名稱 x 時,會先在 local 作用域搜尋是否有什麼東西帶著這個名稱,如果找不到,就再找其它作用域,至於型別相不相同並不重要。

現在導入 inheritance 來看:
class Base {
  private:
    intx;
  public:
    virtual void mf1() = 0;
    virtual void mf1();
    void mf3();
    ...
};

class Derived : public Base {
  public:
    virtual void mf1();
    void mf4();
    ...
};

void Derived::mf4() {
    ...
    mf2();
    ...
}

當 compiler 在執行 mf4() function 時,看到名稱 mf2,必須估算它指涉(refer to)什麼東西,compiler 的作法是搜尋各個作用域:
首先搜尋的是 local 作用域,也就是 mf4() 函蓋的作用域,沒有找到任何名為 mf2 的東西,於是再搜尋其外圍作用域,也就是 class Derived 函蓋的作用域,還是沒找到,於是再往外圍移動,到 class Base 函蓋的作用域搜尋,在這 compiler 找到一個名稱 mf2 的東西了,於是停止搜尋。
但如果 class Base 內還是沒有 mf2,就會再找 Base 的 namespace 的作用域(如果有的話),最後搜尋的就是 global 作用域。

重新考慮以下例子:
class Base {
  private:
    intx;
  public:
    virtual void mf1() = 0;
    virtual void mf1(int);
    virtual void mf2();
    void mf3();
    void mf3(double);
    ...
};

class Derived : public Base {
  public:
    virtual void mf1();
    void mf3();
    void mf4();
    ...
};

void Derived::mf4() {
    ...
    mf2();
    ...
}

以作用域為基礎的「名稱遮掩規則」並沒有改變,所以 base class 內的所有名為 mf1, mf3 的 function 都被 derived class 內的 mf1 及 mf3 function 遮掩掉了,以名稱搜尋的觀點來看,Base::mf1() 及 Base::mf3() 不再被 Derived 繼承:
Derived d;
int x;
...
d.mf1();  // 沒問題
d.mf1(x);  // 錯誤,Derived::mf1 遮掩了 Base::mf1
d.mf2();  // 沒問題
d.mf3();  // 沒問題
d.mf3(x);  // 錯誤,Derived::mf3 遮掩了 Base::mf3

即使 base class 與 derived class 內的函式有不同的參數型別也都會遭到遮掩,也不論 function 是 virtual 或 nun-virtual 都一體適用。


這些行為背後的基本理由是為了防止在程式庫或應用框架(application framework)內建立新的 derived class 時,附帶地從疏遠的 base class 繼承 override function。
但一般來說,總是會想繼承 override function,而且事實上,如果使用 public inheritance 而又不 inherit 那些 override function,這是違反 base 和 derived class 之間的 is-a 關係,條款 32 有說明 is-a 是 public inheritance 的基石。
因為,總是會想要推摧 C++ 對「繼承而來的名稱」的預設遮掩行為:
class Base {
  private:
    intx;
  public:
    virtual void mf1() = 0;
    virtual void mf1(int);
    virtual void mf2();
    void mf3();
    void mf3(double);
    ...
};

class Derived : public Base {
  public:
    using Base::mf1;  // 讓 Base class 內的名為 mf1 及 mf3 
    using Base::mf3;  // 的所有東西在 Derived 作用域內都可見
    virtual void mf1();
    void mf3();
    void mf4();
    ...
};

Derived d;
int x;
...
d.mf1();  // 沒問題
d.mf1(x);  // 沒問題,可以呼叫 Base::mf1
d.mf2();  // 沒問題
d.mf3();  // 沒問題
d.mf3(x);  // 沒問題,可以呼叫 Base::mf3

如果繼承 base class 並加上 override function,而又希望重新定義或 override 其中一部分,就必須為那些原本會被遮掩的每個名稱引入一個 using 宣告式。

有時候,可能不想繼承 base class 的所有 function,這是可以理解的,但在 public inheritance 下,這是不可能發生的,因為它違反了 public inheritance 的 「base 和 derived class 之間的 is-a 關係」。
但在 private inheritance 下,這是有意義的,此時,using 宣告式派不上用場,因為它會令繼承而來的某給定名稱之所有同名 function 在 derived class 中都可見,這時,需要不同的技術,也就是 forwarding function:
class Base {
  private:
    intx;
  public:
    virtual void mf1() = 0;
    virtual void mf1(int);
    ...
};

class Derived : private Base {
  public:
    virtual void mf1() {  // forwarding function
        Base::mf1();  // 暗自轉為 inline function
    }
    ...
};

Derived d;
int x;
...
d.mf1();  // 沒問題
d.mf1(x);  // 錯誤,Base::mf1 被遮掩了

以上就是 inheritance 和名稱遮掩的完整故事,但如果 inheritance 結合 template,又將面對「繼承名稱被遮掩」的一個全然不同的型式,詳見條款 43


derived class 內的名稱會遮掩 base class 內的所有同名名稱。
而為了讓被遮掩的名稱可以被找到,可以使用 using 宣告式或 forwarding function。




34、Differentiate between inheritance of interface and inheritance of implementation.
表面上直接了當的 public inheritance 概念,其實是由兩個部分組成:
inheritance of function interface 及 inheritance of function implementation。
這兩種的差異,有些類似 Introduction 中所討論的 function 宣告和 function 定義之間的差異。

身為 class 的設計者,有時會希望 derived class 只繼承成員函式的介面,也就是宣告式;有時又會希望 derived class 同時繼承函式的介面和實作,但又希望可以 override 所繼承的實作;又有時會希望同時繼承函式的介面和實作,但不允許 override 任何東西。

class Shape {
  public:
    virtual void draw() const = 0;  // pure virtual function
    virtual void error(const std::string& msg);  // impure virtual function
    int object ID() const;  // non-virtual function
    ...
};

class Rectangle : public Shape { ... };
class Ellipse : public Shape { ... };

Shape 的 pure virtual function draw() 使它成為一個 abstract class,所以使用者無法產生 Shape class 的 instance,只能產生其 derived class 的 instance。

pure virtual function 有兩個最突出的特色:
  • 宣告一個 pure virtual function 的目的是為了讓 derived class 只繼承 function interface。
  • 可以為 pure virtual function 提供定義,但呼叫它的唯一途徑是「明確指出其 class 名稱」。

Shape* ps = new Shape();  // 錯誤!Shape 是 abstract
Shape* ps1 = new Rectangle();  // 正確
ps1->draw();  // 正確
Shape* ps2 = new Ellipse();  // 正確
ps2->draw();  // 正確
ps1->Shape::draw();  // 正確
ps2->Shape::draw();  // 正確

derived class 也會繼承 impure virtual function interface,但 impure virtual function 會提供一份實作碼,而 derived class 可能會 override 它:
  • 宣告 impure virtual function 的目的是讓 derived class 繼承該 function 的 interface 及 implementation。

但 impure virtual function 同時指定函式宣告和函式預設行為,卻可能造成危險,原因在於如果 derived class,忘記 override impure virtual function,則程式還是可以順利 compile、執行,直到 run time 錯誤才會被發現。
避免這個問題的作法是切斷「interface of virtual function」和其「預設實作」之間的連結:
class Airplane {
  public:
    virtual void fly(const Airport& destination) = 0;
    ...
  protected:
    void defaultfly(const Airport& destination) { ... };
};

class ModelA : public Airplane {
  public:
    virtual void fly(const Airport& destination) {
        defaultFly(destination);
    }
    ...
};

class ModelB : public Airplane {
  public:
    virtual void fly(const Airport& destination) {
        defaultFly(destination);
    }
    ...
};

class ModelC : public Airplane {
  public:
    virtual void fly(const Airport& destination) {
        ...  // C 型飛機需要指定 fly 的方式
    }
    ...
};

以不同的 function 分別提供 interface 及 default implementation,如上述的 fly() 及 defaultfly() ,可能因過度雷同的 function name,而引起 class 命名空間污染問題,因此,另一種作法是利用「pure virtual function 必須在 derived class 中重新宣告,但它們也可以擁有自己的實作」的特性:
class Airplane {
  public:
    virtual void fly(const Airport& destination) = 0;
    ...
};

void Airplane::fly(const Airport& destination) {
    ...
}

class ModelA : public Airplane {
  public:
    virtual void fly(const Airport& destination) {
        Airplane::fly(destination);
    }
    ...
};

class ModelB : public Airplane {
  public:
    virtual void fly(const Airport& destination) {
        Airplane::fly(destination);
    }
    ...
};

class ModelC : public Airplane {
  public:
    virtual void fly(const Airport& destination) {
        ...  // C 型飛機需要指定 fly 的方式
    }
    ...
};
  // 

這個例子幾乎和前一個一模一樣,但 pure virtual function Airplane::fly() 取代了獨立 function Airplane::defaultFly(),只是現在 fly() 被分割為兩個基本要素,其宣告行為表現的是 interface,而定義部分則表現出 default implementation。

如果 member function 是個 non-virtual function,意味著它並不打算在 derived class 中有不同的行為:
  • 宣告 non-virtual function 的目的是為了令 derived class inherit function 的 interface 及一份強制性實作。

non-virtual function 代表的意義是 invariant 凌駕 specification,所以它不該在 derived class 中被重新定義,這也是條款 36 所討論的一個重點。

pure virtual function, impure virtual function, non-virtual function 之間的差異,使得可以精確指定想要 derived class 從 base class inherit 的東西:
只 inherit interface、inherit interface and default implementation、inherit interface and mandatory implementation。


inheritance of interface 和 inheritance of implementation 是不同的。
inheritance of interface 和 inheritance of implementation 是不同的;在 public inheritance 中,derived class 總是 inherit base class 的 interface。
pure virtual functions 只具體指定 inheritance of interface。
simple(impure) virtual functions 具體指定 inheritance of interface plus inheritance of a default implementation。
non-virtual functions 具體指定 inheritance of interface plus inheritance of a mandatory implementation。




35、Consider alternatives to virtual functions.
設計一個人物,可能會因為被傷害或其它因素而降低健康狀況,提供 member function healthValue(),回傳一個整數值,表示人物的健康狀況:
class GameCharacter {
  public:
    virtual int healthValue() const;  // 傳回健康指數
}

將 healthValue 宣告為 virtual function 似乎是在明白不過的作法,但,是否還有其它作法:
  1. 藉由 Non-Virtual Interface 手法實現 Template Method 範式:以 public non-virtual member function 包裏較低存取性的 virtual function。
  2. 藉由 Function Pointer 實現 Strategy 範式:將 virtual function 替換為「函式指標成員變數」。
  3. 藉由 tr1::function 完成 Strategy 範式:以 tr1:function 成員變數替換 virtual function,進而允許使用任何 callable entry 搭配一個相容於需求的簽名式。
  4. 古典的 Strategy 範式:將繼承體系內的 virtual function 替換為另一個繼承體系內的 virtual function。
  5. ...

藉由 Non-Virtual Interface 手法實現 Template Method 範式
這個作法源於一個思想流派,這個流派主張 virtual function 應該幾乎總是 private,所以較好的設計是保留 healthValue() 為 public member function,但讓它為 non-virtual,並呼叫一個 private virtual function 來進行實際工作:
class GameCharacter {
  public:
    int healthValue() const {
        ...  // 可以做一些事前工作
        int retVal = doHealthValue();
        ...  // 可以做一些事後工作
        return retVal;
    }
  private:
    virtual int doHealthValue() const {
        ...
    }
}

這一基本設計,也就是「令使用者透過 public non-virtual member function 間接呼叫 private virtual function」,稱為 non-virtual interface(NVI) 手法,它是所謂 Template Method design pattern (與 C++ template 並無關聯)的一個獨特表現形式。


藉由 Function Pointer 實現 Strategy 範式
要求每個人物的 constructor 接受一個 pointer,指向健康計算 function:
class GameCharacter;  // forward declaration
int defaultHealthCalc(const GameCharacter& gc);
// default algorithm

class GameCharacter {
  public:
    typedef int (*HealthCalcFunc) (const GameCharacter&);
    explicit GameCharacter(HealthCalcFunc hcf = defaultHealthCalc)
            : healthFunc(hcf) {
    }
    int healthValue() const {
        return healthFunc(*this);
    }
  private:
    HealthCalcFunc healthFunc;
}

這是常見的 Strategy design pattern 的簡單應用,比起「植基於 GameCharacter 繼承體系內之 virtual function」,多了些彈性:
  • 同一人物類型之不同實體,可以有不同的健康計算 function。
  • 某已知人物之健康指數計算 function 可在執行期變更。

換句話說,「健康指數計算 function 不再是 GameCharacter 繼承體系內的 member function」,這一事實,意味著,這些 function 將無法存取物件內的非 public 成分。


藉由 tr1::function 完成 Strategy 範式
如果不再使用 function pointer,而是改用一個型別為 tr1::function 的物件,則如條款 54 所說,這樣的物件可持有任何 callable entry,也就是 function pointer, function object 及 member function pointer,只要其 signature 相容於需求端:
class GameCharacter;  // forward declaration
int defaultHealthCalc(const GameCharacter& gc);
// default algorithm

class GameCharacter {
  public:
    typedef std::tr1::function HealthCalcFunc;
    explicit GameCharacter(HealthCalcFunc hcf = defaultHealthCalc)
            : healthFunc(hcf) {
    }
    int healthValue() const {
        return healthFunc(*this);
    }
  private:
    HealthCalcFunc healthFunc;
};

HealthCalcFunc 是個 typedef,用來表現 tr1::function 的某個具現體,意味著該具現體的行為像一般的 function pointer。
而 HealthCalcFunc 的 signature 代表 function 是「接受一個 reference 指向 const GameCharacter,並傳回 int」,這個 tr1::funcion 型別產生的物件,可以持有任何與此 signature 相容的 callable entry。所謂相容,就是這個 callable entry 可被隱式轉換為 const GameCharacter&,而返回型別亦可被隱式轉換為 int。
short calcHealth(const GameCharacter&);  // function

struct HealthCalculator {  // function object
    int operator() (const GameCharacter&) const { ... }
};

class GameLevel {
  public:
    float health(const GameCharacter&) const;  // member function
    ...
};

class EvilBadGuy : public GameCharacter { ... };

class EyeCandyCharacter : public GameCharacter { ... };

EvilBadGuy ebg1(calcHealth);  // 使用 function

EyeCandyCharacter ecc1(HealthCalculator());  // 使用 function object

GameLevel currentLevel;
...
EvilBadGuy ebg2(
    std::tr1::bind(&GameLevel::health, currentLevel, _1);
// 使用 member function

首先,為計算 ebg2 的健康指數,應該使用 GameLevel class 的 member function health。
而 GameLevel::health() 宣稱自己可以接受一個參數,但實際上是接受兩個參數,因為它也獲得一個隱式參數 GameLevel,也就是 this 所指的東西。
然而 GameCharacters 的健康計算 function 只接受單一參數: GameCharacter。
所以如果使用 GameLevel::health() 做為 ebg2 的健康計算函鄉,必須以某種方式轉換它,使用不再接受兩個參數(Game Character 及 GameLevel),轉而接受一個參數(GameCharacter),在上例子,就是使用 currentLevel 作為「bg2 的健康計算 function 所需的那個 GameLevel 物件」,於是將 currentLevel 綁定為 GameLevel 物件,讓它在「每次 GameLevel::health() 被呼叫以計算 ebg2 的健康」時被使用,而這正是 tr1::bind() 的作為。

"_1" 意味「當為 ebg2 呼叫 GameLevel::health() 時,是以 currentLevel 做為 GameLevel 物件」。


古典的 Strategy 範式
典型的 Strategy 作法會將健康計算 function 做成一個分離的繼承體系中的 virtual member function:
其中 GameCharacter 是某個繼承體系的根類別,體系中的 EvilBadGuy 和 EyeCandyCharacter 都是 derived class。
而 HealthCalcFunc 是另一個繼承體系的根類別,體系中的 SlowHealthLoser 及 FastHealthLoser 都是 derived class。
而每個 GameCharacter 物件都內含一個 pointer,指向一個來自 HealthCalcFunc 繼承體系的物件:
class GameCharacter;  // forward declaration

class HealthCalcFunc {
  public:
    ...
    virtual int calc(const GameCharacter& gc) const { ... }
    ...
};

HealthCalcFunc defaultHealthCalc;

class GameCharacter {
  public:
    explicit GameCharacter(HealthCalcFunc* hcf = &defaultHealthCalc)
            : pHealthFunc(hcf) {
    }
    int healthValue() const {
        return pHealthFunc->calc(*this);
    }
  private:
    HealthCalcFunc* pHealthFunc;
};


以上並未徹底而詳盡列出 virtual function 的所有替代方案,此外,每個替代方案也都各有優、缺點。


virtual function 替代方案包括 NVI 手法及 Strategy design pattern 的多種型式。
將機能從 member function 移到 class 外部 function,所帶來的缺點之一是:非成員函式將無法存取 class 內的 non-public 成員。
tr1::function 物件的行為就像一般函式,這樣的物件可以接納「與給定之 target signature 相容」的所有 callable entry。




36、Never redefine an inherited non-virtual function.
class B {
  public:
    void mf();
    ...
};

class D : public B {
  public:
    void mf();  // 不應該 redefine
    ...
};

D x;
B* pB = &x;
pB->mf();  // 呼叫的是 B::mf()

D* pD = &x;
pD->mf();  // 呼叫的是 D::mf()

此例中,造成 mf() 會有兩種行為的原因是:
non-virtual function 是 statically bound,詳述於條款 37
這意思是由於 pB 被宣告為一個 pointer-to-B,則透過 pB 喚起的 non-virtual function 永遠都是 B 所定義的版本,即使 pB 指向一個型別為「B衍生之 class」的物件。

但另一方面,virtual function 卻是 dynamically bound,詳見條款 37,所以如果 mf 是 virtual function,則不論透過 pB 或 pD 呼叫 mf,都會喚起 D::mf(),因為 pB 和 pD 真正指的都是一個型別為 D 的物件。

也就是說,redefine 繼承自 base class 的 non-virtual function,當此 function 被呼叫時,有可能展現出 base class 的 function 行為,也有可能是 derived class 的 function 行為,決定因素不在物件本身,而是在「指向該物件之指標」,而同樣的情況,也會發生在 reference 身上。


上述的討論是在實務面,接著是理論層面的理由:
條款 32 有說明,所謂 public inheritance 意味著 is-a 關係,而條款 34 描述為何在一個 class 內宣告一個 non-virtual function 會為該 class 建立起一個 invariant,凌駕其 spcialization,將這兩個觀點施行於 class B 跟 class D 及 non-virtual member function B:mf() 上:
  • 適用於 B 物件的每件事,也一定適用於 D 物件,因為每個 D 物件都是一個 B 物件。
  • B 的 derived class 一定會繼承 mf 的 interface 和 implementation,因為 mf() 是 B 的 non-virtual function。

如果此時 D 重新定義 mf(),則上述理論將出現矛盾。


Never redefine an inherited non-virtual function.



37、Never redefine a function's inherited default parameter value.
能被繼承的 function 有兩種:virtual 及 non-virtual function。
條款 36 說明:重新定義繼承而來的 non-virtual 永遠是錯的,所以可以安全地將本條款的討論侷限於「繼承一個帶有 default value 的 virtual function」。
在這個情況下,本條款成立的理由就非常明確了,原因是:
virtual function 是 dynamically bound,而 default value 卻是 statically bound。

statically binding 又稱為 early binding,而 dynamically binding 則又稱為 late binding。

物件的所謂 static type,就是它在程式中被宣告時所採用的型別;而物件的 dynamic type 則是指「目前所指物件的型別」,也就是說 dynamic type 可以表現出一個物件將會有什麼行為:
class Shape {
  public:
    enum ShapeColor { Red, Green, Blue };
    virtual void draw(ShapeColor color = Red) const = 0;
    // draw()
    ...
};

class Rectangle : public Shape {
  public:
    virtual void draw(ShapeColor color = Green) const;
    // draw() with different default value
    ...
};

class Circle : public Shape {
  public:
    virtual void draw(ShapeColor color) const;
    // draw() with no default value
    // 這個寫法,在使用者以物件呼叫此 function時,
    // 一定要指定參數值,因為 statically binding 下,
    // 這個 function 並不會從其 base class 繼承 default value。
    // 但若是以 pointer 或 reference 呼叫此 function,
    // 則可以不指定參數值,因為 dynamically binding 下,
    // 這個 function 會從其 base class 繼承 default value。
    ...
};

Shape* ps;  // static type 為 Shape*
Shape* pc = new Circle();  // static type 為 Shape*
Shape* pr = new Rectangle();  // static type 為 Shape*

本例中,ps, pc, pr 都被宣告為 pointer-to-Shape 型別,所以不管它們指向什麼,它們的 static type 都是 Shape*。
而 pc 的 dynamic type 是 Circle*,pr 的 dynamic type 是 Rectangle*,ps 因尚未指向任何物件,所以沒有 dynamic type。

dynamic type 一如名稱所示,可以在執行過程中改變:
ps = pc;  // ps 的 dynamic type 如今是 Circle*
ps = pr;  // ps 的 dynamic type 如今是 Rectangle*

virtual function 係由 dynamic binding 而來,所以呼叫一個 virtual function,是由發出呼叫的那個物件的 dynamic type來決定,要喚起哪一份 function 實作碼:
pc->draw(Shape::Red);  // 呼叫 Circle::draw(Shape::Red)
pr->draw(Shape::Red);  // 呼叫 Rectangle::draw(Shape::Red)

所以在考慮帶有 default value 的 virtual function 時,由於 virtual function 是 dynamically binding,而 default value 是 statically binding,造成可能會在「喚起一個定義於 derived class 內的 virtual function」時,卻使用 base class 所指定的 default value。
這個情況不只侷限於 pointer,也同樣存在於 reference 用法。

為什麼 C++ 堅持以這種方式來運作?
答案在於執行期效率,如果 default value 是 dynamically binding,則 compiler 就必須有辦法在執行期為 virtual function 決定適當的 default value,這將比目前採行的「在編譯期決定」的機制更慢且更為複雜。


如果試著遵守這個條款,並同時提供 default value 給 base 和 derived class,則有不一樣的困擾會出現:
class Rectangle : public Shape {
  public:
    virtual void draw(ShapeColor color = Red) const;
    ...
};

除了程式碼重複之外,還著著相依性,因為如果 Shape::draw() 內的 default value 改變了,則 Rectangle::draw() 內的 default value 也必須跟著改變。

當想令 virtual function 表現出想要的行為但遭遇到困難時,可以考慮替代設計,如條款 35 所列,其中之一是 NVI 手法:
class Shape {
  public:
    enum ShapeColor { Red, Green, Blue };
    void draw(ShapeColor color = Red) const {
        doDraw(color);
    }
    ...
  private:
    virtual void doDraw(ShapeColor color) const = 0;
    // 真正的工作在此處完成
};

class Rectangle : public Shape {
  public:
    ...
  private:
    virtual void doDraw(ShapeColor color) const = 0;
    // 不需要指定 default value
    ...
};


絕對不要重新定義一個繼承而來的 default value,因為 default value 都是 statically binding,而 virtual function - 唯一該 override 的東西 - 卻是 dynamically binding。



38、Model "has-a" or "is-implemented-in-terms-of" through composition.
composition 是型別之間的一種關係,當某種型別的物件內含它種型別的物件,便是這種關係:
class Address { ... };
class PhoneNumber { ... };
class Person {
  public:
    ...
  private:
    std::string name;  // composed object
    Address address;  // composed object
    PhoneNumber voiceNumber;  // composed object
    PhoneNumber facNumber;  // composed object
};

composition 的同意詞包含: layering, containment, aggregation, embedding。

composition 有兩個不同的意義,取進於作用域的不同:
  • application domain: has-a
  • implementation domain: is-implemented-in-terms-of

程式中的物件其實相當於世界中的某些事物,例如人、汽車、視訊畫面…等等,這樣的物件屬於 application domain。
其它物件則純粹是實作細節上的人工製品,像是 buffer、mutex、search tree…等等,這些物件相當於軟體的 implementation domain。
當 composition 發生於 application domain,所表現出來的是 has-a 的關係。
而當 composition 發生於 implementation domain,則是表現出 is-implementated-in-terms-of 的關係。

上述 Person class 明顯示範了 has-a 關係。

比較麻煩的是要區分 is-a 和 is-implementated-in-terms-of 這兩種物件關係。
假設需要一個 template,希望製造出一組 class 用來表現由不重複物件組成的 sets,此時第一個直接是採用標準程式庫提供的 set template,但 set 的實作往往招致「每個元素秏用三個指標」的額外開銷,因為 set 通常以 balanced search tree 實作而成,使它們在搜尋、安插、移除元素時保證擁有 logarithmic-time 效率。
但當程式的空間比速度重要的時候,終究還是得寫個自己的 template。

實作 set 的方法很多,其中一種是在底層採用 linked list,剛好標準函式庫有一個 list template:
template&lttypename T&gt
class Set : public std::list { ... };  // 不正確的用法

雖然看起來是正確的,但條款 32說明 public inheritance 應該是一種 "is-a" 關係,而在 list 中可以內含重複元素,但在 set 中卻不允許內含重複元素,所以用 public inheritance 是不正確的。

正確的作法是應當了解,Set 物件可以根據一個 list 物件實作出來:
template&lttypename T&gt
class Set {
  public:
    bool member(const T& item) const;
    void insert(const T& item);
    void remove(const T& item);
    std::size_t size() const;
  private:
    std::list rep;  // 用來表述 Set 資料
};

template&lttypename T&gt
bool Set::member(const T& item) const {
    return std::find(rep.begin(), rep.end(), item) != rep.end();
}

template&lttypename T&gt
bool Set::insert(const T& item) {
    if (!member(item)) rep.push_back(item);
}

template&lttypename T&gt
bool Set::remove(const T& item) {
    typename std::list::iterator it =
            std::find(rep.begin(), rep.end(), item);
    // typename 的用法詳見條款 42
    if (it != rep.end())
        rep.erase(it);
}

template&lttypename T&gt
bool Set::size() const {
    return rep.size();
}

這些 function 較為簡單,所以適合成為 inlining 候選人,詳見條款 30

上例中,另一個可以改進的部分是讓 Set 介面遵循 STL 容器的協定,就更符合條款 18 對設計介面的警告:「它介面容易被使用,不易被誤用」。


當 composition 發生於 application domain,所表現出來的是 has-a 的關係。
而當 composition 發生於 implementation domain,則是表現出 is-implemented-in-terms-of 的關係。




39、Use private inheritance judiciously.
條款 32 有提到 C++ 如何將 public inheritance 視為 is-a 關係,其中有個例子是 class Student 以 public 形式繼承 class Person,在此以 private inheritance 取代 public inheritance:
class Person { ... };
class Student : private Person { ... };  // private inheritance
void eat(const Person& p);
void study(const Student& s);

Person p;
Student s;

eat(p);  // 沒問題
eat(s);  // 出現 compile error

也就是說,如果 classes 之間是 private inheritance,compiler 不會自動將一個 derived class 物件轉換為一個 base class 物件。
private inheritance 的另一個規則是,由 private base 繼承而來的所有成員,在 derived class 中都會變成 private 屬性


private inheritance 意味 is-implemented-in-terms-of,也就是如果 class D 以 private 形式繼承 class B,則用意只是為了採用 class B 內已經備妥的某些特性。
private inheritance 純粹只是一個實作技術,在軟體「設計」層面上沒有意義,其意義只涉及於軟體實作層面。
不過條款 38提出 composition 也有 is-implemented-in-terms-of 的意義,這樣該如何在兩者中作取捨呢?
答案是:儘可能使用 composition,除非當 protected 成員或 virtual function 牽扯進來的時候,或是涉及空間最佳化的情況下。

假設決定要修改某個 Widget class,讓它紀錄每個 member function 被呼叫的次數,在執行期間可以週期性地審查那份資訊,為了完成這項工作,需要設定某種計時器:
class Timer {
  public:
    explicit Timer(int tickFrequency);
    virtual void onTick() const;
    // 每 tick 一次,該 function 就被執行一次
};

一個 Timer 物件,可調整成需要的任何頻率滴答前進,每滴答一次,就呼叫某個 virtual function,藉由 override 這個 virtual function,取出 Widget 的當時狀態。
為了讓 Widget 重新定義 Timber 內的 virtual function,Widget 必須繼承 Timer,但 public inheritance 並不適合,因為 Widget 並不是一個 Timber:
class Widget : private Timer {
  private:
    virtual void onTick() const;  // 查看 Widget 的資料
    ...
};

這樣的設計是不錯的,但 private inheritance 並非絕對並要,可以用 composition 取而代之:
class Widget {
  private:
    class WidgetTimer : public Timber {
      public:
        virtual void onTick() const;
        ...
    };
    WidgetTimer timer;
    ...
};

這個設計比只使用 private inheritance 要複雜一些,因為同時涉及 public inheritance 和 composition,並導入一個新 class,但以下有兩個理由設明為何 public inheritance + composition 可能會優於 private inheritance:
  • 如果想設計 Widget 使它得以擁有 derived class,但同時想阻止 derived class 重新定義 onTick(),這在 Widget 繼承 Timer 的架構上是無法實現的,但是 composition + public inheritance 的架構上卻可以。
  • 如果要將 Widget 的 compilation dependency 降至最低,如果是 private inheritance,則當 Widget 被 compile 時,Timber 的定義必須可見,但如果是 composition + public inheritance,Widget 可以只帶一個簡單的 WidgetTimber 宣告式即可,有關最小化 compilation dependency 可見條款 31


不過,如上所述,有一種情況涉及空間最佳化,可能會使 private inheritance 成為較好的選擇。
這個情況只適用於所處理的 class 不帶任何資料時,這樣的 class 沒有 non-static member variable、沒有 virtual function(因為這個 function 會為每個物件帶來一個 vptr,詳見條款 7)、也沒有 virtual base class(因為這樣的 base class 也會招來體積上的額外開銷,詳見條款 40)。
於是這種所謂的 empty class 物件不使用任何空間,因為沒有任何屬於物件的資料需要儲存,然而由於技術上的理由,C++ 裁定凡是獨立物件都必須有非零大小:
class Empty { };  // empty class

class HoldsAnInt {  // 應該只需要一個 int 空間
  private:
    int x;
    Empty e;  // 應該不需要任何空間
};

會發現 sizeof(HoldsAnInt) > sizeof(int),在大多數 compiler 中 sizeof(Empty) 獲得 1,因為面對「大小為零之獨立物件」,通常 C++ 官方勒令默默安插一個 char 到 empty class 內。

但這個約束並不適用於 derived class,因為 derived class 內的 base class 並非「獨立」:
class Empty { };  // empty class

class HoldsAnInt : private Empty {
  private:
    int x;
};

幾乎可以確定 sizeof(HoldsAnInt) == sizeof(int),這是所謂的 EBO(Empty Base Optimization)。
所以如果在非常在意空間的情況下,EBO 是值得多留意的,另外 EBO 一般只在單一繼承下才可行,無法被施行於「擁有多個 base」的 derived class 身上。

現實中的 empty class 並非真得是 empty,雖然它們從未擁有 non-static member variable,卻往往內含 typedefs, enums, static member variable, or non-virtual function。


private inheritance 意味 is-implemented-in-terms-of,它通常不如 composition 來得好,但是當 derived class 需要存取 protected base class 的 member,或需要重新定義繼承而來的 virtual function 時,這樣設計是合理的。
private inheritance 可能造成 empty base optimization,對致力於「物件尺寸最小化」的開發著而言,可能很重要。




40、Use multiple inheritance judiciously.
對於 multiple inheritance(MI),C++ 社群便分為兩個基本陣營:
  • 如果 single inheritance(SI) 是好的,那 multiple inheritance 一定更好。
  • 另一派陣營則主張,single inheritance 是好的,但 multiple inheritance 卻不值得使用。

對於 multiple inheritance,最先要認清的一件事是,當在設計時,程式可能從一個以上的 base class 繼承相同名稱,那將會導致較多的 ambiguity 機會:
class BorrowableItem {
  public:
    void checkOut();
    ...
};

class ElectronicGadget {
  private:
    bool checkout() const;
    ...
};

class MP3Player : public BorrowableItem, public ElectronicGadget  { ... };

MP3Player mp;
mp.checkout();  // ambiguity

有關 C++ 用來決議 overriding function 呼叫的規則是,先找到最佳匹配的 function,再檢驗其可取用性,所以此例中的兩個 checkout() 有相同的匹配程度,沒有所謂的最佳匹配,所以會造成 ambiguity。

解決的方式是明確指出要呼叫哪一個 base class 內的 function:
mp.BorowableItem::checkout();

當然也可以指定呼叫 ElectronicGadget::checkout(),然後得到一個「嘗試呼叫 private member function」的錯誤。

multiple inheritance 的意思是指繼承超過以上的 base class,但這些 base class 並不常在繼承體系中又有更高階的 base class,因為那有可能會導致「diamond-shaped multiple inheritance hierarchy」:
class File { ... };
class InputFile : public File { ... };
class OutputFile : public File { ... };
class IOFile : public InputFile, public OutputFile { ... };
// diamond-shaped multiple inheritance hierarchy

當「diamond-shaped multiple inheritance hierarchy」發生的時,就必須面對這樣的問題:
是否打算讓 base class 內的成員變數經由每一條路徑被複製?假設 File class 有個成員變數 fileName,那 IOFile 內該有多少筆這個名稱的資料呢?

從某個角度來說,IOFile 因為從兩個 base class 各繼承一份,所以物件內應該要有兩份 fileName,但從另一個角度來看,IOFile 物件本來就該只有一個檔案名稱。

C++ 的預設作法是執行複製,也就是第一種作法,如果要採用第二種作法,就必須令帶有此資料的 class 成為 virtual base class,須令所有直接繼承自它的 class 採用 「virtual inheritance」:
class File { ... };
class InputFile : virtual public File { ... };
class OutputFile : virtual public File { ... };
class IOFile : public InputFile, public OutputFile { ... };

如果只從正確行為的觀點來看,public inheritance 應該總是 virtual,但是使用 virtual inheritance 的那些 class 所產生的物件往往比使用 non-virtual inheritance 的體積來得大,存取成員變數的速度也較慢,種種細節因 compiler 不同而異,但總是得為 virtual inheritance 付出付價。

對於 virtual base class 的忠告有二:
  • 非必要不使用 virtual base class,平常就使用 non-virtual inheritance。
  • 如果必須使用 virtual base class,儘可能避免在其中放置資料,就可以不必擔心這些 class 身上的初始化和賦值所帶來的詭異事情。


接著以條款 31 的 Interface class IPerson 為例:
class IPerson {  // abstract class
  public:
    virtual ~IPerson();
    virtual std::string name() const = 0;
    virtual std::string birthDate() const = 0;
};

std::tr1::shared_ptr makePerson(DatebaseID personIdentifier);
// factory function

DatabaseID askUserForDatabaseID();

DatabaseID id(askUserForDatabaseID());
std::tr1::shared_ptr pp(makePerson(id));
...

在 makePerson() 如何產生物件並回傳一個指標指向它呢?無疑地一定有某些衍生自 IPerson 的 concrete class,在其中 makePerson() 可以產生物件。
假設這個 class 名為 CPerson,而 CPerson 當然也必須提供「繼承自 IPerson」的所有 pure virtual function 的實作碼,當然可以從無到有地寫出這些東西,但最好可以利用既有組件,假設有個既有資料庫相關的 class,名為 PersonInfo,提供 CPerson 所需要的實質東西:
class PersonInfo {
  public:
    explicit PersonInfo(DatabaseID pid);
    virtual ~PersonInfo();
    virtual const char* theName() const;
    virtual const char* theBirthDate() const;
    ...
  private:
    virtual const char* valueDelimOpen() const;
    virtual const char* valueDelimClose() const;
    ...
};

const char* PersonInfo::valueDelimOpen() const {
    return "[";  // 預設的起始符號
}

const char* PersonInfo::valueDelimClose() const {
    return "]";  // 預設的起始符號
}

const char* PersonInfo::theName() const {
    static char value[Max_Formatted_Field_Value_Length];
    // 給回傳值使用的緩衝區;由於是 static,
    // 所以會自動被初始化為「全部是 0」

    std::strcyp(value, valueDelimOpen());
    ...  // 寫入 name
    std::strcyp(value, valueDelimClose());
    return value;
}

CPerson 和 PersonInfo 的關係是,PersonInfo 剛好有若干 function 可以幫助 CPerson 比較容易實作出來,因此是 is-implemented-in-terms-of,所以有兩種技術可以實現:composition 及 private inheritance。
本例中,CPerson 需要重新定義 valueDelimOpen 及 valueDelimClose,所以最直接的解法是令 Cperson 以 private 形式繼承 PersonInfo。
另外 CPerson 也必須實作 IPerson interface,這得以 public inheritance 才能完成: class IPerson { // abstract class public: virtual ~IPerson(); virtual std::string name() const = 0; virtual std::string birthDate() const = 0; }; class DatabaseID { ... }; class PersonInfo { public: explicit PersonInfo(DatabaseID pid); virtual ~PersonInfo(); virtual const char* theName() const; virtual const char* theBirthDate() const; virtual const char* valueDelimOpen() const; virtual const char* valueDelimClose() const; ... }; class CPerson : public IPerson, private PersonInfo { // multiple inheritance public: explicit CPerson(DatabaseID pid) : PersonInfo(pid) { } virtual std::string name() const { return PersonInfo::theName(); } virtual std::string birthDate() const { return PersonInfo::theBirthDate(); } private: const char* valueDelimOpen() const { return ""; }; const char* valueDelimClose() const { return ""; }; };
這個例子說明了 multiple inheritance 的合理應用:將「public 繼承自某介面」和「private 繼承自某實作」結合在一起。

multiple inheritance 通常較 single inheritance 較為複雜,使用上也較難理解,所以 single inheritance 設計方案通常較受歡迎。


multiple inheritance 較 single inheritance 為複雜,且可能導致 ambiguity 及對 virtual inheritance 的需要。
virtual inheritance 會增加大小、速度、初始化及賦值的複雜度等等,在 virtual base classes 不帶任何資料的情況,將是最具實用價值的情況。
multiple inheritance 具有合理應用的情況,其中之一是對一個 interface class 實作 public inheritance ,對另一個協助實作的 class 進行 private inheritance。
arrow
arrow
    全站熱搜
    創作者介紹

    silverfoxkkk 發表在 痞客邦 留言(0) 人氣()