C++:从零开始手搓一个智能指针吧
前言
智能指针是C++11引入的一个新特性,它是一个模板类,用于管理动态分配的对象。智能指针的主要目的是自动释放内存,避免内存泄漏。智能指针的实现原理是利用了RAII(Resource Acquisition Is Initialization)技术,即资源获取即初始化。智能指针的实现有很多种,比如std::shared_ptr
、std::unique_ptr
、std::weak_ptr
等。本文将从零开始手搓一个智能指针,以msvc的实现为例。
参考这篇文章的实现
类的前向声明
template <typename Ty>
struct shared_ptr;
template <typename Ty>
struct weak_ptr;
template <class Ty>
struct enable_shared_from_this;
template <typename Ty>
constexpr auto can_enable_shared = std::is_base_of_v<enable_shared_from_this<Ty>, Ty>;
引用计数
pirvate部分的_uses
和_weaks
分别表示当前对象的引用计数和弱引用计数。
template <typename Ty>
struct RefCount {
private:
std::atomic_int32_t _uses = 1;
std::atomic_int32_t _weaks = 1;
Ty *_ptr;
};
成员函数
template <typename Ty>
struct RefCount {
RefCount(Ty *raw_ptr) :
_ptr(raw_ptr) {
}
void add_ref() {
_uses++;
}
void sub_ref() {
if ((--_uses) == 0) {
destroy_resource();
sub_wref();
}
}
void add_wref() {
_weaks++;
}
void sub_wref() {
if ((--_weaks) == 0) {
delete_this();
}
}
bool try_add_ref() {
if (_uses == 0) return false;
_uses++;
return true;
}
void destroy_resource() noexcept {
delete _ptr;
}
void delete_this() noexcept {
delete this;
}
int32_t use_count() {
return _uses;
}
private:
std::atomic_int32_t _uses = 1;
std::atomic_int32_t _weaks = 1;
Ty *_ptr;
};
这里有几个需要注意的地方:
-
在delete时候使用noexcept,这样可以避免在析构函数中抛出异常,允许编译器做出额外的优化。
-
在
_uses
变为0,即资源被释放之后,也同时减少代表这个RefCount
实例本身的计数_weaks
,表示现在这个RefCount
实例本身不需要再为这个资源服务了。如果_weaks
也变为0,那么这个RefCount实例本身也可以被释放了。
void sub_ref() {
if ((--_uses) == 0) {
destroy_resource();
sub_wref();
}
}
PtrBase
构造函数的声明
template <class Ty>
struct Ptr_base {
Ptr_base() = default;
Ptr_base(const Ptr_base &) = delete;
Ptr_base &operator=(const Ptr_base &) = delete;
这里的两个delete是为了防止拷贝构造和拷贝赋值,因为在智能指针的实现中,拷贝构造和拷贝赋值都将有可能导致我们实现的引用计数的错误。
PtrBase 的完整定义
template<typename T>
struct Ptr_base {
Ptr_base() = default;
Ptr_base(const Ptr_base &) = delete;
Ptr_base &operator=(const Ptr_base &) = delete;
T *get() const noexcept {
return _ptr;
}
auto use_count() const noexcept {
return _ref ? _ref->use_count() : 0;
}
template<typename _T2>
void swap(Ptr_base<_T2> &rhs) {
std::swap(_ptr, rhs._ptr);
std::swap(_ref, rhs._ref);
}
private:
friend class shared_ptr<T>;
friend class weak_ptr<T>;
T *_ptr = nullptr;
RefCount<T> *_ref = nullptr;
void _init(T *rwa_ptr) {
_ptr = rwa_ptr;
_ref = new RefCount<T>(_ptr);
}
void add_ref() const {
if (_ref) {
_ref->add_ref();
}
}
void sub_ref() const {
if (_ref) {
_ref->sub_ref();
}
}
void add_wref() const {
if (_ref) {
_ref->add_wref();
}
}
void sub_wref() const {
if (_ref) {
_ref->sub_wref();
}
}
template<typename _T2>
void _copy_ptr_from(const Ptr_base<_T2> &rhs) {
_ptr = rhs._ptr;
_ref = rhs._ref;
// note: no need to add_ref here
}
template<typename _T2>
void _move_construct_from(Ptr_base<_T2> &&rhs) noexcept {
_copy_ptr_from(rhs);
rhs._ptr = nullptr;
rhs._ref = nullptr;
}
template<typename _T2>
void _copy_construct_from_shared(const shared_ptr<_T2> &rhs) noexcept {
rhs.add_ref(); // add_ref is here
_copy_ptr_from(rhs);
}
template<typename _T2>
void _weakly_construct_from(const Ptr_base<_T2> &rhs) noexcept {
_copy_ptr_from(rhs);
add_wref();
}
template<typename _T2>
bool _construct_from_weak(const weak_ptr<_T2> &rhs) noexcept {
if (rhs._ref && rhs._ref->try_add_ref()) {
_copy_ptr_from(rhs);
return true;
}
return false;
}
};
- 注意这个
_copy_ptr_from()
函数,在刚写这个函数的时候我发现它并没有进行引用计数的增加,但是注意看:
template<typename _T2>
void _copy_construct_from_shared(const shared_ptr<_T2> &rhs) noexcept {
rhs.add_ref(); // add_ref is here
_copy_ptr_from(rhs);
}
template<typename _T2>
void _weakly_construct_from(const Ptr_base<_T2> &rhs) noexcept {
_copy_ptr_from(rhs);
add_wref();
}
可以看到两个函数调用_copy_ptr_from()
的顺序略有不同。
在_copy_construct_from_shared()
中,首先调用rhs.add_ref()
是至关重要的,以确保在任何可能的异常抛出之前增加引用计数,避免资源泄露。
对于_weakly_construct_from()
,先复制指针和引用计数器信息,然后通过add_wref()
增加弱引用计数是符合逻辑的,因为这个操作不涉及改变资源的拥有权,只是注册了一个新的观察者。
不在_copy_ptr_from()
增加引用计数的原因,也就不言而明了。
-
_copy_construct_from_shared()
中使用rhs.add_ref()
而非成员函数add_ref()
在_copy_construct_from_shared()
中,调用rhs.add_ref()
是为了增加传入的shared_ptr (rhs)
对象所管理资源的引用计数。这是因为我们正在创建一个新的智能指针,它将共享rhs
指向的资源,因此必须表明现在有另一个所有者(即当前对象)也在使用该资源。
使用rhs.add_ref()
而不是this->add_ref()
确保了正确地修改了rhs
所关联的引用计数器,而不是错误地尝试修改还未完全构建或配置好的当前对象的引用计数器。 -
_weakly_construct_from
中使用自己的成员函数add_wref()
而非rhs.add_wref()
这里,使用add_wref()
是为了增加当前对象对资源的弱引用计数,因为该函数设计用来在复制或移动构造过程中设置当前对象的状态。当从一个weak_ptr
构造时,我们希望表明当前对象(作为一个新的弱引用所有者)也关联到了相同的资源,但不增加资源的强引用计数。 -
右值引用和万能转发
跟智能指针本身的实现没什么大关系,不过算是一个C++八股,就刚好顺着记一记。
template<typename _T2>
void _move_construct_from(Ptr_base<_T2> &&rhs) noexcept {
_copy_ptr_from(rhs);
rhs._ptr = nullptr;
rhs._ref = nullptr;
}
这里的_move_construct_from()
函数接受一个右值引用,这是C++11的特性,用来表示一个临时对象。用于支持移动语义和完美转发。当你看到一个函数参数类型为Type &&时,这意味着它可以绑定到一个即将被销毁或不再需要的临时对象上(一个右值),允许在不复制数据的情况下转移资源所有权。
在这里,原来的对象rhs
是一个即将被销毁的临时对象,我们可以直接从它那里“偷走”资源,而不需要复制数据来达到move的效果。这样可以避免不必要的内存分配和数据复制,提高程序的性能。
shared_ptr
template <typename T>
struct shared_ptr : public Ptr_base<T> {
public:
using Base = Ptr_base<T>;
shared_ptr() noexcept = default;
explicit shared_ptr(std::nullptr_t) noexcept {}
explicit shared_ptr(T *raw_ptr) {
Base::_init(raw_ptr);
if constexpr (can_enable_shared<T>) {
raw_ptr->_weak_this_ = *this;
}
}
shared_ptr(const shared_ptr &rhs) noexcept {
Base::_copy_construct_from_shared(rhs);
}
shared_ptr(shared_ptr &&rhs) noexcept {
Base::_move_construct_from(std::move(rhs));
}
shared_ptr &operator=(const shared_ptr &rhs) {
shared_ptr(rhs).swap(*this);
return *this;
}
shared_ptr &operator=(shared_ptr &&rhs) noexcept {
shared_ptr(std::move(rhs)).swap(*this);
return *this;
}
template <typename T2>
explicit shared_ptr(const weak_ptr<T2> &rhs) noexcept { // avoid implicit conversion
Base::_construct_from_weak(rhs);
}
~shared_ptr() {
Base::sub_ref();
}
T &operator*() noexcept {
return *Base::_ptr;
}
const T &operator*() const noexcept {
return *Base::_ptr;
}
T *operator->() {
return &*Base::_ptr;
}
const T *operator->() const {
return &*Base::_ptr;
}
explicit operator bool() const {
return (Base::_ptr != nullptr);
}
};
由于基类的函数基本都实现得很完善,所以shared_ptr
的实现就显得很简单了。
这里讲一个地方:
shared_ptr &operator=(const shared_ptr &rhs) {
shared_ptr(rhs).swap(*this);
return *this;
}
shared_ptr &operator=(shared_ptr &&rhs) noexcept {
shared_ptr(std::move(rhs)).swap(*this);
return *this;
}
一般来说移动构造函数需要加上noexcept
,因为一般移动构造函数与析构函数提供了强异常安全保证,而它保证强异常安全的方式,就是移动noexcept
。若一定要抛出异常,则破坏了它的强异常安全保证。(例如在移动时已经申请了新的空间,这时抛出了异常…就会导向不可知的结果)
重点说一下这个函数
shared_ptr &operator=(shared_ptr &&rhs) noexcept {
shared_ptr(std::move(rhs)).swap(*this);
return *this;
}
-
首先,
std::move()
将rhs
转换为右值引用,这样我们就可以调用shared_ptr
的移动构造函数了。、 -
shared_ptr(std::move(rhs))
创建了一个匿名的临时对象,这个对象现在拥有了rhs
的资源,而rhs
自己则变成了一个空指针。 -
swap(*this)
交换了当前对象和临时对象的资源,这样当前对象就拥有了rhs
的资源,而临时对象则拥有了当前对象的资源。 -
最后,返回当前对象的引用。
Q: 为什么还需要std::move()即使参数已经是右值引用(&&rhs)?
A: 这是因为在函数参数列表中,rhs
是一个右值引用,但在函数体内,它是一个左值。这是因为在函数体内,rhs
是一个有名字的变量,而有名字的变量都是左值。因此,我们需要使用std::move()
来将rhs
转换为右值引用,这样我们就可以调用shared_ptr
的移动构造函数了。
weak_ptr
template <typename T>
struct weak_ptr : public Ptr_base<T> {
public:
using Base = Ptr_base<T>;
weak_ptr() noexcept = default;
constexpr weak_ptr(std::nullptr_t) noexcept {}
weak_ptr(const weak_ptr &rhs) {
Base::_weakly_construct_from(rhs);
}
weak_ptr(const shared_ptr<T> &rhs) {
Base::_weakly_construct_from(rhs);
}
weak_ptr &operator=(const weak_ptr &rhs) {
weak_ptr(rhs).swap(*this);
return *this;
}
~weak_ptr() {
Base::sub_wref();
}
shared_ptr<T> lock() const noexcept {
shared_ptr<T> ret;
ret._construct_from_weak(*this);
return ret;
}
bool expired() const {
return Base::_ref->use_count() == 0;
}
};
注意看这个地方:
shared_ptr &operator=(const shared_ptr &rhs) {
shared_ptr(rhs).swap(*this);
return *this;
}
weak_ptr &operator=(const weak_ptr &rhs) {
weak_ptr(rhs).swap(*this);
return *this;
}
如何去理解这里的copy-and-swap呢?
从直觉上来说,赋值运算符居然要用swap
,这多少显得有点怪异。
但是考虑这样一个例子,
a = b
这里的a
和b
并不是普通的整形变量,而是指向特定资源的指针。当将b赋值给a后,a就不再指向原来的资源A了,而是指向了b所指向的资源B。因此,对A的引用计数需要减一,对B的引用计数需要加一。这样的操作通过swap
函数来实现。
在copy-and-swap操作中,shared_ptr(rhs)
先创建了一个临时对象,命中了
shared_ptr(const shared_ptr &rhs) noexcept {
Base::_copy_construct_from_shared(rhs);
}
这个构造函数,相当于copy了一个rhs
,使赋值符号右边的rhs
的引用计数加一。然后,将这个引用计数加一过后的rhs
和*this
进行swap,这样*this
就指向了rhs
所指向的资源(引用计数已经加一了),而rhs
则指向了*this
原来指向的资源。
最后,当创建的临时对象生命周期结束(即离开了作用域,这里是return后),它的析构函数会将rhs
指向的资源的引用计数减一(此时已经是A的引用计数了),从而达到想要的目的。
~shared_ptr() {
Base::sub_ref();
}
make_shared
template <typename T, typename... Ts>
shared_ptr<T> make_shared(Ts &&...args) {
return shared_ptr<T>{new T(std::forward<Ts>(args)...)};
}
这里,std::forward<Ts>(args)...
确保args...
中的每个参数都以其原始的值类别(lvalue或rvalue)传递给T的构造函数,比如如果任何args是临时对象(rvalues),它们将通过移动构造(如果可用)传递给T的构造函数,从而提高效率。如果args是lvalues,它们将通过常规的拷贝构造传递。
enable_shared_from_this
template <typename T>
struct enable_shared_from_this {
mutable weak_ptr<T> _weak_this_;
protected:
constexpr enable_shared_from_this() noexcept :
_weak_this_() {
}
enable_shared_from_this(const enable_shared_from_this &) noexcept {
}
enable_shared_from_this &operator=(const enable_shared_from_this &) noexcept {
return *this;
}
~enable_shared_from_this() = default;
public:
shared_ptr<T> shared_from_this() {
return shared_ptr<T>(_weak_this_);
}
shared_ptr<T const> shared_from_this() const {
return shared_ptr<T const>(_weak_this_);
}
friend class shared_ptr<T>;
};
}