深入 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

参考资料