Guru of the Week 條款07:編譯期的依賴性 (轉)

worldblog發表於2007-12-10
Guru of the Week 條款07:編譯期的依賴性 (轉)[@more@] 

GotW #07 Compile-Time Dependencies

著者:Herb Sutter 

翻譯:kingofark

[宣告]:本文內容取自網站上的Guru of the Week欄目,其著作權歸原著者本人所有。譯者kingofark在未經原著者本人同意的情況下翻譯本文。本翻譯內容僅供自學和參考用,請所有閱讀過本文的人不要擅自轉載、傳播本翻譯內容;本翻譯內容的人請在閱讀瀏覽後,立即刪除其。譯者kingofark對違反上述兩條原則的人不負任何責任。特此宣告。

Revision 1.0

Guru of the Week 條款07:編譯期的依賴性:namespace prefix = o ns = "urn:schemas--com::office" />

 

難度:7 / 10

 

(大多數員使用#include包含的頭都比實際需要的多。你也是這樣的嗎?想知道的話,請看本條款。)

 

 

[問題]

 

[注意:這個問題比想象的還要難!下面程式中的註釋都是非常有用的。]

 

大多數程式設計師使用#include包含的標頭檔案都比實際需要的要多。這會嚴重的影響並延長程式的建立時間(build time),特別是當一個被頻繁使用的標頭檔案中包含了太多其它的標頭檔案的時候,問題越發嚴重。

 

首先,在下面的標頭檔案當中,有哪些#include語句可以在不對程式產生副作用的情況下被直接去掉?其次,還有哪些#include語句可以在對程式進行適當的修改之後被去掉?程式將如何修改?(你不能改變X類和Y類的公共介面;也就是說,你對這個標頭檔案所作的任何修改都不能影響它的程式碼)。

 

  // gotw007.h (implementation file is gotw007.cpp)


  //


  #include "a.h"  // class A


  #include "b.h"  // class B


  #include "c.h"  // class C


  #include "d.h"  // class D


  // (注意: 只有A和C有虛擬(virtual functions))


  #include


  #include


  #include


  #include


  #include


 


  class X : public A {


  public:


  X  ( const C& );


  D  Function1( int, char* );


  D  Function1( int, C );


  B&  Function2( B );


  void Function3( std::wostringstream& );


  std::ostream& print( std::ostream& ) const;


  private:


  std::string  name_;


  std::list clist_;


  D  d_;


  };


  std::ostream& operator<

  { return x.print(os); }


 


  class Y : private B {


  public:


  C  Function4( A );


  private:


  std::list<:wostringstream> alist_;


  };


 

 

[解答]

 

首先,我們考慮那些可以被直接去掉的#include語句(或者說是標頭檔案)。為了便於檢視,我們再把原始的程式碼列在下面:

 

  // gotw007.h (其實現檔案為gotw007.cpp)


  //


  #include "a.h"  // class A


  #include "b.h"  // class B


  #include "c.h"  // class C


  #include "d.h"  // class D


  // (注意: 只有A和C有虛擬函式(virtual functions))


  #include


  #include


  #include


  #include


  #include


 


  class X : public A {


  public:


  X  ( const C& );


  D  Function1( int, char* );


  D  Function1( int, C );


  B&  Function2( B );


  void Function3( std::wostringstream& );


  std::ostream& print( std::ostream& ) const;


  private:


   std::string  name_;


  std::list clist_;


  D  d_;


  };


  std::ostream& operator<

  { return x.print(os); }


 


  class Y : private B {


  public:


  C  Function4( A );


  private:


  std::list<:wostringstream> alist_;


  };


 

1.  我們可以直接去掉的標頭檔案有:

 

l  iostream,因為程式裡儘管用到了流,但並沒有用到iostream裡特定的東西。

l  ostream和sstream,因為程式中的引數和返回型別被前置宣告(forward-declared)是可以的,所以其實只需要iowd就夠了(要注意,並沒有與iosfwd相對應的諸如stringfwd或者listfwd之類的標準標頭檔案;iosfwd是考慮到向下相容性問題的產物,它使得以前那些不支援模板的流子的程式碼仍然可用而不需要修改或者重寫。)

 

我們不能直接去掉的標頭檔案有:

 

l  a.h,因為A是X的基類。

l  b.h,因為B是Y的基類。

l  c.h,因為現有的許多需要list能夠看見對C的定義(這些編譯器應該在未來的版本中修正這一點。)

l  d.h,list和string,因為X需要知道D和string的大小,X和Y都需要知道list的大小。

 

 

其次,我們再來考查那些可以透過隱藏X和Y的實現細節來被去掉的#include語句(或者說是標頭檔案):

 

2.  我們可以透過讓X和Y使用pimpl_的方法來去掉d.h,list和string(也就是說,其私有部分被一個指標代替,這個指標指向型別被前置宣告(forward-declared)的實體),因為這時,X和Y都不再需要知道list、D或者string的大小。這也使我們可以幹掉c.h,因為這時在X::clist中的C物件只作為引數或者返回值出現。

 

重要的事項:即使ostream沒有被定義,內聯的operator<

 

最後,我們來看一下那些可以透過其它微小的修改而去掉的標頭檔案:

 

3.  我們注意到B是Y的private基類,而且B沒有虛擬函式(virtual function),因此b.h也是有可能被去掉的。有一個(也只有一個)主要的原因使我們在派生類的時候使用private繼承,那就是想要過載(overr)虛擬函式(virtual function)。如此看來,在這裡與其讓Y繼承自B,還不如讓Y擁有一個型別為B的成員。要去掉b.h,我們應該讓Y的這個型別為B的成員存在於Y中隱藏的pimpl_部分。

 

[學習指導]:請使用pimpl_把程式碼的呼叫者與程式碼的實現細節隔離開來。

 

摘錄自GotW的編碼標準:

 

—  封裝(encapsulation)和隔離(insulation):

— 在宣告一個類的時候,應避免暴露出其私有成員:

— 應該使用一個形如“struct Xxxxlmpl* pimpl_”的不透明的指標來私有成員(包括狀態變數和成員函式),例如:

class Map {private: struct Maplmpl* pimpl_;};

(Lakos96: 398-405; Meyers92: 111-116; Murray93: 72-74)

 

4.  基於以下幾個原因,我們目前還不能夠對a.h動手腳:A被用作public基類;A含有虛擬函式(virtual function),因而其IS-A關係可能會被程式碼的呼叫者所使用。然而我們注意到,X和Y兩個類之間沒有任何關係,因此我們至少可以把X和Y的定義分別放到兩個不同的標頭檔案中間去(為了不影響現有的程式碼,我們還應該把現有的標頭檔案作為一個存根(stub),讓其用#include包含x.h和y.h)。如此以來,我們至少可以讓y.h不用#include包含a.h,因為現在它只把A用作函式引數的型別,不需要A的定義。

 

 

綜上所述,我們現在可以得到一個清爽的標頭檔案了:

 

  //---------------------------------------------------------------


  // 新檔案x.h: 只包含兩個#include!


  //


  #include "a.h"  // class A


  #include


 


 class C;


  class D;


 


  class X : public A {


  public:


  X  ( const C& );


  D  Function1( int, char* );


  D  Function1( int, C );


  B&  Function2( B );


  void Function3( std::wostringstream& );


  std::ostream& print( std::ostream& ) const;


  private:


  class XImpl* pimpl_;


  };


 


  inline std::ostream& operator<

  { return x.print(os); }


  // 注意: 這裡不需要ostream的定義!


 


 


  //---------------------------------------------------------------


  // 新檔案y.h: 沒有#include!


  //


  class A;


  class C;


 


  class Y {


  public:


  C  Function4( A );


  private:


  class YImpl* pimpl_;


  };


 


 


  //---------------------------------------------------------------


  // gotw007.h 作為存根包含兩個#include,又透過x.h附帶了另外兩個#include)


  //


  #include "x.h"


  #include "y.h"


 


 


  //---------------------------------------------------------------


  // gotw007.cpp中的新結構... 注意:impl 物件應該在X和Y的建構函式中


  // 用new來建立,並在X和Y的解構函式中用delete來清除。


  // X和Y 的成員函式要透過pimpl_ 指標來訪問資料


  //


  struct XImpl  // 是的, 我們可以用"struct" ,雖然前置宣告的時候


  {  // 我們用了"class"


  std::string  name_;


  std::list clist_;


  D  d_;


  }


 


  struct YImpl


  {


  std::list<:wostringstream> alist_;


  B b_;


  }


 

最後說幾句:到現在,X的使用者只需要用#include包含a.h和iosfwd就可以了。而Y的使用者也只需要包含a.h和iosfwd,即使後來為了程式碼而需要包含y.h並去掉gotw007.h,也照樣是一行#include都不用多加。與原來的程式相比,這是多麼大的改進呀!


來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/10752043/viewspace-990891/,如需轉載,請註明出處,否則將追究法律責任。

相關文章