一講,我們描述了一個某種程度上可以當成智能指針用的類 shape\_wrapper。使用那個智能指針,可以簡化資源的管理,從根本上消除資源(包括內存)泄漏的可能性。這一講我們就來進一步講解,如何將 shape\_wrapper 改造成一個完整的智能指針。你會看到,智能指針本質上并不神秘,其實就是 RAII 資源管理功能的自然展現而已。在學完這一講之后,你應該會對 C++ 的 unique\_ptr 和 shared\_ptr 的功能非常熟悉了。同時,如果你今后要創建類似的資源管理類,也不會是一件難事。
```
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_;
};
```
和 shape_wrapper 比較一下,我們就是在開頭增加模板聲明 template <typename T>,然后把代碼中的 shape 替換成模板參數 T 而已。這些修改非常簡單自然吧?模板本質上并不是一個很復雜的概念。這個模板使用也很簡單,把原來的 shape_wrapper 改成 smart_ptr<shape> 就行。
目前這個 smart_ptr 的行為還是和指針有點差異的:
* 它不能用 * 運算符解引用
* 它不能用 -> 運算符指向對象成員
* 它不能像指針一樣用在布爾表達式里
不過,這些問題也相當容易解決,加幾個成員函數就可以:
```
template <typename T>
class smart_ptr {
public:
…
T& operator*() const { return *ptr_; }
T* operator->() const { return ptr_; }
operator bool() const { return ptr_; }
}
```
## 拷貝構造和賦值
拷貝構造和賦值,我們暫且簡稱為拷貝,這是個比較復雜的問題了。關鍵還不是實現問題,而是我們該如何定義其行為。假設有下面的代碼:
```
smart_ptr<shape> ptr1{create_shape(shape_type::circle)};
smart_ptr<shape> ptr2{ptr1};
```
對于第二行,究竟應當讓編譯時發生錯誤,還是可以有一個更合理的行為?我們來逐一檢查一下各種可能性。最簡單的情況顯然是禁止拷貝。我們可以使用下面的代碼:
```
template <typename T>
class smart_ptr {
…
smart_ptr(const smart_ptr&)
= delete;
smart_ptr& operator=(const smart_ptr&)
= delete;
…
};
```
禁用這兩個函數非常簡單,但卻解決了一種可能出錯的情況。否則,smart\_ptrptr2{ptr1}; 在編譯時不會出錯,但在運行時卻會有未定義行為——由于會對同一內存釋放兩次,通常情況下會導致程序崩潰。
.
我們是不是可以考慮在拷貝智能指針時把對象拷貝一份?不行,通常人們不會這么用,因為使用智能指針的目的就是要減少對象的拷貝啊。何況,雖然我們的指針類型是 shape,但實際指向的卻應該是 circle 或 triangle 之類的對象。在 C++ 里沒有像 Java 的 clone 方法這樣的約定;一般而言,并沒有通用的方法可以通過基類的指針來構造出一個子類的對象來。我們要么試試在拷貝時轉移指針的所有權?大致實現如下:
```
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 對象的內容可能已經被部分破壞了,對象不再處于一個完整的狀態。
**上面代碼里的這種慣用法(見參考資料 \[1\])則保證了強異常安全性:** 賦值分為拷貝構造和交換兩步,異常只可能在第一步發生;而第一步如果發生異常的話,this 對象完全不受任何影響。無論拷貝構造成功與否,結果只有賦值成功和賦值沒有效果兩種狀態,而不會發生因為賦值破壞了當前對象這種場景。
.
如果你覺得這個實現還不錯的話,那恭喜你,你達到了 C++ 委員會在 1998 年時的水平:上面給出的語義本質上就是 C++98 的 auto\_ptr 的定義。如果你覺得這個實現很別扭的話,也恭喜你,因為 C++ 委員會也是這么覺得的:auto\_ptr 在 C++17 時已經被正式從 C++ 標準里刪除了。
.
上面實現的最大問題是,它的行為會讓程序員非常容易犯錯。一不小心把它傳遞給另外一個 smart\_ptr,你就不再擁有這個對象了……
### “移動”指針?
在下一講我們將完整介紹一下移動語義。這一講,我們先簡單看一下 smart\_ptr 可以如何使用“移動”來改善其行為。我們需要對代碼做兩處小修改:
```
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;
}
…
};
```
看到修改的地方了嗎?我改了兩個地方:
* 把拷貝構造函數中的參數類型 smart\_ptr& 改成了 smart\_ptr&&;現在它成了移動構造函數。
* 把賦值函數中的參數類型 smart\_ptr& 改成了 smart\_ptr,在構造參數時直接生成新的智能指針,從而不再需要在函數體中構造臨時對象。現在賦值函數的行為是移動還是拷貝,完全依賴于構造參數時走的是移動構造還是拷貝構造。
根據 C++ 的規則,如果我提供了移動構造函數而沒有手動提供拷貝構造函數,那后者自動被禁用(記住,C++ 里那些復雜的規則也是為方便編程而設立的)。于是,我們自然地得到了以下結果:
```
smart_ptr<shape> ptr1{create_shape(shape_type::circle)};
smart_ptr<shape> ptr2{ptr1}; // 編譯出錯
smart_ptr<shape> ptr3;
ptr3 = ptr1; // 編譯出錯
ptr3 = std::move(ptr1); // OK,可以
smart_ptr<shape> ptr4{std::move(ptr3)}; // OK,可以
```
這個就自然多了。這也是 C++11 的 unique\_ptr 的基本行為。
.
## 子類指針向基類指針的轉換
哦,我撒了一個小謊。不知道你注意到沒有,一個 circle\* 是可以隱式轉換成 shape\* 的,但上面的 smart\_ptr卻無法自動轉換成 smart\_ptr。這個行為顯然還是不夠“自然”。
.
不過,只需要額外加一點模板代碼,就能實現這一行為。在我們目前給出的實現里,只需要增加一個構造函數即可——這也算是我們讓賦值函數利用構造函數的好處了。
```
template <typename U>
smart_ptr(smart_ptr<U>&& other)
{
ptr_ = other.release();
}
```
這樣,我們自然而然利用了指針的轉換特性:現在 smart\_ptr可以移動給 smart\_ptr,但不能移動給 smart\_ptr。不正確的轉換會在代碼編譯時直接報錯。
.
需要注意,上面這個構造函數不被編譯器看作移動構造函數,因而不能自動觸發刪除拷貝構造函數的行為。如果我們想消除代碼重復、刪除移動構造函數的話,就需要把拷貝構造函數標記成 = delete 了(見“拷貝構造和賦值”一節)。不過,更通用的方式仍然是同時定義標準的拷貝 / 移動構造函數和所需的模板構造函數。下面的引用計數智能指針里我們就需要這么做。
.
至于非隱式的轉換,因為本來就是要寫特殊的轉換函數的,我們留到這一講的最后再討論。
## 引用計數
unique\_ptr 算是一種較為安全的智能指針了。但是,一個對象只能被單個 unique\_ptr 所擁有,這顯然不能滿足所有使用場合的需求。一種常見的情況是,多個智能指針同時擁有一個對象;當它們全部都失效時,這個對象也同時會被刪除。這也就是 shared\_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;
};
```
這個 shared\_count 類除構造函數之外有三個方法:一個增加計數,一個減少計數,一個獲取計數。注意上面的接口增加計數不需要返回計數值;但減少計數時需要返回計數值,以供調用者判斷是否它已經是最后一個指向共享計數的 shared\_ptr 了。由于真正多線程安全的版本需要用到我們目前還沒學到的知識,我們目前先實現一個簡單化的版本:
```
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’
錯誤原因是模板的各個實例間并不天然就有 friend 關系,因而不能互訪私有成員 ptr\_ 和 shared\_count\_。我們需要在 smart\_ptr 的定義中顯式聲明:
```
templatefriend class smart\_ptr;
```
此外,我們之前的實現(類似于單一所有權的 unique\_ptr )中用 release 來手工釋放所有權。在目前的引用計數實現中,它就不太合適了,應當刪除。但我們要加一個對調試非常有用的函數,返回引用計數值。定義如下:
```
long use_count() const
{
if (ptr_) {
return shared_count_
->get_count();
} else {
return 0;
}
}
```
這就差不多是一個比較完整的引用計數智能指針的實現了。我們可以用下面的代碼來驗證一下它的功能正常:
```
class shape {
public:
virtual ~shape() {}
};
class circle : public shape {
public:
~circle() { puts("~circle()"); }
};
int main()
{
smart_ptr<circle> ptr1(new circle());
printf("use count of ptr1 is %ld\n",
ptr1.use_count());
smart_ptr<shape> ptr2;
printf("use count of ptr2 was %ld\n",
ptr2.use_count());
ptr2 = ptr1;
printf("use count of ptr2 is now %ld\n",
ptr2.use_count());
if (ptr1) {
puts("ptr1 is not empty");
}
}
```
這段代碼的運行結果是:
>
> use count of ptr1 is 1
> use count of ptr2 was 0
> use count of ptr2 is now 2
> ptr1 is not empty~circle()
上面我們可以看到引用計數的變化,以及最后對象被成功刪除。指針類型轉換對應于 C++ 里的不同的類型強制轉換:
* static\_cast
* reinterpret\_cast
* const\_cast
* dynamic\_cast
智能指針需要實現類似的函數模板。實現本身并不復雜,但為了實現這些轉換,我們需要添加構造函數,允許在對智能指針內部的指針對象賦值時,使用一個現有的智能指針的共享計數。如下所示:
```
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_;
}
}
```
這樣我們就可以實現轉換所需的函數模板了。下面實現一個 dynamic\_pointer\_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);
}
```
在前面的驗證代碼后面我們可以加上:
```
smart_ptr<circle> ptr3 =
dynamic_pointer_cast<circle>(ptr2);
printf("use count of ptr3 is %ld\n",
ptr3.use_count());
```
編譯會正常通過,同時能在輸出里看到下面的結果:
> use count of ptr3 is 3
最后,對象仍然能夠被正確刪除。這說明我們的實現是正確的。代碼列表為了方便你參考,下面我給出了一個完整的 smart\_ptr 代碼列表:
```
#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;
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;
}
}
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
{
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。這對這個智能指針在它的目標場景能正確使用是十分必要的。我們會在下面的幾講里回到這個話題。
## 內容小結
這一講我們從 shape\_wrapper 出發,實現了一個基本完整的帶引用計數的智能指針。這個智能指針跟標準的 shared\_ptr 比,還缺了一些東西(見參考資料 \[2\]),但日常用到的智能指針功能已經包含在內。現在,你應當已經對智能指針有一個較為深入的理解了。
### 課后思考
這里留幾個問題,你可以思考一下:
1. 不查閱 shared\_ptr 的文檔,你覺得目前 smart\_ptr 應當添加什么功能嗎?
2. 你想到的功能在標準的 shared\_ptr 里嗎?
3. 你覺得智能指針應該滿足什么樣的線程安全性?
- C++基礎
- 什么是 POD 數據類型?
- 面向對象三大特性五大原則
- 低耦合高內聚
- C++類型轉換
- c++仿函數
- C++仿函數了解一下?
- C++對象內存模型
- C++11新特性
- 智能指針
- 動手實現C++的智能指針
- C++ 智能指針 shared_ptr 詳解與示例
- 現代 C++:一文讀懂智能指針
- Lamda
- c++11多線程
- std::thread
- std::async
- std::promise
- std::future
- C++11 的內存模型
- 初始化列表
- std::bind
- std::tuple
- auto自動類型推導
- 可變參數模板
- 右值引用與移動語義
- 完美轉發
- 基于范圍的for循環
- C++11之POD類型
- std::enable_if
- C++14/17
- C++20
- 協成
- 模塊
- Ranges
- Boost
- boost::circular_buffer
- 使用Boost.Asio編寫通信程序
- Boost.Asio C++ 網絡編程
- 模板
- 模板特化/偏特化
- C++模板、類模板、函數模板詳解都在這里了
- 泛化之美--C++11可變模版參數的妙用
- 模板元編程
- 這是我見過最好的模板元編程文章!