The Virtues Of Bastard

Blood is inherited and virtue is acquired.
-- Venezuelan Proverb

引子

在剛剛結(jié)束的《權(quán)力的游戲》第六季里,最讓人熱血沸騰的是第九集《The War of Bastards》;而在第十集,Jon Snow,這個(gè)具有公正,勇敢,富有榮譽(yù)感等多種美德的bastard,正式獲得繼承權(quán),成為臨冬城主,北境之王!

Jon Snow

這讓我想起了C++語(yǔ)言中的一個(gè)有趣特性:私有繼承。

私有繼承或許是C++語(yǔ)言特有的一種特性。所以你在各種面向?qū)ο?/strong>教材中,很難看到針對(duì)這種用法的討論。在現(xiàn)實(shí)項(xiàng)目中,也很難看到它的蹤影。

它究竟有何用途?

匿名訪問(wèn)

我們知道繼承意味著子類父類之間存在IS-A的關(guān)系。所以,子類可自動(dòng)被當(dāng)作父類來(lái)使用(當(dāng)然要符合里氏替換原則)。

下面的代碼,可以非常順利的編譯通過(guò)。

struct Base {};
struct Derived : public Base {}; 

void f(Base* base); 
// ... 

Derived derived; 

f(&derived); // 編譯正常 

但如果DerivedBase之間是私有繼承的關(guān)系,在Derived的外部客戶看來(lái), 它和Base之間并不存在IS-A的關(guān)系。事實(shí)上,在客戶眼里,它們什么關(guān)系也沒有。Base作為 Derived的一種內(nèi)部實(shí)現(xiàn)細(xì)節(jié),從邏輯上被徹底隱藏了。比如:

struct Derived : private Base {}; // 私有繼承 

// ... 

Derived derived; 

f(&derived); // 不能編譯!! 

而在類的設(shè)計(jì)者看來(lái),DerivedBase的任何外部客戶一樣,可以自由訪問(wèn)Base的所有public成員,但對(duì)其非public成員卻毫無(wú)權(quán)限。也就是說(shuō),Base相當(dāng)于Derived的一個(gè)成員變量。所以它們之間的關(guān)系相當(dāng)于委托。

但它比委托要更方便。因?yàn)樵谖蟹绞较拢?code>Derived必須定義一個(gè) Base 類型的成員變量,并通過(guò)變量訪問(wèn)Base的成員。而私有繼承則無(wú)須這么做。其差別如下所示:

struct Base 
{ 
  void f(); 
};

// 委托
struct Object 
{ 
  void f1() 
  { 
    base.f(); // 需通過(guò)成員變量訪問(wèn) 
  } 
  
private:  
  Base base; 
}; 

// 私有繼承
struct Derived : private Base 
{ 
  void f1() 
  { 
    f(); // 可直接訪問(wèn)
  } 
};

避免"中間人"

另外,編程實(shí)踐中最讓人厭煩的活動(dòng)之一,是編寫中間人Middle Man)式的轉(zhuǎn)調(diào)代碼。(關(guān)于中間人,參見Martin Fowler《重構(gòu)》

比如,我們?cè)瓉?lái)有一個(gè)類Foo,存在著多個(gè)public成員函數(shù)接口?,F(xiàn)在,系統(tǒng)中另一處需要一個(gè)類Bar,Bar只需要提供一個(gè)接口int f(),其功能需求和Foo提供的同名接口完全一致。所以,我們想通過(guò)委托關(guān)系復(fù)用Foo的實(shí)現(xiàn)。

經(jīng)典委托的實(shí)現(xiàn)方式,是直接轉(zhuǎn)調(diào)其接口。比如:

struct Foo 
{ 
  int f();
  int g(); 
  int h(); 
  // ... 
};
 
struct Bar 
{ 
  int f() 
  { 
    return foo.f(); 
  } 

private: 
  Foo foo; 
};

如果使用私有繼承,則可以通過(guò)部分暴露的方式來(lái)簡(jiǎn)化你的工作:

struct Bar : private Foo // 私有繼承 
{ 
  // 部分暴露 Foo 的接口。 
  using Foo::f; 
};

我們知道,實(shí)現(xiàn)繼承本質(zhì)上是一種擴(kuò)展關(guān)系。而這種通過(guò)私有繼承進(jìn)行部分暴露接口的用法是一種反擴(kuò)展

避免額外的派生類

公有繼承一樣,私有繼承的子類可以實(shí)現(xiàn)父類的虛函數(shù)。

所以,如果簡(jiǎn)單的以委托的方式來(lái)實(shí)現(xiàn)組合,程序員們則不得不先通過(guò)派生類給出實(shí)現(xiàn),然后再組合派生類。如下圖所示:

組合派生類
組合派生類

但這樣的實(shí)現(xiàn)方式,不僅需要定義一個(gè)新的類,更重要的是,派生類的實(shí)現(xiàn)方式很可能需要使用客戶類的內(nèi)容,而客戶類并不想將這些內(nèi)容公開。兩個(gè)類該如何配合,就變成了設(shè)計(jì)者一個(gè)非常棘手的問(wèn)題。

但通過(guò)私有繼承,則可以完美的解決這中問(wèn)題。如下圖所示:

通過(guò)私有繼承避免額外派生類
通過(guò)私有繼承避免額外派生類

對(duì)遺留系統(tǒng)結(jié)構(gòu)體的封裝

在遺留系統(tǒng)中,會(huì)存在一些只有數(shù)據(jù)沒有行為的結(jié)構(gòu)體。而這樣的結(jié)構(gòu)體經(jīng)常作為參數(shù),在模塊之間到處傳遞。不同模塊在獲取數(shù)據(jù)之后,會(huì)根據(jù)這些數(shù)據(jù)進(jìn)行一系列的計(jì)算。

比如, 我們又一個(gè)名為Rectangle的數(shù)據(jù)結(jié)構(gòu):

struct Rectangle
{
   int width;
   int height;
};

對(duì)于這樣的數(shù)據(jù)結(jié)構(gòu),我們當(dāng)然想進(jìn)行封裝,以享用封裝所帶來(lái)好處。而如果這一切都發(fā)生在可控的單個(gè)子系統(tǒng)內(nèi)部,毫無(wú)疑問(wèn),你應(yīng)該這么做。

但如果Rectangle跨多個(gè)子系統(tǒng),或者一個(gè)子系統(tǒng)過(guò)大,你可能就會(huì)面臨下列問(wèn)題:

  1. 每個(gè)子系統(tǒng)對(duì)于Rectangle的行為定義都是不一樣的,也就是說(shuō),它們唯一共享的就是數(shù)據(jù);
  2. 不同子系統(tǒng)由不同的團(tuán)隊(duì)維護(hù),你無(wú)權(quán)修改它們的代碼,也無(wú)權(quán)修改共享的頭文件;
  3. 直接進(jìn)行封裝,會(huì)造成大面積的代碼的修改;
  4. 其它子系統(tǒng)的開發(fā)語(yǔ)言仍然是C
  5. ...

總而言之,你不能修改原有的結(jié)構(gòu)體Rectangle。 這種情況下,你依然想在正在重構(gòu)的代碼中對(duì)Rectangle進(jìn)行封裝,那該怎么
辦?

私有繼承,是解決這類問(wèn)題的不錯(cuò)選擇。


struct MyRectangle : private Rectangle 
{ 
  int getArea() const 
  { 
    return width * height; 
  }
   
  int getPerimeter() const 
  { 
    return 2 * (width + height); 
  } 
};

為什么是私有繼承?因?yàn)槲覀兿脒M(jìn)行封裝,讓子系統(tǒng)內(nèi)部的代碼沒有人可以自由的訪問(wèn)數(shù)據(jù),以享用封裝帶來(lái)的好處。

為什么不使用委托?因?yàn)槿绻麤]有之前所說(shuō)的那些約束,這些數(shù)據(jù)和行為本來(lái)就應(yīng)該屬于一個(gè)類。而委托關(guān)系,會(huì)造成我們?cè)L問(wèn)每個(gè)數(shù)據(jù)成員時(shí),都必須通過(guò)成員變量進(jìn)行間接訪問(wèn),這毫無(wú)疑問(wèn)給我們帶來(lái)了不必要的負(fù)擔(dān)。

私有繼承也意味著,在客戶代碼那里IS-A關(guān)系的喪失。好在我們還有強(qiáng)制轉(zhuǎn)換的武器,我們只需要在子系統(tǒng)邊界對(duì)其進(jìn)行類型強(qiáng)換,在子系統(tǒng)內(nèi)部均使用MyRectangle即可。

盡管看起來(lái)讓人有些不安,由于這種繼承完全沒有修改任何內(nèi)存布局,所以這種強(qiáng)轉(zhuǎn)是絕對(duì)安全的。

// 本子系統(tǒng)邊界函數(shù)
void s1_boundary(Rectangle* rect) 
{ 
  // ...  
  // 強(qiáng)制轉(zhuǎn)換 
  s1_internal1((MyRectangle&)*rect);
  // ...
}

// 子系統(tǒng)內(nèi)部函數(shù),使用 MyRectangle 
void s1_internal1(MyRectangle& rect) 
{ 
  int perimeter = rect.getPerimeter(); 
  // ...  
  s1_internal2(rect);  
  // ... 
} 

// 子系統(tǒng)內(nèi)部函數(shù),使用 MyRectangle 
void s1_internal2(MyRectangle& rect) 
{ 
  int area = rect.getArea();  
  // ... 
  s2_boundary((Rectangle*)&rect); 
} 

// 其它子系統(tǒng)的邊界函數(shù),仍然使用 Rectangle 
void s2_boundary(Rectangle* rect);

這種方法,在一些以消息作為進(jìn)程間通信手段的嵌入式系統(tǒng)中,是一種非常有效的封裝手段。

私有繼承也是繼承

Scott Meyers在其著作《Effective C++》中,將私有繼承定義為和組合一樣的關(guān)系(is-implemented-in-terms- of)。

盡管他也提到私有繼承的一個(gè)優(yōu)勢(shì)是可以實(shí)現(xiàn)父類的虛函數(shù),但他沒有明確的指出,私有繼承也是一種繼承。

只是這種繼承關(guān)系通過(guò)private關(guān)鍵字對(duì)外界隱藏了真相;但是,在類的內(nèi)部, 子類和基類IS-A關(guān)系依然成立。我將這種關(guān)系稱之為:私有繼承的子類是父類的私生子。

這就意味著,當(dāng)我們想利用繼承關(guān)系來(lái)完成一些特定的設(shè)計(jì),但又不想讓這種關(guān)系被外部利用時(shí),私有繼承就是絕佳的選擇。

比如:在一顆二叉樹上,正常情況下,每個(gè)結(jié)點(diǎn)都有存在一個(gè)父結(jié)點(diǎn)和兩個(gè)子節(jié)點(diǎn)。但存在一些例外情況:根節(jié)點(diǎn)沒有父節(jié)點(diǎn),而葉子節(jié)點(diǎn)則沒有子節(jié)點(diǎn)。

所以,我們用這樣的數(shù)據(jù)結(jié)構(gòu)來(lái)表現(xiàn)一個(gè)節(jié)點(diǎn):

struct Node 
{ 
  // ... 
private:  
  Node* parent; 
  Node* leftChild, rightChild; 
}; 

在這個(gè)樹上,有時(shí)候一個(gè)節(jié)點(diǎn)的狀態(tài)變化必須通知給其所有父子節(jié)點(diǎn),而其父子也會(huì)進(jìn)一步將此事件向上下傳播。如下:

void Node::notifyEvent() 
{ 
  notifyParent();
  notifyChildren();
}

void Node::notifyParent() 
{ 
  if(parent != 0) parent->onChildStateChange(); 
}
 
void Node::notifyChildren() 
{ 
  if(leftChild != 0)
  { 
    leftChild->onParentStateChange();
  }
  
  if(rightChild != 0) 
  {
    rightChild->onParentStateChange(); 
  }
}

可以看到,代碼中存在一些空指針判斷,如果這些空指針只有少數(shù)的幾個(gè),也沒有什么大問(wèn)題。但如果這些的事件很多,每個(gè)事件的處理手段也不一樣,可能就會(huì)早就很多的空指針判斷語(yǔ)句,這讓我們的代碼很不干凈。

當(dāng)然解決空指針問(wèn)題的常用手段是空對(duì)象模式Null Object)。所以,我們將設(shè)計(jì)修改為:

struct NodeEventListener
{
  virtual void onParentStateChange() = 0; 
  virtual void onChildStateChange() = 0;
  // ... 
  virtual ~NodeEventListener(){} 
};

struct Node : NodeEventListener 
{ 
  Node(Node* parent, Node* leftChild, Node* rightChild); 
  
  void onParentStateChange(); 
  void onChildStateChange(); 
  // ... 

private:  
  NodeEventListener* parent; 
  NodeEventListener* leftChild; 
  NodeEventListener* rightChild; 
};
 
namespace 
{  
  struct NullNode : NodeEventListener 
  { 
    void onParentStateChange() {} 
    void onChildStateChange() {}
 
    // ... 
    static NullNode* getInstance() 
    { 
      static NullNode instance;
      return &instance; 
    } 
  }; 

  NodeEventListener* getListener(NodeEventListener* node) 
  { 
    return node == 0 ? NullNode::getInstance() : node; 
  } 
} 

Node::Node
  ( Node* parent
  , Node* leftChild
  , Node* rightChild) 
  : parent(getListener(parent))  
  , leftChild(getListener(leftChild))
  , rightChild(getListener(rightChild)) 
{ } 

void Node::notifyParent() 
{ 
   parent->onChildStateChange();
}

void Node::notifyChildren() 
{ 
  leftChild->onParentStateChange();
  rightChild->onParentStateChange();
}

void Node::onParentStateChange() 
{ 
  // 真正的事件處理代碼  
} 

void Node::onChildStateChange() 
{ 
  // 真正的事件處理代碼 
}

作為一個(gè)C++的標(biāo)準(zhǔn)空對(duì)象模式的實(shí)現(xiàn),上述設(shè)計(jì)解決了空指針判斷的問(wèn)題。并且Node類和NodeEventListener之間從概念層面的 IS-A也是成立的。所以,我們理應(yīng)為這樣的結(jié)果感到欣慰。

但如果你再仔細(xì)審視一下,就會(huì)產(chǎn)生這樣的疑問(wèn):這個(gè)IS-A的關(guān)系,需要被Node外部的用戶知道嗎?你希望他們可以通過(guò)NodeEventListener類型來(lái)調(diào)用NodeonParentStateChange()onChildStateChange()等方法嗎?

答案是否定的,因?yàn)檫@是一個(gè)內(nèi)部設(shè)計(jì)。

所以我們需要將這些接口隱藏起來(lái)。方法很簡(jiǎn)單:將繼承關(guān)系改為 private, 并將繼承自NodeEventListener的函數(shù)也設(shè)為 private 即可:

// ... 

struct Node : private NodeEventListener 
{ 
  Node(Node* parent, Node* leftChild, Node* rightChild); 
  // ... 

  // 來(lái)自于 NodeEventListener 的函數(shù)被設(shè)為私有 

private: 
  void onParentStateChange(); 
  void onChildStateChange(); 
  // ... 

private:  
  NodeEventListener* parent; 
  NodeEventListener* leftChild; 
  NodeEventListener* rightChild; 
};

盡管我們將繼承關(guān)系改為了私有,但請(qǐng)注意,parent等成員變量仍然是NodeEventListener類型,而構(gòu)造函數(shù)的參數(shù)類型卻是Node類型(想想為什么?)。 從NodeNodeEventListener的自動(dòng)類型轉(zhuǎn)換在構(gòu)造函數(shù)的實(shí)現(xiàn)代碼里完成,這說(shuō)明NodeNodeEventListener 之間的IS-A關(guān)系,在Node看來(lái),依然是成立的。

越獄

我們現(xiàn)在知道,私有繼承會(huì)對(duì)外屏蔽子類和父類的繼承關(guān)系。子類的外部客戶不能將其看作父類類型,更不可能調(diào)用子類繼承自父類的函數(shù)。

因此,在下面的代碼中,Derived似乎永遠(yuǎn)也無(wú)法被當(dāng)作 Foo::f(Base*) 的參數(shù),以利用Foo所提供的服務(wù)。

struct Base 
{ 
  virtual void f() = 0; 
  virtual ~Base() {} 
}; 

struct Foo 
{ 
  void f(Base* base) 
  { 
    base->f(); 
  } 
};
 
struct Derived : private Base 
{
  // ... 
private:  
  void f() {} 
}; 

Really?你不妨嘗試編譯一下下面的代碼,然后就會(huì)驚訝的發(fā)現(xiàn),竟然是通過(guò)的。

// ... 
struct Derived : private Base 
{ 
  // ... 
  void doSth(Foo* foo) 
  { 
    foo->f(this); 
  } 

private:  
  void f() {} 
};

其實(shí)不必驚訝,這是因?yàn)椋哼@種繼承關(guān)系只是對(duì)外界進(jìn)行了隱藏,但其并沒有消失,只是作為Derived的一個(gè)私人秘密不為外人所知罷了。

在需要的時(shí)候,Derived就可以將這種關(guān)系擺出來(lái),讓別人看在其父類的面子上為其提供服務(wù)。只要這種基于IS-A 關(guān)系的類型轉(zhuǎn)換得以成功,別人才不管你是不是私生子。

這樣的發(fā)現(xiàn),可以在很多場(chǎng)合給我們的設(shè)計(jì)帶來(lái)便利。比如,很多嵌入式設(shè)備都提供了Timer服務(wù)。而Timer模塊要求:用戶必須通過(guò)注冊(cè)一個(gè)接口,以便于當(dāng)指定的時(shí)間到期后,可以進(jìn)行回調(diào)。比如:

struct TimerEventHandler 
{ 
  virtual void onTimeout() = 0; 
  virtual ~TimerEventHandler() {} 
};
 
struct Timer 
{ 
  static void registerHandler
    ( unsigned int timeout 
    , TimerEventHandler* handler);
}; 

然后,一個(gè)類NetworkClient需要Timer服務(wù)。但當(dāng)Timer過(guò)期之后,其需要進(jìn)行的操作都是自己的私有成員。如果創(chuàng)建一個(gè)專門的類以實(shí)現(xiàn)TimerEventHandler,就要么需要使用友元關(guān)系,要么就必須把相關(guān)的成員改為public。無(wú)論怎樣,都會(huì)破壞NetworkClient的封裝性。

這種情況下,讓 NetworkClient直接繼承(實(shí)現(xiàn))TimerEventHandler,就是一個(gè)最恰當(dāng)?shù)倪x擇。同樣,由于我們不希望這層關(guān)系為外界所知,從而產(chǎn)生不必要的依賴,我們應(yīng)選擇私有繼承

struct NetworkClient : private TimerEventHandler 
{ 
  void connect() 
  { 
    server.connect(); 
    Timer::registerHandler(30, this); 
    // ...
  } 

private:  
  // 對(duì) TimerEventHandler 的實(shí)現(xiàn) 
  void onTimeout() 
  { 
    state = STATE_DISCONNECTED;
    sendAlarm(CONNECT_TIMEOUT);
  }

  void sendAlarm(AlarmType alarmType); 

private: 
   State state; 
  // ... 
};

結(jié)論

C++私有繼承是一種非常有趣的關(guān)系。我們?cè)谥皫讉€(gè)大型C++項(xiàng)目里,對(duì)于上述場(chǎng)景下,均經(jīng)常性的使用私有繼承。(估計(jì)我們是C++社區(qū)里使用這個(gè)特性最多的團(tuán)隊(duì):D)。

善用它,可以幫助我們?cè)?strong>設(shè)計(jì)的便利性,信息隱藏,組合方面帶來(lái)諸多好處。私有繼承的美德可以讓它成為軟件設(shè)計(jì)一方之王。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容