午夜视频在线网站,日韩视频精品在线,中文字幕精品一区二区三区在线,在线播放精品,1024你懂我懂的旧版人,欧美日韩一级黄色片,一区二区三区在线观看视频

分享

pimpl idiom in c++

 答案95 2016-01-27

 Introduction

在C++里面, 經(jīng)常出現(xiàn)的情況就是頭文件里面的類定義太龐大了,而這個(gè)類的成員變量涉及了很多 其他文件里面的類,從而導(dǎo)致了其他引用這個(gè)類的文件也依賴于這些成員變量的定義。 在這種情況下,就出現(xiàn)了在C++里面特有的一個(gè)idiom,叫做Pimpl idiom。

考慮一下下面的情況,假設(shè)有一個(gè)類A,它包含了成員變量b和c,類型分別為B和C,而如果D類 要使用A類的話,那也變相依賴了B和C。如下:

  1. 1 #include "B.h"
  2. 2 #include "C.h"
  3. 3
  4. 4 class A
  5. 5 {
  6. 6 private:
  7. 7 B b;
  8. 8 C c;
  9. 9 };

這個(gè)時(shí)候如果D要使用A類的話,那么D就要像下面那樣去寫:

  1. 1 #include "A.h"
  2. 2
  3. 3 class D
  4. 4 {
  5. 5 private:
  6. 6 A a;
  7. 7 };

雖然形式上是只需要include A.h,但是在鏈接程序的時(shí)候,卻需要把B和C的模塊也一并鏈接進(jìn)去。

初步的解決方案可以是把A里面的b和c變成指針類型,然后利用指針聲明的時(shí)候類型可以是不完全類型, 從而在A.h里面不用include B.h和C.h。當(dāng)然,這也只是解決的部分的問題。 如果A里面需要用到十幾個(gè)成員變量的話,這個(gè)時(shí)候頭文件的size就會(huì)變得很大,這也是一個(gè)問題。 而且有些時(shí)候,變成指針類型也不一定是可行的。這個(gè)時(shí)候,一個(gè)簡(jiǎn)單的想法就是把所有私有的 成員變量的聲明都放到cpp文件里面去,這樣使用A的類就可以完全不用知道A類的成員變量了。

Pimpl Idiom

而Pimpl idiom就是這樣的解決方案。所謂的Pimpl idiom,就是聲明一個(gè)類中類, 然后再聲明一個(gè)成員變量,類型是這個(gè)類中類的指針。用上面的例子來(lái)說(shuō)明一下會(huì)清楚一下, 代碼如下:

  1. 1 class A
  2. 2 {
  3. 3 private:
  4. 4 struct Pimpl;
  5. 5 Pimpl* m_pimpl;
  6. 6 };

有了上面的定義,那么D類就可以完全不用知道A類的細(xì)節(jié),而且鏈接的時(shí)候也可以完全不用管B和C了。 然后在A.cpp里面,我們就像下面這樣去定義就好了:

  1. 1 struct A::Pimpl
  2. 2 {
  3. 3 B b;
  4. 4 C c;
  5. 5 };
  6. 6
  7. 7 A::A()
  8. 8 : m_pimpl(new Pimpl)
  9. 9 {
  10. 10 m_impl->b; // 使用b
  11. 11 }

而現(xiàn)在我們STL有auto_ptr,boost有shared_ptr,再要自己來(lái)管理內(nèi)存好像 就有寫多次一舉了。所以在Herb Sutter的Using auto_ptr Effectively里面, 也提到了用auto_ptr來(lái)進(jìn)行“經(jīng)典”的Pimpl的編寫。

也就是如下面這樣:

  1. 1 #include <memory>
  2. 2
  3. 3 class A
  4. 4 {
  5. 5 public:
  6. 6 A();
  7. 7
  8. 8 private:
  9. 9 struct Pimpl;
  10. 10 std::auto_ptr<Pimpl> m_pimpl;
  11. 11 };

可以當(dāng)你寫了上面的代碼之后,編譯,Bang! 編譯器給你報(bào)一個(gè)錯(cuò),說(shuō)是Pimpl是incomplete type。這下你就蒙了吧?!

其實(shí)要fix上面的編譯錯(cuò)誤,你只需要加上A的destructor的聲明,然后在cpp文件里面實(shí)現(xiàn)一個(gè) 空的destructor就可以了。

但是這個(gè)是為什么呢?

auto_ptr的模板特化

其實(shí)上面問題的原因,是跟模板特化的這個(gè)C++變態(tài)特性有關(guān)的。

我們先來(lái)看一下auto_ptr的簡(jiǎn)化定義:

  1. 1 template <typename T>
  2. 2 class auto_ptr
  3. 3 {
  4. 4 public:
  5. 5 auto_ptr()
  6. 6 : m_ptr(NULL)
  7. 7 {}
  8. 8
  9. 9 auto_ptr(T* p)
  10. 10 : m_ptr(p)
  11. 11 {}
  12. 12
  13. 13 ~auto_ptr()
  14. 14 {
  15. 15 if (m_ptr)
  16. 16 {
  17. 17 delete m_ptr;
  18. 18 }
  19. 19 }
  20. 20
  21. 21 private:
  22. 22 T* m_ptr;
  23. 23 };

我們看到auto_ptr在他的構(gòu)造函數(shù)里面自動(dòng)的delete了他的m_ptr,這個(gè)就是比較經(jīng)典的 利用RAII實(shí)現(xiàn)的智能指針了。

然后還要知道,auto_ptr是一個(gè)模板類,而模板類的一個(gè)特點(diǎn)是, 當(dāng)他的成員函數(shù)只有在被調(diào)用的時(shí)候才會(huì)真正的做函數(shù)特化。

也就是說(shuō),如果有下面的這樣一個(gè)模板類:

  1. 1 template <typename T>
  2. 2 class TemplateClass
  3. 3 {
  4. 4 public:
  5. 5 void Foo()
  6. 6 {
  7. 7 int a = 1;
  8. 8 return;
  9. 9 }
  10. 10
  11. 11 void Bar()
  12. 12 {
  13. 13 this->m_ptr = "syntax correct, but semantic incorrect.";
  14. 14 }
  15. 15 };
  16. 16
  17. 17 int main(int argc, char *argv[])
  18. 18 {
  19. 19 TemplateClass<int> a;
  20. 20 a.Foo();
  21. 21 return 0;
  22. 22 }

上面的代碼,是可以通過(guò)編譯并且正確運(yùn)行的??梢钥吹紽oo這個(gè)函數(shù)是正確的,而Bar函數(shù)雖然 語(yǔ)法上是正確的,但是他的語(yǔ)義是錯(cuò)的。但是由于我們只調(diào)用了Foo,沒有調(diào)用Bar, 所以只有Foo被真正的特化并且做了完全的編譯,而Bar只是做了語(yǔ)法上的檢查, 并沒有做語(yǔ)義的檢查。所以上面的代碼在C++里面是100%的正確的。

所以auto_ptr里面的成員函數(shù),包括構(gòu)造和析構(gòu)函數(shù),都是在被調(diào)用的時(shí)候才進(jìn)行真正的特化。

Default Destructor

還記得在學(xué)C++的剛開始的時(shí)候書上這么說(shuō)過(guò),不定義構(gòu)造函數(shù)或者析構(gòu)函數(shù), 那么編譯器會(huì)幫我們?cè)煲粋€(gè)默認(rèn)的。而這個(gè)默認(rèn)的構(gòu)造或者析構(gòu)函數(shù)只會(huì)做成員變量還有父類的 默認(rèn)初始化或者析構(gòu),其他什么都不會(huì)做。

那么我們看回利用了Pimpl的A的定義。在這個(gè)定義里面,由于我沒有寫析構(gòu)函數(shù)的聲明, 所以編譯器自動(dòng)幫我定義了一個(gè)。而A里面有一個(gè)auto_ptr成員變量,所以在這個(gè)默認(rèn)的 析構(gòu)函數(shù)里面會(huì)析構(gòu)這個(gè)成員變量。所謂的析構(gòu),其實(shí)就是調(diào)用析構(gòu)函數(shù)而已。 所以,在這個(gè)默認(rèn)的析構(gòu)函數(shù)里面,調(diào)用了auto_ptr的析構(gòu)函數(shù),這個(gè)時(shí)候, auto_ptr的析構(gòu)函數(shù)就被編譯器特化了。

而在auto_ptr的析構(gòu)函數(shù)里面,delete了模板參數(shù)的指針類型的成員變量。 而在A這個(gè)例子里面,模板參數(shù)就是Pimpl。而在特化的這一瞬間,Pimpl是被聲明了, 但是還沒有被定義。

所以例子里面的A在經(jīng)過(guò)編譯后是和下面的代碼等價(jià)的:

  1. 1 class A
  2. 2 {
  3. 3 public:
  4. 4 A();
  5. 5 ~A()
  6. 6 {
  7. 7 ~auto_ptr<Pimpl>(m_pimpl);
  8. 8 }
  9. 9
  10. 10 private:
  11. 11 struct Pimpl;
  12. 12 std::auto_ptr<Pimpl> m_pimpl;
  13. 13 };
  14. 14
  15. 15 auto_ptr<Pimpl>::~auto_ptr()
  16. 16 {
  17. 17 delete m_ptr; // m_ptr的類型是Pimpl*
  18. 18 }

那為什么當(dāng)我加上A的析構(gòu)函數(shù)的聲明之后,編譯就可以通過(guò)呢?因?yàn)楫?dāng)我們聲明了A的析構(gòu)函數(shù)之后, 編譯器就不會(huì)自動(dòng)生成析構(gòu)函數(shù)的實(shí)現(xiàn)了,而由于我們會(huì)在cpp文件里面去寫析構(gòu)函數(shù)的實(shí)現(xiàn), 而在此之前,我們就會(huì)在cpp文件的開頭定義好Pimpl的實(shí)現(xiàn)。所以當(dāng)我們自己寫的A的析構(gòu)函數(shù) 被編譯器看見的時(shí)候,Pimpl就是一個(gè)已經(jīng)定義好的類型,所以就沒有問題了。

Pimpl by boost::shared_ptr

其實(shí)使用auto_ptr來(lái)實(shí)現(xiàn)Pimpl Idiom并不是唯一的方法,Pimpl還可以用 boost::scoped_ptr和boost::shared_ptr來(lái)實(shí)現(xiàn)。而scoped_ptr和auto_ptr 其實(shí)是一樣的,也是需要用戶手工的聲明一個(gè)析構(gòu)函數(shù)來(lái)實(shí)現(xiàn)Pimpl Idiom,這里就不說(shuō)了。

但是通過(guò)shared_ptr來(lái)實(shí)現(xiàn)的話,我們就連析構(gòu)函數(shù)都可以省略!也就是說(shuō), 如果我寫下面的代碼,是完全正確的:

  1. 1 class A
  2. 2 {
  3. 3 public:
  4. 4 A();
  5. 5
  6. 6 private:
  7. 7 struct Pimpl;
  8. 8 boost::shared_ptr<Pimpl> m_pimpl;
  9. 9 };

需要注意的是,雖然析構(gòu)函數(shù)可以省略,但是構(gòu)造函數(shù)還是必須明確聲明的。 這又是為什么呢?為什么auto_ptr不行,但是shared_ptr就可以呢?

答案就在shared_ptr的實(shí)現(xiàn)里面。

相信shared_ptr應(yīng)該是每個(gè)較為深入學(xué)過(guò)C++的人都會(huì)理解原理的一個(gè)類了,其中shared_ptr 的實(shí)現(xiàn)又可以分為侵入式和非侵入式的,而boost::shared_ptr的實(shí)現(xiàn)是非侵入式的。 也就是說(shuō)要用shared_ptr的類不需要任何改動(dòng)就可以使用了。

來(lái)看看簡(jiǎn)化之后的shared_ptr的實(shí)現(xiàn)吧:

  1. 1 class sp_counted_base
  2. 2 {
  3. 3 public:
  4. 4 virtual ~sp_counted_base(){}
  5. 5 };
  6. 6
  7. 7 template<typename T>
  8. 8 class sp_counted_base_impl : public sp_counted_base
  9. 9 {
  10. 10 public:
  11. 11 sp_counted_base_impl(T *t):t_(t){}
  12. 12 ~sp_counted_base_impl(){delete t_;}
  13. 13 private:
  14. 14 T *t_;
  15. 15 };
  16. 16
  17. 17
  18. 18 class shared_count
  19. 19 {
  20. 20 public:
  21. 21 static int count_;
  22. 22 template<typename T>
  23. 23 shared_count(T *t):
  24. 24 t_(new sp_counted_base_impl<T>(t))
  25. 25 {
  26. 26 count_ ++;
  27. 27 }
  28. 28 void release()
  29. 29 {
  30. 30 --count_;
  31. 31 if(0 == count_) delete t_;
  32. 32 }
  33. 33 ~shared_count()
  34. 34 {
  35. 35 release();
  36. 36 }
  37. 37 private:
  38. 38 sp_counted_base *t_;
  39. 39 };
  40. 40 int shared_count::count_(0);
  41. 41
  42. 42 template<typename T>
  43. 43 class myautoptr
  44. 44 {
  45. 45 public:
  46. 46 template<typename Y>
  47. 47 myautoptr(Y* y):sc_(y),t_(y){}
  48. 48 ~myautoptr(){ sc_.release();}
  49. 49 private:
  50. 50 shared_count sc_;
  51. 51 T *t_;
  52. 52 };
  53. 53
  54. 54 int main()
  55. 55 {
  56. 56 myautoptr<A> a(new B);
  57. 57 }

從上面的代碼可以看到,shared_ptr里面不單存了一個(gè)模板類型的指針, 還存了一個(gè)shared_count。 這個(gè)shared_count的作用就是用來(lái)作為引用計(jì)數(shù)還有自動(dòng)管理指針用的。 而shared_count里面又存了一個(gè)sp_counted_base,而sp_counted_base_impl 是一個(gè)模板類,其繼承于sp_counted_base。這其實(shí)是一個(gè)模板技巧,也就是聲明一個(gè) 通用的基類,然后定義一個(gè)模板類來(lái)繼承于這個(gè)基類,而其他類通過(guò)基類的指針來(lái)使用這個(gè)模板類, 這樣就可以在編譯時(shí)確定一些類型信息,而同時(shí)把一些通用的實(shí)現(xiàn)細(xì)節(jié)推遲到運(yùn)行時(shí)。這句話什么意思呢? 看完接下來(lái)的解釋你就明白了。

接下來(lái)我們又要注意到,shared_ptr和shared_count的構(gòu)造函數(shù)都是模板成員函數(shù), 模板類型由參數(shù)決定,而這個(gè)技巧和上面的模板繼承技巧組合在一起,就是這節(jié)開始的時(shí)候, 例子中不用寫析構(gòu)函數(shù)的理由。

首先,當(dāng)我們聲明一個(gè)shared_ptr<int>的時(shí)候,它只是把里面的t_成員給特化了, 而shared_count里面存的是什么類型的指針仍然沒有確定。

而當(dāng)我們調(diào)用shared_ptr<int>(new int(3))的時(shí)候,他就調(diào)用了shared_ptr的構(gòu)造函數(shù)。 這個(gè)時(shí)候就特化了模板構(gòu)造函數(shù),然后這個(gè)構(gòu)造函數(shù)里面又調(diào)用了shared_count的構(gòu)造函數(shù), 所以shared_count的構(gòu)造函數(shù)也被特化,而又同時(shí)特化了sp_counted_base_impl, 這個(gè)時(shí)候里面的指針就完全被特化了。

而我們看到,在shared_ptr被析構(gòu)的時(shí)候,它調(diào)用的是shared_count的release函數(shù), release函數(shù)里面又delete了它的類型為sp_counted_base的指針, 所以調(diào)用的是sp_counted_base的析構(gòu)函數(shù)(虛函數(shù))。因?yàn)槭翘摵瘮?shù),當(dāng)具體類型確定之后, 是會(huì)具體調(diào)用到具體的析構(gòu)函數(shù)的。但是在編譯的時(shí)候,不需要知道具體的類型。

說(shuō)了那么多,其實(shí)就是一句話,調(diào)用shared_ptr的析構(gòu)函數(shù)的時(shí)候,它不需要知道具體的指針類型。 也就是說(shuō)這個(gè)類型即使incomplete也沒有關(guān)系。而在調(diào)用shared_ptr的構(gòu)造函數(shù)的時(shí)候, shared_ptr就是會(huì)知道這個(gè)類型的所有信息,從而使得delete的時(shí)候調(diào)用到具體的析構(gòu)函數(shù)。

所以對(duì)于shared_ptr來(lái)說(shuō),構(gòu)造函數(shù)需要知道所有的類型信息,而析構(gòu)函數(shù)是不要知道類型信息的。 回到例子里面,當(dāng)我們不聲明析構(gòu)函數(shù)的時(shí)候,編譯器為我們定義了一個(gè)默認(rèn)的析構(gòu)函數(shù), 這個(gè)時(shí)候shared_ptr的析構(gòu)函數(shù)就會(huì)被特化并定義,同時(shí)也調(diào)用sp_counted_base 的析構(gòu)函數(shù)也就被編譯了。但是這個(gè)時(shí)候并不許要具體的類型信息, 所以類型是incomplete也是可以的。當(dāng)我們定義A的構(gòu)造函數(shù)的時(shí)候,這個(gè)時(shí)候shared_ptr 的構(gòu)造函數(shù)就被特化,從而shared_count的構(gòu)造函數(shù)被特化,而sp_counted_base_impl 也就是被特化了。這個(gè)時(shí)候shared_ptr也就有了所有必要的類型信息, 他的析構(gòu)函數(shù)就可以正常的工作了。

這就是為什么用shared_ptr來(lái)實(shí)現(xiàn)Pimpl可以不用寫析構(gòu)函數(shù)的原因了, 為了實(shí)現(xiàn)這個(gè)功能,shared_ptr犧牲了一點(diǎn)點(diǎn)的空間來(lái)完成上面的概念,比普通的shared_ptr 多了一個(gè)sizeof(sp_counted_base*)的大小。

    本站是提供個(gè)人知識(shí)管理的網(wǎng)絡(luò)存儲(chǔ)空間,所有內(nèi)容均由用戶發(fā)布,不代表本站觀點(diǎn)。請(qǐng)注意甄別內(nèi)容中的聯(lián)系方式、誘導(dǎo)購(gòu)買等信息,謹(jǐn)防詐騙。如發(fā)現(xiàn)有害或侵權(quán)內(nèi)容,請(qǐng)點(diǎn)擊一鍵舉報(bào)。
    轉(zhuǎn)藏 分享 獻(xiàn)花(0

    0條評(píng)論

    發(fā)表

    請(qǐng)遵守用戶 評(píng)論公約

    類似文章 更多