深入 C++ 的 unique_ptr

TuxedoLinux發表於2018-06-06

深入 C++ 的 unique_ptr

從異常安全說起

  使用 raw pointer 管理動態記憶體時,經常會遇到這樣的問題:

  • 忘記delete記憶體,造成記憶體洩露。
  • 出現異常時,不會執行delete,造成記憶體洩露。

  下面的程式碼解釋了,當一個操作發生異常時,會導致delete不會被執行:

1
2
3
4
5
6
7
8
9
void func()
{
auto ptr = new Widget;
// 執行一個會丟擲異常的操作
func_throw_exception();
delete ptr;
}

  在 C++98 中我們需要用一種笨拙的方式,寫出異常安全的程式碼:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
void func()
{
auto ptr = new Widget;
try {
func_throw_exception();
}
catch(...) {
delete ptr;
throw;
}
delete ptr;
}

  使用智慧指標能輕易寫出異常安全的程式碼,因為當物件退出作用域時,智慧指標將自動呼叫物件的解構函式,避免記憶體洩露:

1
2
3
4
5
6
void func()
{
std::unique_ptr<Widget> ptr{ new Widget };
func_throw_exception();
}

unique_ptr 的原理

  讓我們瞭解一下unique_ptr的實現細節:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
namespace std {
template <typename T, typename D = default_delete<T>>
class unique_ptr
{
public:
explicit unique_ptr(pointer p) noexcept;
~unique_ptr() noexcept;
T& operator*() const;
T* operator->() const noexcept;
unique_ptr(const unique_ptr &) = delete;
unique_ptr& operator=(const unique_ptr &) = delete;
unique_ptr(unique_ptr &&) noexcept;
unique_ptr& operator=(unique_ptr &&) noexcept;
// ...
private:
pointer __ptr;
};
}

  從上面的程式碼中,我們可以瞭解到:

  • unique_ptr內部儲存一個 raw pointer,當unique_ptr析構時,它的解構函式將會負責析構它持有的物件。
  • unique_ptr提供了operator*()operator->()成員函式,像 raw pointer 一樣,我們可以使用*解引用unique_ptr,使用->來訪問unique_ptr所持有物件的成員。
  • unique_ptr並不提供 copy 操作,這是為了防止多個unique_ptr指向同一物件。
  • unique_ptr提供了 move 操作,因此我們可以用std::move()來轉移unique_ptr

  很顯然,預設情況下,unique_ptr會使用delete析構物件,不過我們可以使用自定義的 deleter。

1
2
3
4
5
6
7
struct Widget{ };
// ...
auto deleter = []( Widget *p ) {
cout << "delete Widget!" << endl;
delete p;
};
unique_ptr<Widget, decltype(deleter)> ptr{ new Widget, deleter };

  當然,我們可以使用 C++11 的 alias template 特性,這樣就可以避免指定 deleter 的型別:

1
2
3
4
5
6
7
8
9
10
11
12
13
struct Widget{ };
template <typename T>
using uniquePtr = unique_ptr<T, void(*)(T*)>;
void func()
{
uniquePtr<Widget> ptr( new Widget,
[]( Widget *p ) {
cout << "delete Widget!" << endl;
delete p;
});
}

  unique_ptr為陣列提供了模板偏特化,因此unique_ptr也可以指向陣列:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
namespace std {
template <typename T, typename D>
class unique_ptr<T[], D>
{
public:
// ...
T& operator[]( size_t i ) const;
};
template <typename T>
class default_delete<T[]>
{
public:
// ...
void operator()( T *p ) const; // call delete[] p
};
}

  當unique_ptr指向陣列時,可以使用[]來訪問陣列元素。default_delete也為陣列提供模板偏特化,因此當unique_ptr被銷燬時,會呼叫delete []釋放陣列記憶體。

1
2
3
unique_ptr<string[]> ptr{ new string[100] };
ptr[0] = "hello";
ptr[1] = "world";

一些陷阱

  unique_ptr是用來獨佔地持有物件的,所以通過同一原生指標來初始化多個unique_ptr,下面是一種錯誤的使用方式:

1
2
3
4
struct Widget{ };
Widget *ptr = new Widget;
unique_ptr<Widget> p1{ ptr };
unique_ptr<Widget> p2{ ptr }; // ERROR: multiple ownership

  當p1p2各自被銷燬的時候,它們指向的Widget將被delete兩次。

再談異常安全

  C++14 提供了std::make_unique<T>()函式用來直接建立unique_ptr,但 C++11 並沒有提供,不過其實現並不複雜:

1
2
3
4
5
6
template <typename T, typename... Ts>
std::unique_ptr<T> make_unique( Ts&&... params ) {
return std::unique_ptr<T>( new T( std::forward<Ts>(params)... ) );
}
// ...
auto ptr = make_unique<std::string>("senlin");

  思考一下使用make_unique的好處?


  使用unique_ptr並不能絕對地保證異常安全,你可能很驚訝於這個結論。讓我們看看一個例子:

1
func(unique_ptr<T>{ new T }, func_throw_exception());

  C++ 標準並沒有規定編譯器對函式引數的求值次序,所以有可能出現這樣的次序:

  • 呼叫new T分配動態記憶體。
  • 呼叫func_throw_exception()函式。
  • 呼叫unique_ptr的建構函式。

  呼叫func_throw_exception()函式會丟擲異常,所以無法構造unique_ptr,導致new T所分配的記憶體不能回收,造成了記憶體洩露。解決這個問題,需要使用make_unique函式:

1
func(make_unique<T>(), func_throw_exception());

  這種情況下,成功解決了記憶體洩露的問題。


  make_unique在初始化物件的時候使用的()而不是{},所以下面的程式碼顯然是初始化10個元素:

1
2
3
auto up = make_unique<vector<int>>( 10, 100 );
cout << "size: " << up->size() << endl;
// size: 10

  但是如果使用std::initializer_list來初始化物件時,要怎樣做呢?嗯嗯,看看下面的程式碼:

1
2
3
4
auto initList = { 1, 2, 3, 4, 5 };
auto up = make_unique<vector<int>>( initList );
cout << "size: " << up->size() << endl;
// size: 5

相關文章