c++私有繼承與組合淺析

簡介


繼承是面向對象編程的重要屬性。有效地使用繼承,可以複用現有的設計,加速產品開發進度。

公有繼承塑造了is-a關係,如蘋果is a水果,所以蘋果可以以public方式繼承水果,所有被改寫的成員函數滿足“不要求更多,也不承諾更少”的原則。

關於私有繼承,它體現了is-implemented-in-terms-of關係,但這種關係又可以使用組合的方式來實現。

那麼,私有繼承與組合之間到底是什麼關係?什麼時候應該使用私有繼承?什麼時候又應該儘量使用組合以替代晦澀的私有繼承?本文作一探討。

從一個示例剖析


以下示例從一個模板類使用不同方式生成兩個子類:

// base
template <class T>
class MyList
{
public:
	bool Insert(const T&, size_t index);
	T Access(size_t index) const;
	size_t Size() const;
	
private:
	T* buf_;
	size_t bufsize_;
};

//繼承方式
template <class T>
class MySet1 : private MyList<T>
{
public:
	bool Add(const T&); // call Insert()
	T Get(size_t index) const; // call Access()
	using MyList<T>::Size;
	//...
};

// 組合方式
template <class T>
class MySet2
{
public:
	bool Add(const T&); // call impl_.Insert()
	T Get(size_t index) const; // call impl_.Access()
	size_t Size() const; // call impl_.Size()
	//...
private:
	MyList<T> impl_;
};

說明:

  • MySet1和MySet2的功能完全相同,沒有任何實際意義上的差別
  • 私有繼承表現is-implemented-in-terms-of(根據某物實作出)意義,使得子類可以使用基類的public/protected成份
  • 組合表現has-a(有一個)意義,也連帶有is-implemented-in-terms-of意義,它只能使用其他類的public成份
  • 私有繼承是單組合的一個超集,即,所有組成能完成的事情,私有繼承也都能完成
  • 私有繼承時,子類只能擁有一份基類,如果需要該類的多個實體,只能使用組合

那麼,什麼時候需要使用私有繼承?

  • 當需要改寫虛函數時,特別地,對於純虛基類,只能繼承
  • 需要處理protected 成員時
  • 當類之間的生命週期需要特別注意時,可能需要使用繼承
  • 需要分享某個共同的虛基類或者改寫某個虛基類的構建程序時
  • 基類是空基類時,即基類沒有數據成員時,使用繼承可以複用空基類的最佳化而獲得空間優勢

基於以上分析,示例中在構建MySet時沒有理由需要使用繼承。組合完美地完成了任務,並減少了類之間的耦合。

組合的優點:

  • 允許使用多個其他類的實例
  • 使得其他類作爲一個成員使用,帶來更多彈性

對MySet2稍作修改,可以得到一個更多彈性的版本:

template <class T, class Impl = MyList<T>>
class MySet3
{
public:
	bool Add(const T&); // call impl_.Insert()
	T Get(size_t index) const; // call impl_.Access()
	size_t Size() const; // call impl_.Size()
	//...
private:
	Impl impl_;
};

有時候,可能需要私有繼承與組合的靈活結合,以達到巧妙複用各自優點的效果。

根據如下基類:

class B
{
public:
	virtual int Func1();
protected:
	bool Func2();
private:
	bool Func3(); // call Func1
};

需要改寫虛函數Func1,或存取Func2,就需要使用繼承。如下:

class D : private B
{
public:
	int Func1();
	//... 
	// maybe call B::Func2()
};

這份代碼滿足了要求,允許D改寫Func1。

但是,它也允許所有D的成員取用Func2,並非所有成員都需要的。但私有繼承還是把protected接口全部透露了出來。

很明顯,私有繼承是必要的,那麼我們如何能夠只導入真正需要的耦合呢?

增加一點代碼,就可以做的更好。

// 私有繼承
class DerivedImpl : private B
{
public:
	int Func1();
	//... need call Func2
};

// 組合
class Derived
{
// ... not use Func2
private:
	DerivedImpl impl_;
};

看看,這樣就良好地警衛並封裝了對B的依賴。Derived只直接依賴B的public接口和DerivedImpl的public接口。

這也遵循了“一個類一個任務”的設計準則。

小結


繼承往往被過度運用。在設計過程中,耦合關係要儘量減少。

如果類與類之間的關係可以有多種方式來表達,就使用其中關係最弱的一種。而繼承幾乎是最強烈的關係。

一般來說,可以參考以下:

  • 如果組合可行,就不用考慮繼承
  • 如果私有繼承ok,不要使用public繼承
  • 如果類之間可以使用多種關係,就使用耦合最弱的一種
  • 避免使用多重繼承
參考資料

《Exxceptional C++》

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