这是视频笔记,编译版是Visual Studio 2022提供的MSVC v143,C 版本是20,也可以使用其他版本,因为智能指针的源代码没有使用新的C 特性。
这段视频讲的是unique_ptr。在看源码之前,建议你先看一遍C reference关于unique_ptr了解它的接口。
unique_ptr原理很简单,就是RAII,资源通过智能指针的结构和分析进行管理。在构造过程中分配和创建管理对象,在构造过程中销毁和释放管理对象。源代码中使用了大量的元编程,很难阅读。
VS有书签,大家可以把关键的代码标记一下,方便查找,因为unique_ptr源代码分散在许多地方。VS有些快捷键也很方便,比如转到定义,找到所有引用,向前导航,向后导航。我把所有这些快捷键都绑在鼠标上,因为鼠标上有很多键。
阅读源码会有点烦人。我建议你先把它放在一边unique_ptr再次阅读源代码的精度,使以后阅读更加复杂shared_ptr和weak_ptr会轻松很多。
声明
首先来看unique_ptr的声明:
template <class _Ty, class _Dx = default_delete<_Ty>> class unique_ptr;
模板有两个参数:
- _Ty:管理对象的类型。
- _Dx:删除器的类型。
删除器用于销毁和释放管理对象,可以是以下类型:
- 函数对象。
- 引用函数对象的左值。
- 引用函数的左值。
函数对象可以像函数一样使用函数调用运算符(提供operator()函数)被调用的对象。函数的指针,仿函数,闭包,std::function它们都是函数对象。请注意,函数和函数的引用不是函数对象,可以在需要函数对象的地方隐式转换为函数指针(退化)。这些概念将在后面使用。
请注意,函数不能作为模板参数,因为函数存储在过程地址空间的代码区域,不能直接存储在类别或对象中。但是,函数的引用可以作为模板参数,因为对象可以使用成员变量来保存引用。
default_delete
unique_ptr提供默认删除器:
template <class _Ty> struct default_delete; template <class _Ty> struct default_delete {
// default deleter for unique_ptr constexpr default_delete() noexcept = default; template <class _Ty2, enable_if_t<is_convertible_v<_Ty2*, _Ty*>, int> = 0> default_delete(const default_delete<_Ty2>&) noexcept {
} void operator()(_Ty* _Ptr) const noexcept /* strengthened */ {
// delete a pointer static_assert(0 < sizeof(_Ty), "can't delete an incomplete type");
delete _Ptr;
}
};
default_delete也是模板类,模板参数是被管理对象的类型。内部定义了3个成员函数:
- 默认构造函数。这里直接使用了default,还添加了两个说明符:constexpr和noexcept,这两个关键字在标准库中经常出现,因为标准库是非常注重性能的。后面还定义了其他的构造函数,所以编译器不会帮我们生成默认构造函数,那么这里就必须自己定义了(使用default)。
- 拷贝构造函数。这是模板函数,模板参数是另一个unique_ptr的被管理对象的类型。这个函数是为转移管理权提供服务的,将被管理对象的管理权从旧的unique_ptr转移到新的unique_ptr。这里使用SFINAE对管理权的转移做了限制,旧的被管理对象的指针必须能隐式转换成新的被管理对象的指针,这和普通指针的行为是一样的。通过其他类型的default_delete来拷贝构造、拷贝赋值、移动构造、移动赋值时,就会调用这个函数来检查(编译期)。使用SFINAE是为了在编辑代码时就给用户提示,而不是在编译时才提示。注意,C++20提供了更加好用的concept来代替SFINAE,后面的视频也会使用concept来仿写智能指针。
- 函数调用运算符。unique_ptr使用这个函数来销毁和释放被管理对象。因为是默认的删除器,这里就直接使用了delete。这里使用了const说明符,因为这个函数没有改变自己的成员变量(其实根本没有成员变量)。这里使用了noexcept说明符,注释中的
strengthened
说明C++标准并没有这么要求,而是MSVC的做法。调用这个函数的时机有3个:unique_ptr被销毁,管理权被转移(通过operator=),用户手动销毁被管理对象(通过reset()函数)。
这里提到了很多概念,不理解也没关系,后面还会反复提及。
定义
然后来看uniqur_ptr的定义:
template <class _Ty, class _Dx /* = default_delete<_Ty> */>
class unique_ptr {
// non-copyable pointer to an object
};
template <class _Ty, class _Dx>
class unique_ptr<_Ty[], _Dx> {
// non-copyable pointer to an array object
};
unique_ptr有两个版本:管理单个对象,管理数组。通过模板偏特化来区分。数组一般通过std::vector和std::string这样的容器来管理,所以这里只看管理单个对象的版本。
_Compressed_pair
unique_ptr只有一个成员:
private:
_Compressed_pair<_Dx, pointer> _Mypair;
};
unique_ptr需要在内部保存指针和删除器。为了性能,没有直接保存,而是通过_Compressed_pair
来保存:
template <class _Ty1, class _Ty2, bool = is_empty_v<_Ty1> && !is_final_v<_Ty1>>
class _Compressed_pair final : private _Ty1 {
// store a pair of values, deriving from empty first
public:
_Ty2 _Myval2;
using _Mybase = _Ty1; // for visualization
template <class... _Other2>
constexpr explicit _Compressed_pair(_Zero_then_variadic_args_t, _Other2&&... _Val2) noexcept(
conjunction_v<is_nothrow_default_constructible<_Ty1>, is_nothrow_constructible<_Ty2, _Other2...>>)
: _Ty1(), _Myval2(_STD forward<_Other2>(_Val2)...) {
}
template <class _Other1, class... _Other2>
constexpr _Compressed_pair(_One_then_variadic_args_t, _Other1&& _Val1, _Other2&&... _Val2) noexcept(
conjunction_v<is_nothrow_constructible<_Ty1, _Other1>, is_nothrow_constructible<_Ty2, _Other2...>>)
: _Ty1(_STD forward<_Other1>(_Val1)), _Myval2(_STD forward<_Other2>(_Val2)...) {
}
constexpr _Ty1& _Get_first() noexcept {
return *this;
}
constexpr const _Ty1& _Get_first() const noexcept {
return *this;
}
};
template <class _Ty1, class _Ty2>
class _Compressed_pair<_Ty1, _Ty2, false> final {
// store a pair of values, not deriving from first
public:
_Ty1 _Myval1;
_Ty2 _Myval2;
template <class... _Other2>
constexpr explicit _Compressed_pair(_Zero_then_variadic_args_t, _Other2&&... _Val2) noexcept(
conjunction_v<is_nothrow_default_constructible<_Ty1>, is_nothrow_constructible<_Ty2, _Other2...>>)
: _Myval1(), _Myval2(_STD forward<_Other2>(_Val2)...) {
}
template <class _Other1, class... _Other2>
constexpr _Compressed_pair(_One_then_variadic_args_t, _Other1&& _Val1, _Other2&&... _Val2) noexcept(
conjunction_v<is_nothrow_constructible<_Ty1, _Other1>, is_nothrow_constructible<_Ty2, _Other2...>>)
: _Myval1(_STD forward<_Other1>(_Val1)), _Myval2(_STD forward<_Other2>(_Val2)...) {
}
constexpr _Ty1& _Get_first() noexcept {
return _Myval1;
}
constexpr const _Ty1& _Get_first() const noexcept {
return _Myval1;
}
};
_Compressed_pair是一个pair,不仅可以保存两个对象,还有压缩的功能。模板参数有两个:删除器的类型,指针的类型。删除器可能是空类,比如默认的default_delete,这时删除器会占用一个字节的内存。如果_Compressed_pair直接将空类的删除器声明为自己的成员变量,那么就会占用4或8个字节的空间。_Compressed_pair通过让自己继承空类的删除器,来将多余的字节省去。这是通过EBO实现的。_Compressed_pair有两个版本:
- 主模板类:通过继承来保存空类的删除器,通过成员变量来保存指针。
- 偏特化类:通过成员变量来保存删除器和指针。
这两个版本的区别在于对删除器的操作不同,而对指针的操作是一样的。
_Compressed_pair有两个构造函数,都是模板函数,模板的参数有两个:_Other1,_Other2。_Other1是初始化删除器用的,_Other2是初始化指针用的。为什么有两个版本呢?首先,用户构造unique_ptr的时候可能不会传入自定义的删除器,这时unique_ptr会使用默认的删除器。那么unique_ptr的构造函数就有两种:没有传入删除器的版本,传入删除器的版本。_Compressed_pair的构造函数也就有两种:没有传入删除器的版本,传入删除器的版本。也就是说_Other1可能为空,也可能不为空。其次,用户构造unique_ptr的时候可能不会传入指针,这时unique_ptr会使用空指针。那么unique_ptr的构造函数就有两种:没有传入指针的版本,传入指针的版本。_Compressed_pair的构造函数也就有两种:没有传入指针的版本,传入指针的版本。也就是说_Other2可能为空,也可能不为空。综上所述,_Compressed_pair的构造有4种情况:
- _Other1空,_Other2空
- _Other1空,_Other2不空
- _Other1不空,_Other2空
- _Other1不空,_Other2不空
只使用一个构造函数能应对所有的情况吗?不能。因为使用一个构造函数是这样的:
public:
template <class... _Other1, class... _Other2>
_Compressed_pair(_Other1&&... _Val1, _Other2&&... _Val2);
所有的参数都会被_Other2捕获。那么就需要使用两个构造函数了,_Compressed_pair根据_Other1是否为空来区分两个构造函数,那么构造函数就会像这样:
public:
template <class... _Other2>
_Compressed_pair(_Other2&&... _Val2);
template <class _Other1, class... _Other2>
_Compressed_pair(_Other1&& _Val1, _Other2&&... _Val2);
情况1会选择版本1,其他情况会选择版本2。这是有问题的。既然编译器不能自动选择正确的版本,那么就需要户来手动选择了。_Compressed_pair通过标签分派来帮助编译器选择正确的版本:
struct _Zero_then_variadic_args_t {
explicit _Zero_then_variadic_args_t() = default;
}; // tag type for value-initializing first, constructing second from remaining args
struct _One_then_variadic_args_t {
explicit _One_then_variadic_args_t() = default;
}; // tag type for constructing first from one arg, constructing second from remaining args
这样就形成了源码中的构造函数。其实只使用一个标签就够了,用户不使用版本1就表明希望使用版本2。版本2也没必要使用可变参,因为删除器在unique_ptr中是作为第二个参数传入的,第一个参数是指针,用户肯定在传入删除器的同时也传入了指针,那么_Other2就不可能为空。
_Compressed_pair的构造函数有noexcept声明, 只有当删除器和指针的构造都不抛出异常时,_Compressed_pair的构造才能保证不会抛出异常。
下面来看怎么访问_Compressed_pair保存的删除器和指针。指针被声明成public成员,可以直接访问。删除器有两种情况。版本2的删除器被声明成public成员,可以直接访问。版本1的删除器是_Compressed_pair的空基类子对象,需要对_Compressed_pair进行类型转换才能访问。为了使用方便,_Compressed_pair为两种情况提供了统一的接口。注意,_Compressed_pair提供了非const和const版本的接口,而unique_ptr并不会用到const版本的_Compressed_pair。const版本返回的是const删除器,因为const _Compressed_pair不能修改自己的成员。
成员类型
unique_ptr定义了3个类型:
public:
using pointer = get_deleter_pointer_type<T, std::remove_reference_t<D>>::type;
using element_type = T;
using deleter_type = D;
分别是:指针的类型,被管理对象的类型,删除器的类型。get_deleter_pointer_type用来获取指针的类型,有两个版本,通过SFINAE来选择,如果删除器定义了指针的类型,则使用该类型,否则使用普通的指针:
template <class T, class D, class = void>
struct get_deleter_pointer_type {
using type = T*;
};
template <class T, class D>
struct get_deleter_pointer_type<T, D, std::void_t<typename D::pointer>> {
using type = D::pointer;
};
成员函数(拷贝)
unique_ptr只能移动,不能拷贝:
public:
unique_ptr(const unique_ptr&) = delete;
unique_ptr& operator=(const unique_ptr&) = delete;
成员函数(修改)
release:释放管理权。将unique_ptr保存的指针和空指针交换,然后返回之前保存的指针。
public:
pointer release() noexcept {
return _STD exchange(_Mypair._Myval2, nullptr);
}
reset:重置被管理对象。将unique_ptr保存的指针和用户传入的指针(默认为空)交换,然后调用保存的删除器来删除之前的被管理对象。注意,reset没有自检,执行reset(get())
是危险的,而执行reset(release())
是没有危险的。reset(get())
和reset()
一样都会删除被管理对象,但不会将指针置空。reset(release())
将管理权从unique_ptr转移到用户,再从用户转移回unique_ptr。注意,reset只是重置被管理对象,如果希望将删除器也一并重置,应该使用move。
public:
void reset(pointer _Ptr = nullptr) noexcept {
pointer _Old = _STD exchange(_Mypair._Myval2, _Ptr);
if (_Old) {
_Mypair._Get_first()(_Old);
}
}
swap:交换被管理对象和删除器。和另一个unique_ptr交换指针和删除器。注意,如果删除器的类型不是引用,则交换的是unique_ptr保存的删除器,如果删除器的类型是引用,则交换的是被引用的删除器。因为引用在初始化后就不能更改绑定的对象了,对引用的更改都是对原始对象的更改。
public:
void swap(unique_ptr& _Right) noexcept {
_Swap_adl(_Mypair._Myval2, _Right._Mypair._Myval2);
_Swap_adl(_Mypair._Get_first(), _Right._Mypair._Get_first());
}
成员函数(观察)
get:获取指针。
public:
_NODISCARD pointer get() const noexcept {
return _Mypair._Myval2;
}
get_deleter:获取删除器。const版本返回的是const删除器,因为const unique_ptr不能修改自己的成员变量。
public:
_NODISCARD _Dx& get_deleter() noexcept {
return _Mypair._Get_first();
}
_NODISCARD const _Dx& get_deleter() const noexcept {
return _Mypair._Get_first();
}
bool:检查是否管理了对象。
public:
explicit operator bool() const noexcept {
return static_cast<bool>(_Mypair._Myval2);
}
operator*:解引用指针。在C++标准中,如果指针为空,解引用会抛出异常,但是MSVC在这里添加了noexcept说明符,而C++ reference中是noexcept(noexcept(*std::declval<pointer>()))
。返回的是引用,但是没有添加const说明符,这和普通指针的行为是一样的。 operator->:获取指针。和普通指针的行为一样。
public:
_NODISCARD add_lvalue_reference_t<_Ty> operator*() const noexcept /* strengthened */ {
return *_Mypair._Myval2;
}
_NODISCARD pointer operator->() const noexcept {
return _Mypair._Myval2;
}
析构函数
析构函数:删除被管理对象。
public:
~unique_ptr() noexcept {
if (_Mypair._Myval2) {
_Mypair._Get_first()(_Mypair._Myval2);
}
}
构造函数(默认)
unique_ptr():默认构造函数。用户没有传入指针和删除器,所以使用空指针和默认删除器,调用_Compressed_pair的版本1构造函数来初始化_Mypair。 unique_ptr(nullptr_t):参数为空指针,行为和默认构造函数一样。unique_ptr(pointer _Ptr)
可以实现这个函数的功能,但是这里可以添加constexpr说明符,提高性能。
public:
template <class _Dx2 = _Dx, _Unique_ptr_enable_default_t<_Dx2> = 0>
constexpr unique_ptr() noexcept : _Mypair(_Zero_then_variadic_args_t{
}) {
}
template <class _Dx2 = _Dx, _Unique_ptr_enable_default_t<_Dx2> = 0>
constexpr unique_ptr(nullptr_t) noexcept : _Mypair(_Zero_then_variadic_args_t{
}) {
}
_Unique_ptr_enable_default_t用来约束删除器的类型,删除器需要满足两个条件:不是指针,可以默认构造。如果删除器的类型为指针,而指针会默认初始化为空,那么unique_ptr就不能正确地删除被管理对象了。删除器需要能够默认构造,所以它的类型不能是引用。这里使用conjunction_v而不是&&,是因为conjunction_v可以短路求值。
template <class _Dx2>
using _Unique_ptr_enable_default_t =
enable_if_t<conjunction_v<negation<is_pointer<_Dx2>>, is_default_constructible<_Dx2>>, int>;
在C++20中,可以使用concept来代替SFINAE:
template <class _Dx2>
concept _Unique_ptr_enable_default_t =
!is_pointer_v<_Dx2> && is_default_constructible_v<_Dx2>;
public:
template <_Unique_ptr_enable_default_t _Dx2 = _Dx>
constexpr unique_ptr() noexcept : _Mypair(_Zero_then_variadic_args_t{
}) {
}
template <_Unique_ptr_enable_default_t _Dx2 = _Dx>
constexpr unique_ptr(nullptr_t) noexcept : _Mypair(_Zero_then_variadic_args_t{
}) {
}
构造函数(传入指针)
unique_ptr(pointer _Ptr):参数为指针,使用默认删除器。
public:
template <class _Dx2 = _Dx, _Unique_ptr_enable_default_t<_Dx2> = 0>
explicit unique_ptr(pointer _Ptr) noexcept : _Mypair(_Zero_then_variadic_args_t{
}, _Ptr) {
}
构造函数(传入指针和删除器)
指针使用值传递,删除器使用引用传递。这是因为指针占用空间很小,而删除器占用空间可能很大。删除器有拷贝构造和移动构造两种初始化方式:
public:
unique_ptr(pointer p, const D& d);
unique_ptr(pointer p, D&& d);
根据删除器的类型D
的不同,这两个构造函数会被实例化成不同的形式:
- 非引用
A
,这时构造函数为:
public:
unique_ptr(pointer p, const A& d); // const D& -> const A&
unique_ptr(pointer p, A&& d); // D&& -> A&&
- 左值引用
A&
,这时构造函数为:
public:
unique_ptr(pointer p, A& d); // const D& -> const A& & -> A&
unique_ptr(pointer p, A& d); // D&& -> A& && -> A&
- 常量左值引用
const A&
,这时构造函数为:
public:
unique_ptr(pointer p, const A& d); // const D& -> const const A& & -> const A&
unique_ptr(pointer p, const A& d); // D&& -> const A& && -> const A&
当删除器为引用时,根据引用折叠,两个构造函数都会实例化成拷贝初始化的形式。为了使移动初始化的构造函数能够正确地实例化,应该避免引用折叠,方法是将模板参数的引用去除:
public:
unique_ptr(pointer p, const D& d);
unique_ptr(pointer p, remove_reference_t<D>&& d);
当删除器为引用时,不应该使用移动构造对其初始化,方法是将其删除:
public: unique_ptr(pointer p, const D& d); template <class D2 = D> requires (!is_reference_v<D2>) unique_ptr(pointer p, D&& d); template <class D2 = D> requires is_reference_v<D2> unique_ptr(pointer, remove_reference_t<D>&&