STL智能指针源码浅析

"STL Smart Pointer"

Posted by leiyiming on April 12, 2017

本文从源码入手,浅析 STL中各种智能指针。源码版本: VS2015 C++

auto_ptr


以下是 auto_ptr 的部分源码:

template<class _Ty>
	class auto_ptr
		{	// wrap an object pointer to ensure destruction
public:
	typedef auto_ptr<_Ty> _Myt;
	typedef _Ty element_type;

	explicit auto_ptr(_Ty *_Ptr = 0) _THROW0()
		: _Myptr(_Ptr)
		{	// construct from object pointer
		}

	auto_ptr(_Myt& _Right) _THROW0()
		: _Myptr(_Right.release())
		{	// construct by assuming pointer from _Right auto_ptr
		}

	···

	template<class _Other>
		auto_ptr(auto_ptr<_Other>& _Right) _THROW0()
		: _Myptr(_Right.release())
		{	// construct by assuming pointer from _Right
		}

	_Myt& operator=(_Myt& _Right) _THROW0()
		{	// assign compatible _Right (assume pointer)
		reset(_Right.release());
		return (*this);
		}

	~auto_ptr() _NOEXCEPT
		{	// destroy the object
		delete _Myptr;
		}

	···

	_Ty *release() _THROW0()
		{	// return wrapped pointer and give up ownership
		_Ty *_Tmp = _Myptr;
		_Myptr = 0;
		return (_Tmp);
		}

	void reset(_Ty *_Ptr = 0)
		{	// destroy designated object and store new pointer
		if (_Ptr != _Myptr)
			delete _Myptr;
		_Myptr = _Ptr;
		}

private:
	_Ty *_Myptr;	// the wrapped object pointer
	};

auto_ptr 只有一个指针成员,指向用于包裹的对象(资源),auto_ptr 的特点在于拷贝构造函数 和 operator= 的实现。介绍这些之前首先来看一下 release 和 reset 两个成员函数。 release 成员函数作用是返回包裹资源的指针,并将成员指针指向空。reset 函数则是将成员指针释放,然后指向传入的资源(即重新设置指针)。

资源独占性

auto_ptr 的拷贝构造函数和 operator= 都调用了被复制对象的 release 的函数去设置自身的成员指针,所以在拷贝完成之后,被复制对象的成员指针将会指向空!这也就是说 auto_ptr 对象对资源的所有权是独占的!,可以简单理解为不可能有两个或者以上的 auto_ptr 指向同一个资源对象,它的复制也伴随着所有权的转移。

无法用于数组指针

再看 auto_ptr 的析构函数,是用 delete _Myptr; 直接释放成员指针,所以 auto_ptr 无法包裹数组指针! ,否则在释放时则会引起内存泄漏。

不能在STL容器中使用

由于 STL 中容器的元素必须具备拷贝构造和可赋值的特性,也就是说容器中对象可以进行安全的赋值操作,可以将一个对象拷贝到另一个对象,从而获得两个独立的,逻辑上相同的拷贝(这一点被运用到例如swap、sort等等函数中)。所以,在设计上 auto_ptr 是不能用在 STL容器中的!,STL 容器也有相关措施去保障这一点,不要尝试写出类似 std::vector<std::auto_ptr> 的代码,编译器会主动报错。

unique_ptr


auto_ptr 确保同一时间资源只能被一个智能指针引用,但是复制过程中隐式地将原来指针失效的做法十分容易引起误会,并且由于删除操作的固定(直接使用delete释放指针), auto_ptr 并不支持包裹数组指针或者是包裹非动态申请的对象。C++ 11 新标准中引入了 unique_ptr ,除了保证资源的独占性,它还有以下三个方面的特征。

template<class _Ty,
	class _Dx>	// = default_delete<_Ty>
	class unique_ptr
		: public _Unique_ptr_base<_Ty, _Dx>
	{	// non-copyable pointer to an object
public:
	typedef unique_ptr<_Ty, _Dx> _Myt;
	typedef _Unique_ptr_base<_Ty, _Dx> _Mybase;
	typedef typename _Mybase::pointer pointer;
	typedef _Ty element_type;
	typedef _Dx deleter_type;

	using _Mybase::get_deleter;

	constexpr unique_ptr() _NOEXCEPT
		: _Mybase(pointer())
		{	// default construct ···}

	constexpr unique_ptr(nullptr_t) _NOEXCEPT
		: _Mybase(pointer())
		{	// null pointer construct ···}

	_Myt& operator=(nullptr_t) _NOEXCEPT
		{	// assign a null pointer ···}

	explicit unique_ptr(pointer _Ptr) _NOEXCEPT
		: _Mybase(_Ptr)
		{	// construct with pointer
		static_assert(!is_pointer<_Dx>::value,
			"unique_ptr constructed with null deleter pointer");
		}

	unique_ptr(pointer _Ptr,
		typename _If<is_reference<_Dx>::value, _Dx,
			const typename remove_reference<_Dx>::type&>::type _Dt) _NOEXCEPT
		: _Mybase(_Ptr, _Dt)
		{	// construct with pointer and (maybe const) deleter&
		}

	···

	unique_ptr(unique_ptr&& _Right) _NOEXCEPT
		: _Mybase(_Right.release(),
			_STD forward<_Dx>(_Right.get_deleter()))
		{	// construct by moving _Right
		}

	_Myt& operator=(_Myt&& _Right) _NOEXCEPT
		{	// assign by moving _Right
			···
		}

	~unique_ptr() _NOEXCEPT
		{	// destroy the object
		if (get() != pointer())
			this->get_deleter()(get());
		}

	···

	unique_ptr(const _Myt&) = delete;
	_Myt& operator=(const _Myt&) = delete;
	};

允许自定义删除器

首先,类的声明为 template<class _Ty, class _Dx> class unique_ptr 模板参数多出了一个类型 _Dx ,这个参数是用来作为删除器的仿函数类类型。 unique_ptr 允许用户传入一个仿函数作为删除器,自定义智能指针在析构时所需要进行的操作。如果缺省,_Dx 的类型为 default_delete<_Ty>, 这是 stl 自定义的一个仿函数,功能就是简单的调用 delete 操作符删除指针参数。

我们也可以创建一个仿函数自定义删除器,去实现特定的功能,unique_ptr 自定义删除器实现区域互斥锁:

//自定义删除器
struct  mutex_deleter
{
	void operator() (std::mutex* x)
	{
		x->unlock();    //解锁
	}
};

···
{
	//传入删除器类型
	std::unique_ptr<std::mutex, mutex_deleter> mutex_ptr(new std::mutex()); 

	mutex_ptr->lock();    //上锁
	···                   //互斥操作
	                      //不需要手动调用unlock
}//离开区域,智能指针调用删除器进行自动解锁

不允许使用左值引用进行拷贝

在上面部分源码的最后两行,unique_ptr 使用左值引用的拷贝构造函数和 operator= 操作符都被删除掉了(令 函数声明 = delete 即删除函数,C++ 11 的新特性)。只保留了直接通过裸指针和右值引用进行拷贝的接口,用户必须通过 std::move 函数来将左值引用转换为右值引用,才可以进行拷贝操作,被复制的智能指针一样会失效,但这就明确地告知用户被拷贝的指针将会失效。这类构造和赋值通常被称为 “move构造和move赋值”

//正确的构造方法
std::unique_ptr<int> ptr(new int(88));   //使用构造函数赋值
std::unique_ptr<int> ptr = new int(88);  //使用 “=” 进行拷贝赋值

//错误的拷贝用法
std::unique_ptr<int> copy_ptr(ptr);      //报错!不能使用左值引用进行拷贝
std::unique_ptr<int> copy_ptr = ptr;     //报错!不能使用左值引用行拷贝

//正确的拷贝用法,使用 std::move 使左值引用变右值引用
std::unique_ptr<int> copy_ptr(std::move(ptr));        // ptr 失效
std::unique_ptr<int> copy_ptr = std::move(ptr);       // ptr 失效

也由于拥有move构造和move赋值,unique_ptr 是可以放入 STL 容器中的:

std::unique_ptr<int> up(new int(88));

std::vector < std::unique_ptr<int> > vec_ptr;
vec_ptr.push_back(std::move(up));                //使用move构造

std::cout << *vec_ptr[0].get() << std::endl;

if (up.get() != NULL)
	std::cout << *up.get() << std::endl;         
else
	std::cout << "NULL" << std::endl;            

支持数组指针

unique_ptr 还有另外的模板类型声明,template<class _Ty,class _Dx> class unique_ptr<_Ty[], _Dx>,添加了对数组的支持,相应缺省的删除器也会使用 delete[] 操作去释放整个数组的空间。

std::auto_ptr<int> autoptr(new int[3]);  //报错!
std::unique_ptr<int> unptr(new int[3]);  //正确

_Ref_count_base


_Ref_count_base 是实现引用计数的纯虚类,它被 shared_ptr 和 weak_ptr 两大引用计数智能指针的父类包含。它封装了引用计数的方法,并提供了销毁和删除的抽象接口。在看 shared_ptr 和 weak_ptr 的源码前有必要了解其大致的实现思路,以下是它的源码(省去了部分操作函数和重载函数):

class _Ref_count_base
	{	// common code for reference counting
private:
	virtual void _Destroy() _NOEXCEPT = 0;
	virtual void _Delete_this() _NOEXCEPT = 0;

private:
	_Atomic_counter_t _Uses;
	_Atomic_counter_t _Weaks;

protected:
	_Ref_count_base()
		{	// construct
		_Init_atomic_counter(_Uses, 1);
		_Init_atomic_counter(_Weaks, 1);
		}

public:
	virtual ~_Ref_count_base() _NOEXCEPT
		{}	// ensure that derived classes can be destroyed properly
		
	void _Incref()
		{	// increment use count
		_MT_INCR(_Uses);
		}

	void _Incwref()
		{	// increment weak reference count
		_MT_INCR(_Weaks);
		}

	void _Decref()
		{	// decrement use count
		if (_MT_DECR(_Uses) == 0)
			{	// destroy managed resource, decrement weak reference count
			_Destroy();
			_Decwref();
			}
		}

	void _Decwref()
		{	// decrement weak reference count
		if (_MT_DECR(_Weaks) == 0)
			_Delete_this();
		}

	long _Use_count() const _NOEXCEPT
		{	// return use count
		return (_Get_atomic_count(_Uses));
		}

	bool _Expired() const _NOEXCEPT
		{	// return true if _Uses == 0
		return (_Use_count() == 0);
		}

	virtual void *_Get_deleter(const _XSTD2 type_info&) const _NOEXCEPT
		{	// return address of deleter object
		return (0);
		}
	};

引用计数操作的原子性

_Ref_count_base 包含了两个成员变量 _Uses 和 _Weaks ,他们均是 unsigned long 类型的整型数值,分别表示被共享引用的次数和weak引用的次数。有意思的是,他们是通过别名 _Atomic_counter_t 来声明的,很明显提示这两个计数器是原子的。当构造时,_Uses 和 _Weaks 都被初始化为了 1 。

_Incref, _Incwref, _Decref, _Decwref 方法分别是用于增加和减少计数的接口,主要是调用了 MT_INCR 和 _MT_DECR 两个宏,其定义如下:

#define _MT_INCR(x) \
_InterlockedIncrement(reinterpret_cast<volatile long *>(&x))
#define _MT_DECR(x) \
_InterlockedDecrement(reinterpret_cast<volatile long *>(&x))

可见,这两个宏其实是调用了 Windows 下原子增操作和原子减操作的接口,除了这些,设置初值和得到返回值也均是原子操作,所以计数操作都是原子操作,即 share_ptr 和 weak_ptr 的引用计数操作是线程安全的!

销毁和删除接口

需要注意的是,计数减操作函数会做一个判断,如果执行减操作之后,引用计数值变为 0 时,则要调用相应的销毁函数或者删除函数来执行指针释放操作。而这个操作是纯虚的,提供给子类的接口,具体在 share_ptr 和 weak_ptr 中再讲解这两个接口。

virtual void _Destroy() _NOEXCEPT = 0;
virtual void _Delete_this() _NOEXCEPT = 0;

shared_ptr