首先尝试实现一个简易版的 std::unique_ptr<T>,我们的目标是帮助读者理解实现此类指针所需的代码逻辑,而并非鼓励替代标准库实现 —— 标准智能指针经过充分测试且稳定可靠,应当优先使用。
unique_ptr<T> 实际类型签名为 unique_ptr<T,D>,其中 D 默认为 default_deleter<T>。我们将同时实现标量版本和数组版本的特化,数组版本需要提供 operator[],而标量版本不应暴露该操作符。
首先是基础删除器实现,定义基础删除器类型(用户可自定义删除器,只需保持 operator() 签名一致):
namespace managing_memory_book {
// 基本删除器类型
template <class T>
struct deleter_pointer_wrapper {
void (*pf)(T*);
deleter_pointer_wrapper(void (*pf)(T*)) : pf{ pf } {
}
void operator()(T* p) const { pf(p); }
};
template <class T>
struct default_deleter {
void operator()(T* p) const { delete p; }
};
template <class T>
struct default_deleter<T[]> {
void operator()(T* p) const { delete[] p; }
};
// ...
}
目前我们已实现三种调用方式相同,且均为类类型的删除器(这种统一性设计的原因稍后会明朗,但请注意保持一致性往往有其价值)。
其中较特殊的是 deleter_pointer_wrapper<T>,封装了可复制的状态(函数指针),但调用行为与其他两种删除器一致:当对 T* 调用时,会执行用户提供的函数操作。
下一步是确定 unique_ptr<T,D> 的实现形式。考虑到大多数删除器是无状态的,可利用空基类优化(EBO) —— 直接从删除器类型继承。唯一例外是,当删除器为函数指针时(因函数指针不能作为基类),此时改为从 deleter_pointer_wrapper<T> 继承。为了在这两种方案中选择,需要检测 D 是否为函数指针,这将通过自定义的 is_deleter_function_candidate<T> 类型特征实现。
检测函数指针类删除器的实现部分如下:
#include <type_traits>
namespace managing_memory_book {
// ...
template <class T>
struct is_deleter_function_candidate
: std::false_type {};
template <class T>
struct is_deleter_function_candidate<void (*)(T*)>
: std::true_type {};
template <class T>
constexpr auto is_deleter_function_candidate_v =
is_deleter_function_candidate<T>::value;
// ...
}
这段代码的含义应该不言自明 —— 其核心思想是:大多数类型都不适合作为删除函数,但符合void(*)(T*)签名的函数指针类型除外。
接下来我们实现通用的unique_ptr<T>标量版本。通过之前实现的删除器函数检测特性,可以条件选择基类:对于普通删除器类型,直接继承D,对于函数指针类型,则继承deleter_pointer_wrapper<T>。
析构函数中,将基类指针转换为相应类型来释放资源:
namespace managing_memory_book {
// ...
// unique_ptr 通用模板
template <class T, class D = default_deleter<T>>
class unique_ptr : std::conditional_t <
is_deleter_function_candidate_v<D>,
deleter_pointer_wrapper<T>, D
> {
using deleter_type = std::conditional_t <
is_deleter_function_candidate_v<D>,
deleter_pointer_wrapper<T>,
D
>;
T* p = nullptr;
public:
unique_ptr() = default;
unique_ptr(T* p) : p{ p } {
}
unique_ptr(T* p, void (*pf)(T*))
: deleter_type{ pf }, p{ p } {
}
~unique_ptr() {
(*static_cast<deleter_type*>(this))(p);
}
};
// ...
}
本质上,同样的方法也适用于类型的T[]特化:
namespace managing_memory_book {
// ...
// unique_ptr 数组特化版
template <class T, class D>
class unique_ptr<T[], D> : std::conditional_t <
is_deleter_function_candidate_v<D>,
deleter_pointer_wrapper<T>,
D
> {
using deleter_type = std::conditional_t <
is_deleter_function_candidate_v<D>,
deleter_pointer_wrapper<T>,
D
>;
T* p = nullptr;
public:
unique_ptr() = default;
unique_ptr(T* p) : p{ p } {
}
unique_ptr(T* p, void (*pf)(T*))
: deleter_type{ pf }, p{ p } {
}
~unique_ptr() {
(*static_cast<deleter_type*>(this))(p);
}
};
}
默认的unique_ptr在概念上的行为类似于空指针,这对大多数人来说不足为奇。现在已经有了基本的概念,继续研究特定于unique_ptr的语义。
unique_ptr 标量版本和数组版本的特殊成员函数实现代码将保持一致。前文已探讨过析构函数和默认构造函数,现在成对分析其余四个函数:
禁用复制操作
该类型表示对所指对象的独占所有权
若允许复制,将导致所有权归属不明确(原对象还是复制对象?)
实现移动操作
用于完成所有权的转移
通用版本及其数组特化版本的实现代码如下(代码使用了 <utility> 头文件中的 std::exchange() 和 std::swap()):
// ...
unique_ptr(const unique_ptr&) = delete;
unique_ptr& operator=(const unique_ptr&) = delete;
void swap(unique_ptr &other) noexcept {
using std::swap;
swap(p, other.p);
}
unique_ptr(unique_ptr &&other) noexcept
: p{ std::exchange(other.p, nullptr) } {
}
unique_ptr& operator=(unique_ptr &&other) noexcept {
unique_ptr{ std::move(other) }.swap(*this);
return *this;
}
// ...
至此,大部分实现逻辑应该已经不言自明。这里使用了std::exchange(),其作用是:将other.p的值复制给this→p,然后将nullptr赋给other.p —— 这正符合所有权转移的预期行为。该类型的移动操作不仅简单高效,而且保证不会抛出异常,这两个特性都非常理想。
以下操作在通用版本和数组特化版本中都将实现:
operator bool(仅当对象非空时返回true)
empty()(仅当对象为空时返回true)
operator==()和operator!=()
这些操作的实现本质上都很简单。另一个需要提供的成员函数是get()(包括const和非const版本),用于向需要与系统调用等底层函数交互的使用端代码暴露底层指针:
// ...
bool empty() const noexcept { return !p; }
operator bool() const noexcept { return !empty(); }
bool operator==(const unique_ptr &other)
const noexcept {
return p == other.p;
}
// 自 C++20 起,可由 operator==() 推导而来
bool operator!=(const unique_ptr &other)
const noexcept {
return !(*this == other);
}
T *get() noexcept { return p; }
const T *get() const noexcept { return p; }
// ...
自C++20起,只要operator==()提供了标准签名,就无需显式实现operator!=(),编译器会自动基于operator==()合成operator!=(),这一机制非常简洁。
现在,看看指针式操作函数operator*()、operator→()和operator[]()的实现方式。
标量版本和数组版本需要实现不同的指针式操作:
标量版本需实现解引用操作符和箭头操作符
数组版本需实现下标操作符
// ...
T& operator*() noexcept { return *p; }
const T& operator*() const noexcept { return *p; }
T* operator->() noexcept { return p; }
const T* operator->() const noexcept { return p; }
// ...
operator→() 成员函数是个特殊的存在:当作用于对象时,会不断在返回对象上被重调用(持续递归),直到最终返回原始指针,编译器才知道如何继续处理。
对于数组版本(unique_ptr<T[]>特化),需要实现operator[],这比实现operator*()或operator→()更符合逻辑:
// ...
T& operator[](std::size_t n) noexcept {
return p[n];
}
const T& operator[](std::size_t n) const noexcept {
return p[n];
}
// ...
这些成员函数存在看似重复的const和非const版本,这个“惯例”始于之前介绍的get()成员函数。虽然语法相似,但其语义截然不同:只有const版本可通过const修饰的unique_ptr<T>对象调用。
如果使用C++23编译器,通过编写适当的模板成员函数,可以让编译器自动合成实际使用到的版本:
// 以下实现同时适用于数组和非数组类型
template <class U>
decltype(auto) get(this U && self) noexcept {
return self.p;
}
// 以下两个函数仅适用于非数组类型
template <class U>
decltype(auto) operator*(this U && self) noexcept {
return *(self.p);
}
template <class U>
decltype(auto) operator->(this U && self) noexcept {
return self.p;
}
// 以下函数仅适用于数组类型
template <class U>
decltype(auto) operator[](this U && self,
std::size_t n) noexcept {
return self.p[n];
}
这一机制将需要编写的成员函数数量减半。其原理在于:C++23引入了“deduced this”特性,允许开发者用this关键字显式标记成员函数的第一个参数。结合转发引用(U&&类型),编译器可自动推导this的常量性(const-ness),从而用单个函数同时表达const和非const版本。这些函数使用了decltype(auto)返回类型,能根据return语句中的表达式自动推断返回类型的cv限定符(第3章讨论过)和引用属性。
至此,已经完成了一个简单但功能完备的unique_ptr<T>基础实现,可满足大多数使用场景。尽管unique_ptr<T>非常实用,但并非万能 —— 实际编程中还存在其他需求。接下来着手实现简化版的shared_ptr<T>,看看如何实现共享所有权语义。
使用自制的unique_ptr<T>和默认删除器的简单程序:
// ... (我们自己的unique_ptr<T>在这里…)
struct X {};
int main() {
unique_ptr<X> p{ new X };
} // X::~X() called here
另一个使用自定义删除器的示例:
// ... (我们自己的unique_ptr<T>在这里…)
class X {
~X(){}
public:
static void destroy(X *p) { delete p; }
};
int main() {
unique_ptr<X, &X::destroy> p{ new X };
} // X::destroy(p.get()) 在这里调用