1. 程式人生 > 其它 >02 | 自己動手,實現C++的智慧指標

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。這對這個智慧指標在它的目標場景能正確使用是十分必要的。我們會在下面的幾講裡回到這個話題。