C++RAII与智能指针源码分析(上)
“ 去留无意看天边云卷云舒。”
在《》一文中我们曾提到RAII(资源获取于初始化)技术,今天准备在这里详细讲一下。有人说“学习C++ 不知道RAII那就是不合格的c++程序员”,RAII对C++的重要性可见一斑。通过之前的介绍,大家应该能感受到RAII实际上就是GC技术,对于GC技术:C语言没有,C++就是RAII,JAVA就是GC。所以,可以毫不夸张的说RAII就是C++的“基础逻辑”。
智能指针也是利用RAII的原理为管理动态分配对象的生命周期而设计的,通过保证智能指针对象在适当的时机以适当的方式析构来防止内存泄漏。所以理解了RAII对学习智能指针会大有裨益。在这里我会来总结智能指针的使用方法,C++11提供的四种智能指针的源代码分析,在这篇文章我们只分析RAII智能指针,使用了引用计数的智能指针在下篇文章中分析。
0. RAII技术
RAII(Resource Acquisition Is Initialization)是由c++之父Bjarne Stroustrup提出的,中文翻译为资源获取即初始化。他说:“使用局部对象来管理资源的技术称为资源获取即初始化。”这里的资源主要是指操作系统中有限的东西如内存、网络套接字等等,局部对象是指存储在栈中的对象,它的生命周期是由操作系统来管理的,无需人工介入。
资源的使用一般经历三个步骤:
获取资源
使用资源
销毁资源
但是资源的销毁往往是程序员经常忘记的一个环节:
bool process() {object_counter* p = new object_counter;try {if (!success) {throw process_exception(); // 抛出异常,p资源没有释放}} catch ( ... ) {delete p;}delete p;return true;}
所以程序界就想如何在程序员中让资源自动销毁呢?c++之父给出了解决问题的方案:RAII。它充分的利用了C++语言局部对象自动销毁的特性来控制资源的生命周期。给一个简单的例子来看下局部对象的自动销毁的特性:
using namespace std;class Person{public:Person(const std::string name = "", int age = 0) :name_(name), age_(age) {std::cout << "construct: init a person!" << std::endl;}~person() {std::cout << "destruct: destory a person!" << std::endl;}const std::string& getName() const {return this->name_;}int getAge() const {return this->age_;}private:const std::string name_;int age_;};int main() {Person p;return 0;}运行结果:construct: init a person!destruct: destory a person!
从Person可以看出,当我们在main函数中声明一个局部对象的时候,会自动调用构造函数进行对象的初始化,当整个main函数执行完成后,自动调用析构函数来销毁对象,整个过程无需人工介入,由操作系统自动完成。
于是,很自然联想到,当我们在使用资源的时候,在构造函数中进行初始化,在析构函数中进行销毁。RAII的总结如下:
将每个资源封装入一个类,其中:
构造函数请求资源,并建立所有类不变式,或在它无法完成时抛出异常;
析构函数释放资源并决不抛出异常。
始终经由 RAII 类的实例使用满足要求的资源,该资源:
自身拥有自动存储期或临时生存期,或具有与自动或临时对象的生存期绑定的生存期。
我们在《》一文中,已经实现了一个管理内存的RAII。C++提供了四个RAII内存管理对象。
template <typename T>class scoped_ptr {public:explicit scoped_ptr(T* p) : p_(p) {}~scoped_ptr() { delete p_; }T* operator->() { return p_; }const T* operator->() const { return p_; }T& operator*() { return *p_; }const T& operator*() const { return *p_; }private:T* p_;};// 用法:scoped_ptr<object_counter> p(new object_counter);bool process() {scoped_ptr<object_counter> p(new object_counter);try {if (!success) {throw process_exception(); // 抛出异常,p资源没有释放}} catch ( ... ) {...}return true;}
但是,RAII不仅仅是管理内存,还可以管理互斥锁之类的其他资源。我们在这里也实现一个自己的RAII互斥锁对象mutex_guard。C++提供了一个RAII互斥锁对象:lock_guard。
class mutex_guard {public:explicit mutex_guard(std::mutex& m) : m_(m), must_unlock_(true) { // 初始化析构需要解锁m_.lock();}~mutex_guard() {if (must_unlock_) {m_.unlock();}}void reset() {m_.unlock();must_unlock_ = false; // 在过程中解锁,置位表示锁已经揭开,析构中不需要重复解锁}private:std::mutex& m_;bool must_unlock_;}
为了深入理解RAII技术,我们在下一节分析C++提供RAII内存管理对象std::auto_ptr和std::unique_ptr的源代码。
1. auto_ptr与unique_ptr
auto_ptr源代码解析
auto_ptr是从C98残留下来的弃用特性,它是一种针对智能指针进行标准化的尝试,这种尝试后来成为C++11中的unique_ptr。由于C++98中没有移动语义,因此auto_ptr使用赋值操作来完成移动任务,复制后将其置为空。因此,不能在容器中使用auto_ptr对象。unique_ptr可以做auto_ptr能够做的任何事,当你需要使用智能指针时,unique_ptr基本上应是手头首选。
auto_ptr的源代码非常简单,首先来auto_ptr源码分析:
template<typename _Tp>class auto_ptr{private:_Tp* _M_ptr; // auto_ptr对象使用_M_ptr_来指向一个heap内的对象public:typedef _Tp element_type;// 显式构造函数,接受一个原生指针来生成auto_ptr对象explicit auto_ptr(element_type* __p = 0) throw() : _M_ptr(__p) { }// 拷贝构造函数,只能接收一个作为左值的auto_ptr对象auto_ptr(auto_ptr& __a) throw() : _M_ptr(__a.release()) { }// 拷贝构造函数,兼容_Tp1*可以隐式转换为_Tp*的auto_ptr对象作为初值template<typename _Tp1>auto_ptr(auto_ptr<_Tp1>& __a) throw() : _M_ptr(__a.release()) { }// 赋值构造函数,同样只能接收一个作为左值的auto_ptr对象auto_ptr& operator=(auto_ptr& __a) throw(){reset(__a.release());return *this;}template<typename _Tp1>auto_ptr& operator=(auto_ptr<_Tp1>& __a) throw(){reset(__a.release());return *this;}// auto_ptr对象析构时,销毁其所指物~auto_ptr() { delete _M_ptr; }// 解引用操作element_type& operator*() const throw() {__glibcxx_assert(_M_ptr != 0);return *_M_ptr;}element_type* operator->() const throw() {__glibcxx_assert(_M_ptr != 0);return _M_ptr;}/*** 通过get函数可以获取管理对象**/element_type* get() const throw() { return _M_ptr; }/*** release和reset是auto_ptr最重要的成员方法;* 可以看出当auto对象被拷贝或者赋值时,对象所有权会转移;* 因此,千万不要使用by value的方式传递auto_ptrdui'xian*/element_type* release() throw() {element_type* __tmp = _M_ptr;_M_ptr = 0;return __tmp;}void reset(element_type* __p = 0) throw() {if (__p != _M_ptr){delete _M_ptr;_M_ptr = __p;}}};
值得注意的是,由于auto_ptr的拷贝构造函数和赋值构造函数操作会控制权转移,因此auto_ptr的拷贝构造函数和赋值构造函数使用的入参并不是const_by_reference。这样,在这种情况下会编译不过:
auto_ptr<int> pt(auto_ptr<int> new(int(3)));
因为auto_ptr<int> new(int(3))是一个右值,右值不能赋给左值引用。因此auto_prt中还提供了如下拷贝构造函数和赋值构造函数,入参为auto_ptr_ref对象,就是为了解决此场景:
auto_ptr(auto_ptr_ref<element_type> __ref) throw() : _M_ptr(__ref._M_ptr) { }auto_ptr& operator=(auto_ptr_ref<element_type> __ref) throw() {if (__ref._M_ptr != this->get()){delete _M_ptr;_M_ptr = __ref._M_ptr;}return *this;}
只要auto_ptr<int> new(int(3))可以隐式的转为auto_ptr_ref就可以构造auto_ptr对象。
auto_ptr的使用方法:
auto_ptr<int> ap1(auto_ptr<int>(new int(3)));cout << *ap1 << endl;auto_ptr<int> p(new int(3));auto_ptr<int> ap2(p); // 控制权转移,p已经不能再使用了cout << *ap2 << endl;cout << *(ap2.get()) << endl;
unique_ptr源代码解析
unique_ptr的特征:
std::unique_ptr是小巧、高速的、具备只移型别的智能指针,对托管资源实施专属所有权语义。默认的,资源析构采用delete运算符来实现,但可以指定自定义删除器。有状态的删除器和采用函数指针实现的删除器会增加std::unique_ptr型别的对象尺寸。
——来自《Effective Modern C++》条款18
unique_ptr的重要使用场景是工厂函数,以及用作实现Pimpl习惯用法的机制。
unique_ptr的源代码分析如下(源码分析,直接在源代码的注释中),通过学习源代码,我们可以很轻松的理解上面总结的unique_ptr的特征,建议大家重点只移型别和管理专属所有权,而不用去死记硬背:
// _Tp为管理对象的类型,_Dp为析构器的类型// default_delete是默认析构器,默认析构器中使用delete运算符实现对象的析构template <typename _Tp, typename _Dp = default_delete<_Tp>>class unique_ptr{// 使用__uniq_ptr_impl管理要管理的heap对象// _Tp为管理对象类型,_Dp为析构器__uniq_ptr_impl<_Tp, _Dp> _M_t;public:using pointer = typename __uniq_ptr_impl<_Tp, _Dp>::pointer;using element_type = _Tp;using deleter_type = _Dp;};
在看unique_ptr的构造函数之前,我们先看一下__uniq_ptr_impl的源代码:
// _Tp为管理对象的类型,_Dp为析构器的类型template <typename _Tp, typename _Dp>class __uniq_ptr_impl{template <typename _Up, typename _Ep, typename = void>struct _Ptr{using type = _Up*;};// pointer实际上就是_Tp*using pointer = typename _Ptr<_Tp, _Dp>::type;private:// 使用tuple管理指针和析构器,通过get<0>获取_Tp*,get<1>获取析构器tuple<pointer, _Dp> _M_t;public:__uniq_ptr_impl() = default;// 先通过_M_t()获取指针,再赋值__uniq_ptr_impl(pointer __p) : _M_t() { _M_ptr() = __p; }// 自定义析构器template<typename _Del>__uniq_ptr_impl(pointer __p, _Del&& __d) : _M_t(__p, std::forward<_Del>(__d)) { }// 获取所管理的对象pointer& _M_ptr() { return std::get<0>(_M_t); }// 获取析构器_Dp& _M_deleter() { return std::get<1>(_M_t); }};
接着,我们来分析unique_ptr的构造函数和析构函数:
// 默认构造函数,显式创建一个空的unique_ptr对象template<typename _Up = _Dp, typename = _DeleterConstraint<_Up>>constexpr unique_ptr() noexcept : _M_t() {}// 使用指针__p构造一个unique_ptr对象template<typename _Up = _Dp, typename = _DeleterConstraint<_Up>>explicit unique_ptr(pointer __p) noexcept : _M_t(__p) {}// 使用指针__p和自定义析构器__d构造一个unique_ptr对象unique_ptr(pointer __p,typename conditional<is_reference<deleter_type>::value,deleter_type, const deleter_type&>::type __d) noexcept: _M_t(__p, __d) { }// Disable copy from lvalue.不允许复制,体现专属所有权语义// 使用了C++11特性deleteunique_ptr(const unique_ptr&) = delete;unique_ptr& operator=(const unique_ptr&) = delete;// Move constructor.体现专属所有权语义和只移型别// 只允许使用移动拷贝构造函数// 如果复制一个unique_ptr对象,会将源unique_ptr对象管理的资源release掉unique_ptr(unique_ptr&& __u) noexcept: _M_t(__u.release(), std::forward<deleter_type>(__u.get_deleter())) { }// 这个也是移动拷贝构造函数// 只是使用的类型是可以隐式转换的其他unique_ptr对象template<typename _Up, typename _Ep, typename = _Require<__safe_conversion_up<_Up, _Ep>,typename conditional<is_reference<_Dp>::value,is_same<_Ep, _Dp>,is_convertible<_Ep, _Dp>>::type>>unique_ptr(unique_ptr<_Up, _Ep>&& __u) noexcept: _M_t(__u.release(), std::forward<_Ep>(__u.get_deleter())){ }// Assignment,也可以说明是专属所有权语义和只移型别unique_ptr& operator=(unique_ptr&& __u) noexcept{// __u.release()释放并返回源unique_ptr对象管理的资源// reset是将__u.release()返回的资源赋给目标(当前)unique_ptr对象reset(__u.release());get_deleter() = std::forward<deleter_type>(__u.get_deleter());return *this;}// 析构函数,调用析构器析构掉管理的资源,并将__ptr指向nullptr~unique_ptr(){auto& __ptr = _M_t._M_ptr();if (__ptr != nullptr)get_deleter()(__ptr);__ptr = pointer();}// get_deleter()(__ptr);的解读// get_deleter()返回的是析构器,默认的析构器为struct default_delete<_Tp>// struct default_delete<_Tp>有一个operator()操作符void operator()(_Tp* __ptr) const{delete __ptr;}// 所以get_deleter()(__ptr);实际上就是delete __ptr;
unique_ptr的重要成员方法解读:
// 可以像raw pointer一样,解引用typename add_lvalue_reference<element_type>::type operator*() const{__glibcxx_assert(get() != pointer());return *get();}// 像raw pointer一样获取保存的指针,调用get方法pointer operator->() const noexcept{_GLIBCXX_DEBUG_PEDASSERT(get() != pointer());return get();}pointer get() const noexcept{ return _M_t._M_ptr(); }// 释放对所管理资源的所有权pointer release() noexcept{pointer __p = get();_M_t._M_ptr() = pointer();return __p;}// 重置所管理的资管void reset(pointer __p = pointer()) noexcept{using std::swap;swap(_M_t._M_ptr(), __p);if (__p != pointer() get_deleter()(__p);}
接下来我们看一下unique_ptr的使用,我们看一下怎么为一个unique_ptr对象指定析构器:
struct Investment {}; // 待管理对象// 默认的析构器struct default_delete<_Tp>实际上是一个仿函数// 我们就可以定义一个lambd作为析构器auto delInvmt = [](Investment* pInvestment){makeLogEntry(pInvestment); // 做一些删除前的工作delete pInvestment;};// 使用decltype推到出自定义析构器的类型unique_ptr<Investment, decltype(delInvmt)> pInv((new Investment), delInvmt);
值得注意的是,如果要自定义析构器,就必须使用构造函数,而无法通过C++14提供的make_unique函数。我们看一下上面的运行结果:
struct Investment{~Investment(){cout << "called ~Investment()..." << endl;}};int main(){auto ivmt = [](Investment* pInvestment){cout << "user-defined delete..." << endl;delete pInvestment;};// 包在括号中,方便观察结果{unique_ptr<Investment, decltype(ivmt)> pInvestment((new Investment), ivmt);}return 0;}// 运行结果user-defined delete...called ~Investment()...
另外unique_ptr不允许以赋值语法将一个raw pointer当作初值:
unipue_ptr<int> pInt = new int(2); // errorunique_ptr<int> pInt(new int(2)); // OK
不能使用普通的拷贝或者赋值:
unique_ptr<int> pInt1(new int(2)); // pInt1现在为左值unique_ptr<int> pInt2(pInt1); // error// 需要使用移动语义,使用move将左值转为右值unique_ptr<int> pInt2(std::move(pInt1));unique_ptr<int> pInt2;pInt2 = pInt1; // errorpInt2 = std::move(pInt1);
应用场景:使用unique_ptr作为成员
struct Investment{~Investment(){cout << "called ~Investment()..." << endl;}};struct Stock{Stock() : pInvestment(new Investment);// 不需要定义析构函数,因为unique_ptr为你管理heap上的对象private:unique_ptr<Investment> pInvestment;};
关于RAII的介绍和unique_ptr的源码分析就到这里,RAII为我们提供了一个安全地管理heap对象的方法,unique_ptr则为我们找到一个替代裸指针的方式。
