02 | 自己動手,實現C++的智慧指標
第一步:針對單獨型別的模板
為了完成智慧指標首先第一步的想法。
class shape_wrapper { public: explicit shape_wrapper( shape* ptr = nullptr) : ptr_(ptr) {} ~shape_wrapper() { delete ptr_; } shape* get() const { return ptr_; } private: shape* ptr_; };
這個類可以完成智慧指標的最基本的功能:對超出作用域的物件進行釋放。
但是
1.這個類只適用於 shape 類
2.該類物件的行為不夠像指標
3.拷貝該類物件會引發程式行為異常
要讓這個類能夠包裝任意型別的指標,我們需要把它變成一個類模板
第二步:變成類模板
template <typename T> class smart_ptr { public: explicit smart_ptr(T* ptr = nullptr) : ptr_(ptr) {} ~smart_ptr() { delete ptr_; } T* get() const { return ptr_; } private: T* ptr_; };
解決像不像指標的問題:
它不能用 * 運算子解引用
它不能用 -> 運算子指向物件成員
它不能像指標一樣用在布林表示式裡
template <typename T> class smart_ptr { public: … T& operator*() const { return *ptr_; } T* operator->() const { return ptr_; } operator bool() const { return ptr_; } }
第三步:拷貝構造和賦值
第一種粗魯的方式
解決了會對同一記憶體釋放兩次,通常情況下會導致程式崩潰的問題。
template <typename T> class smart_ptr { … smart_ptr(const smart_ptr&) = delete; smart_ptr& operator=(const smart_ptr&) = delete; … };
第二種移動所有權
大致實現如下
template <typename T> class smart_ptr { … smart_ptr(smart_ptr& other) { ptr_ = other.release(); } smart_ptr& operator=(smart_ptr& rhs) { smart_ptr(rhs).swap(*this); return *this; } … T* release() { T* ptr = ptr_; ptr_ = nullptr; return ptr; } void swap(smart_ptr& rhs) { using std::swap; swap(ptr_, rhs.ptr_); } … };
在拷貝建構函式中,通過呼叫 other 的 release 方法來釋放它對指標的所有權。在賦值函式中,則通過拷貝構造產生一個臨時物件並呼叫 swap 來交換對指標的所有權。實現上是不復雜的。
如果你學到的賦值函式還有一個類似於 if (this != &rhs) 的判斷的話,那種用法更囉嗦,而且異常安全性不夠好——如果在賦值過程中發生異常的話,this 物件的內容可能已經被部分破壞了,物件不再處於一個完整的狀態。
上面程式碼裡的這種慣用法則保證了強異常安全性:賦值分為拷貝構造和交換兩步,異常只可能在第一步發生;而第一步如果發生異常的話,this 物件完全不受任何影響。無論拷貝構造成功與否,結果只有賦值成功和賦值沒有效果兩種狀態,而不會發生因為賦值破壞了當前物件這種場景。
還是不夠完善,沒有支援移動語義,於是繼續進行修改如下
template <typename T> class smart_ptr { … smart_ptr(smart_ptr&& other) { ptr_ = other.release(); } smart_ptr& operator=(smart_ptr rhs) { rhs.swap(*this); return *this; } … };
修改的地方:
1.把拷貝建構函式中的引數型別 smart_ptr& 改成了 smart_ptr&&;現在它成了移動建構函式。
2.把賦值函式中的引數型別 smart_ptr& 改成了 smart_ptr,在構造引數時直接生成新的智慧指標,從而不再需要在函式體中構造臨時物件。現在賦值函式的行為是移動還是拷貝,完全依賴於構造引數時走的是移動構造還是拷貝構造。
根據 C++ 的規則,如果我提供了移動建構函式而沒有手動提供拷貝建構函式,那後者自動被禁用
到這裡我們完成了一個C++11 的 unique_ptr 的基本行為。
第四步:unique_ptr的完善:子類指標向基類指標的轉換
直接上程式碼,利用內建型別自己的判斷來輔助完成轉換
template <typename U> smart_ptr(smart_ptr<U>&& other) { ptr_ = other.release(); }
到這裡我們完成了一個完整的unique_ptr的轉換
第五步:unique_ptr轉變為shared_ptr
多個不同的 shared_ptr 不僅可以共享一個物件,在共享同一物件時也需要同時共享同一個計數。當最後一個指向物件(和共享計數)的 shared_ptr 析構時,它需要刪除物件和共享計數。我們下面就來實現一下
我們先來寫出共享計數的介面:
class shared_count { public: shared_count(); void add_count(); long reduce_count(); long get_count() const; };
class shared_count { public: shared_count() : count_(1) {} void add_count() { ++count_; } long reduce_count() { return --count_; } long get_count() const { return count_; } private: long count_; };
大體框架:建構函式、解構函式和私有成員變數
template <typename T> class smart_ptr { public: explicit smart_ptr(T* ptr = nullptr) : ptr_(ptr) { if (ptr) { shared_count_ = new shared_count(); } } ~smart_ptr() { if (ptr_ && !shared_count_ ->reduce_count()) { delete ptr_; delete shared_count_; } } private: T* ptr_; shared_count* shared_count_; };
建構函式跟之前的主要不同點是會構造一個 shared_count 出來。解構函式在看到 ptr_ 非空時(此時根據程式碼邏輯,shared_count 也必然非空),需要對引用數減一,並在引用數降到零時徹底刪除物件和共享計數。原理就是這樣,不復雜。
當然,我們還有些細節要處理。為了方便實現賦值(及其他一些慣用法),我們需要一個新的 swap 成員函式:
void swap(smart_ptr& rhs) { using std::swap; swap(ptr_, rhs.ptr_); swap(shared_count_, rhs.shared_count_); }
賦值函式可以跟前面一樣,保持不變,但拷貝構造和移動建構函式是需要更新一下的:
smart_ptr(const smart_ptr& other) { ptr_ = other.ptr_; if (ptr_) { other.shared_count_ ->add_count(); shared_count_ = other.shared_count_; } } template <typename U> smart_ptr(const smart_ptr<U>& other) { ptr_ = other.ptr_; if (ptr_) { other.shared_count_ ->add_count(); shared_count_ = other.shared_count_; } } template <typename U> smart_ptr(smart_ptr<U>&& other) { ptr_ = other.ptr_; if (ptr_) { shared_count_ = other.shared_count_; other.ptr_ = nullptr; } }
除複製指標之外,對於拷貝構造的情況,我們需要在指標非空時把引用數加一,並複製共享計數的指標。對於移動構造的情況,我們不需要調整引用數,直接把 other.ptr_ 置為空,認為 other 不再指向該共享物件即可。
不過,上面的程式碼有個問題:它不能正確編譯。編譯器會報錯,像:
fatal error: ‘ptr_’ is a private member of ‘smart_ptr<circle>’
錯誤原因是模板的各個例項間並不天然就有 friend 關係,因而不能互訪私有成員 ptr_ 和 shared_count_。我們需要在 smart_ptr 的定義中顯式宣告:
template <typename U> friend class smart_ptr;
第六步:指標型別轉換
對應於 C++ 裡的不同的型別強制轉換:我們能不能讓我們的智慧指標同樣支援這種轉換?
static_cast reinterpret_cast const_cast dynamic_cast
智慧指標需要實現類似的函式模板。實現本身並不複雜,但為了實現這些轉換,我們需要新增建構函式,允許在對智慧指標內部的指標物件賦值時,使用一個現有的智慧指標的共享計數。如下所示:
我們希望它達成的效果是:
template <typename T, typename U> smart_ptr<T> dynamic_pointer_cast( const smart_ptr<U>& other) { T* ptr = //取出具體的指標 dynamic_cast<T*>(other.get()); return smart_ptr<T>(other, ptr); //再根據具體的指標來構造目標,藉助建構函式來完成 }
補充一個建構函式
template <typename U> smart_ptr(const smart_ptr<U>& other, T* ptr) { ptr_ = ptr; if (ptr_) { other.shared_count_ ->add_count(); shared_count_ = other.shared_count_; } }
總結程式碼
#include <utility> // std::swap class shared_count { // 引用計數類 public: shared_count() noexcept : count_(1) {} void add_count() noexcept { ++count_; } long reduce_count() noexcept { return --count_; } long get_count() const noexcept { return count_; } private: long count_; }; template <typename T> // 有計數功能的智慧指標 class smart_ptr { public: template <typename U> friend class smart_ptr;//模板類自身friend explicit smart_ptr(T* ptr = nullptr) : ptr_(ptr) { if (ptr) { // 如果指標為空,則不建立計數類 shared_count_ = new shared_count(); } } ~smart_ptr() { if (ptr_ && !shared_count_ ->reduce_count()) { delete ptr_; delete shared_count_; } } smart_ptr(const smart_ptr& other) // 相同指標類的拷貝賦值 { ptr_ = other.ptr_; if (ptr_) { other.shared_count_ ->add_count(); shared_count_ = other.shared_count_; } } template <typename U> smart_ptr(const smart_ptr<U>& other) noexcept // 不同指標類的拷貝賦值 { ptr_ = other.ptr_; if (ptr_) { other.shared_count_->add_count(); shared_count_ = other.shared_count_; } } template <typename U> smart_ptr(smart_ptr<U>&& other) noexcept // 移動賦值函式 { ptr_ = other.ptr_; if (ptr_) { shared_count_ = other.shared_count_; other.ptr_ = nullptr;// 設定為nullptr } } template <typename U> smart_ptr(const smart_ptr<U>& other, // 建構函式,經過驗證可以動態轉換之後的呼叫 T* ptr) noexcept { ptr_ = ptr; if (ptr_) { other.shared_count_ ->add_count(); shared_count_ = other.shared_count_; } } smart_ptr& operator=(smart_ptr rhs) noexcept { rhs.swap(*this); return *this; } T* get() const noexcept { return ptr_; } long use_count() const noexcept { if (ptr_) { return shared_count_ ->get_count(); } else { return 0; } } void swap(smart_ptr& rhs) noexcept // 通過成員函式swap內部呼叫 std內部的標準swap { using std::swap; swap(ptr_, rhs.ptr_); swap(shared_count_, rhs.shared_count_); } T& operator*() const noexcept { return *ptr_; } T* operator->() const noexcept { return ptr_; } operator bool() const noexcept { return ptr_; } private: T* ptr_; shared_count* shared_count_; }; template <typename T> void swap(smart_ptr<T>& lhs, // 對物件方法進行封裝 smart_ptr<T>& rhs) noexcept { lhs.swap(rhs); } template <typename T, typename U> smart_ptr<T> static_pointer_cast( const smart_ptr<U>& other) noexcept { T* ptr = static_cast<T*>(other.get()); // 進行檢驗之後會呼叫建構函式 return smart_ptr<T>(other, ptr); } template <typename T, typename U> smart_ptr<T> reinterpret_pointer_cast( const smart_ptr<U>& other) noexcept { T* ptr = reinterpret_cast<T*>(other.get()); return smart_ptr<T>(other, ptr); } template <typename T, typename U> smart_ptr<T> const_pointer_cast( const smart_ptr<U>& other) noexcept { T* ptr = const_cast<T*>(other.get()); return smart_ptr<T>(other, ptr); } template <typename T, typename U> smart_ptr<T> dynamic_pointer_cast( const smart_ptr<U>& other) noexcept { T* ptr = dynamic_cast<T*>(other.get()); return smart_ptr<T>(other, ptr); }
如果你足夠細心的話,你會發現我在程式碼里加了不少 noexcept。這對這個智慧指標在它的目標場景能正確使用是十分必要的。我們會在下面的幾講裡回到這個話題。