智能指針的行為像是指針,但是沒有提供加的功能。例如,Item 13中解釋了如何使用標准auto_ptr和tr1::shared_ptr指針在正確的時間自動刪除堆上的資源。STL容器中的迭代器基本上都是智能指針:當然,你不能通過使用“++”來將鏈表中的指向一個節點的內建指針移到下一個節點上去,但是list::iterator可以這麼做。
真正的指針能夠做好的一件事情是支持隱式轉換。派生類指針可以隱式轉換為基類指針,指向非const的指針可以隱式轉換成為指向const對象的指針,等等。例如,考慮可以在一個三層繼承體系中發生的轉換:
1 class Top { ... }; 2 class Middle: public Top { ... }; 3 class Bottom: public Middle { ... }; 4 Top *pt1 = new Middle; // convert Middle* ⇒ Top* 5 6 Top *pt2 = new Bottom; // convert Bottom* ⇒ Top* 7 8 const Top *pct2 = pt1; // convert Top* ⇒ const Top*
在用戶自定義的智能指針中模仿這種轉換是很微妙的。我們想讓下面的代碼通過編譯:
1 template<typename T> 2 class SmartPtr { 3 public: // smart pointers are typically 4 explicit SmartPtr(T *realPtr); // initialized by built-in pointers 5 ... 6 }; 7 SmartPtr<Top> pt1 = // convert SmartPtr<Middle> ⇒ 8 SmartPtr<Middle>(new Middle); // SmartPtr<Top> 9 SmartPtr<Top> pt2 = // convert SmartPtr<Bottom> ⇒ 10 SmartPtr<Bottom>(new Bottom); // SmartPtr<Top> 11 SmartPtr<const Top> pct2 = pt1; // convert SmartPtr<Top> ⇒ 12 // SmartPtr<const Top>
同一個模板的不同實例之間沒有固有的關系,所以編譯器將SmartPtr<Middle>和SmartPtr<Top>視為完全不同的類,它們之間的關系不比vector<float>和Widget來的近。為了實現SmartPtr類之間的轉換,我們必須顯示的實現。
在上面的智能指針示例代碼中,每個語句都創建了一個新的智能指針對象,所以現在我們把焦點放在如何實現出一個行為表現如我們所願的智能指針構造函數。關鍵的一點是沒有辦法實現我們需要的所有構造函數。在上面的繼承體系中,我們可以用一個SmartPtr<Middle>或一個SmartPtr<Bottom>來構造一個SmartPtr<Top>,但是如果這個繼承體系在未來擴展了,SmartPtr<Top>對象必須能夠從其他智能指針類型中構造出來。例如,如果我們增加了下面的類:
1 class BelowBottom: public Bottom { ... };
我們將會需要支持用SmartPtr<BelowBottom>對象來創建SmartPtr<Top>對象,我們當然不想通過修改SmartPtr模板來實現它。
從原則上來說,我們所需要的構造函數的數量是沒有限制的。既然模板可以被實例化成為沒有限制數量的函數,因此看上去我們不需要一個SmartPtr的構造函數,我們需要的是一個構造函數模板。這樣的模板是成員函數模板(member function templates) (也被叫做member templates)的一個例子——也即是為類產生成員函數的模板:
1 template<typename T> 2 class SmartPtr { 3 public: 4 template<typename U> // member template 5 SmartPtr(const SmartPtr<U>& other); // for a ”generalized 6 7 ... // copy constructor” 8 9 };
這就是說對於每個類型T和每個類型U,一個SmartPtr<T>能夠用SmartPtr<U>創造出來,因為SmartPtr<T>有一個以SmartPtr<U>作為參數的構造函數 。像這樣的構造函數——用一個對象來創建另外一個對象,兩個對象來自於相同的模板但是它們為不同類型(例如,用SmartPtr<U>來創建SmartPtr<T>),它通常被叫做泛化拷貝構造函數(generalized copy constructors)。
上面的泛化拷貝構造函數並沒有被聲明為explicit。這是經過仔細考慮的。內建指針類型之間的類型轉換(例如從派生類轉換到基類指針)是隱式的,並且不需要cast,因此智能指針模仿這種行為就是合理的。在模板化的構造函數上省略explicit正好做到了這一點。
為SmartPtr實現的泛化拷貝構造函數比我們想要的提供了更多的東西。我們想要用SmartPtr<Bottom>創建SmartPtr<Top>,但是我們不想用SmartPtr<Top>創建SmartPtr<Bottom>,因為這違背了public繼承的含義(Item 32)。我們同樣不想用SmartPtr<double>創建SmartPtr<int>,因為沒有從double*到int*之間的隱式轉換。因此,我們必須將成員模板生成的這種成員函數集合剔除掉。
假設SmartPtr遵循auto_ptr和tr1::shared_ptr的設計,也提供一個get成員函數來返回智能指針對象所包含的內建類型指針的一份拷貝(Item 15),我們可以使用構造函數模板的實現來對一些轉換進行限制:
1 template<typename T> 2 class SmartPtr { 3 public: 4 template<typename U> 5 SmartPtr(const SmartPtr<U>& other) // initialize this held ptr 6 : heldPtr(other.get()) { ... } // with other’s held ptr 7 T* get() const { return heldPtr; } 8 ... 9 private: // built-in pointer held 10 T *heldPtr; // by the SmartPtr 11 }
我們在成員初始化列表中用SmartPtr<U>中包含的類型為U*的指針來初始化SmartPtr<T>中的類型為T*的數據成員。這只有在能夠從U*指針到T*指針進行隱式轉換的情況下才能通過編譯,這也正是我們所需要的。實際結果是現在SmartPtr<T>有了一個泛化拷貝構造函數,只有傳遞的參數為兼容類型時才能夠通過編譯。
成員函數模板的使用不僅僅限定在構造函數上。它們的另外一個普通的角色是對賦值的支持。例如,tr1的shared_ptr(Item 13)支持用所有兼容的內建指針來對其進行構造,可以用tr1::shared_ptr,auto_ptr和tr1::weak_ptr(Item 54)來進行構造,對賦值也同樣使用,但是tr1::weak_ptr例外。下面是從tr1的說明中摘錄下來的tr1::shared_ptr的實現,可以看到在聲明模板參數的時候它傾向於使用class而不是typename。(Item 42中描述的,在這個上下文中它們的意義相同。)
1 template<class T> class shared_ptr { 2 public: 3 4 template<class Y> // construct from 5 6 explicit shared_ptr(Y * p); // any compatible 7 8 template<class Y> // built-in pointer, 9 10 11 shared_ptr(shared_ptr<Y> const& r); // shared_ptr, 12 template<class Y> // weak_ptr, or 13 14 explicit shared_ptr(weak_ptr<Y> const& r); // auto_ptr 15 16 template<class Y> 17 18 explicit shared_ptr(auto_ptr<Y>& r); 19 20 template<class Y> // assign from 21 shared_ptr& operator=(shared_ptr<Y> const& r); // any compatible 22 template<class Y> // shared_ptr or 23 shared_ptr& operator=(auto_ptr<Y>& r); // auto_ptr 24 ... 25 };
所有的這些構造函數都是explicit的,除了泛化拷貝構造函數。這就意味著從shared_ptr的一種類型隱式轉換到shared_ptr的另一種類型是允許的,但是內建類型指針和其他的智能指針類型到shared_ptr的隱式轉換是禁止的。(顯示的轉換是可以的(例如通過使用cast))。同樣有趣的是傳遞給tr1::shared_ptr構造函數和賦值運算符的auto_ptr沒有被聲明為const,但是tr1::shared_ptr和tr1::weak_ptr的傳遞卻聲明為const了。這是因為auto_ptr被拷貝的時候已經被修改了(Item 13)。
成員函數模板是美好的東西,但是它們沒有修改語言的基本規則。Item 5解釋了編譯器會自動生成的4個成員函數中的兩個函數為拷貝構造函數和拷貝賦值運算符。Tr1::shared_ptr聲明了一個泛化拷貝構造函數,很清楚的是如果類型T和類型Y是相同的,泛化拷貝構造函數就會被實例化成一個“普通”的拷貝構造函數。那麼編譯器會為tr1::shared_ptr生成一個拷貝構造函數麼?或者說用相同類型的tr1::shared_ptr構造另外一個tr1::shared_ptr的時候,編譯器會實例化泛化拷貝構造函數麼?
正如我所說的,成員模板沒有修改語言的規則。“如果你需要一個拷貝構造函數而你沒有自己聲明,編譯器會為你生成一個”這條規則也是其中之一。在一個類中聲明一個泛化拷貝構造函數(一個member template)不會阻止編譯器生成它們自己的拷貝構造函數(non-template),所以如果你想控制拷貝構造函數的所有方面,你必須同時聲明一個泛化拷貝構造函數和“普通的”構造函數。對於賦值同樣適用。下面是tr1::shared_ptr的定義:
1 template<class T> class shared_ptr { 2 public: 3 shared_ptr(shared_ptr const& r); // copy constructor 4 5 template<class Y> // generalized 6 7 8 9 shared_ptr(shared_ptr<Y> const& r); // copy constructor 10 11 shared_ptr& operator=(shared_ptr const& r); // copy assignment 12 13 template<class Y> // generalized 14 15 16 shared_ptr& operator=(shared_ptr<Y> const& r); // copy assignment 17 ... 18 };