C++
手写 std::unique_ptr
通过独占所有权、移动语义、deleter 压缩和数组特化,完整实现 unique_ptr、release/reset 与 make_unique。
发布于 2026年7月23日
手写 std::unique_ptr
通过独占所有权、移动语义、deleter 压缩和数组特化,完整实现 unique_ptr、release/reset 与 make_unique。
本系列代码使用 C++20 和
oc::handmade命名空间,目标是解释实现机制、复杂度和工程边界,不是替代标准库。智能指针管理生命周期,但不会自动解决业务对象的线程安全。
一、学习目标
- 用类型系统表达独占所有权并禁止复制
- 正确实现移动、release、reset、swap 和自定义 deleter
- 区分单对象与未知边界数组特化,并实现 make_unique
二、前置条件
完成 unordered_map 篇并熟悉 RAII、移动语义、delete/delete[]、空基类优化与完美转发。
Linux/macOS:
g++ -std=c++20 -Wall -Wextra -Wpedantic -Werror smart_ptrs_test.cpp -o smart_ptrs_test
./smart_ptrs_test
Windows PowerShell:
cl /std:c++20 /W4 /WX /EHsc smart_ptrs_test.cpp
.\smart_ptrs_test.exe
三、问题与设计选择
对象版保存 T* 与 deleter,使用 [[no_unique_address]] 压缩无状态 deleter;数组版只提供 operator[],并固定使用 delete[]。移动操作转移指针后清空源对象,release 只放弃所有权而不销毁资源。
教学实现覆盖核心所有权语义、复制或移动、观察接口、异常回滚与主要辅助函数;不复刻标准库的完整转换构造矩阵、allocator 定制、enable_shared_from_this、owner 比较器和 ABI 细节。
四、内存布局与核心不变量
任意时刻至多一个 unique_ptr 拥有某个资源;空对象的指针为 nullptr;非空对象析构或 reset 时恰调用一次与分配方式匹配的 deleter。
判断实现是否正确时,不只数对象指针,还要分别追踪“资源是否活着”“拥有者数量”和“控制块是否活着”。三者的生命周期边界并不相同。
五、核心实现
unique_ptr(const unique_ptr&) = delete;
unique_ptr& operator=(const unique_ptr&) = delete;
unique_ptr(unique_ptr&& other) noexcept
: pointer_(other.release()),
deleter_(std::move(other.deleter_)) {}
pointer release() noexcept {
return std::exchange(pointer_, nullptr);
}
void reset(pointer replacement = nullptr) noexcept {
pointer old = std::exchange(pointer_, replacement);
if (old) deleter_(old);
}
这段代码展示最容易写错的状态转换。完整实现放在下一节,并与另外两种智能指针共用同一份 smart_ptrs.hpp。
六、完整教学实现
namespace oc::handmade {
template<class T>
struct default_delete {
constexpr default_delete() noexcept = default;
template<class U>
requires std::convertible_to<U*, T*>
constexpr default_delete(const default_delete<U>&) noexcept {}
void operator()(T* pointer) const noexcept {
static_assert(sizeof(T) > 0, "cannot delete an incomplete type");
delete pointer;
}
};
template<class T>
struct default_delete<T[]> {
constexpr default_delete() noexcept = default;
template<class U>
requires std::convertible_to<U (*)[], T (*)[]>
constexpr default_delete(const default_delete<U[]>&) noexcept {}
template<class U>
requires std::convertible_to<U (*)[], T (*)[]>
void operator()(U* pointer) const noexcept {
static_assert(sizeof(U) > 0, "cannot delete an incomplete type");
delete[] pointer;
}
};
template<class T, class Deleter = default_delete<T>>
class unique_ptr {
static_assert(
std::is_nothrow_move_constructible_v<Deleter>
&& std::is_nothrow_move_assignable_v<Deleter>,
"the teaching implementation requires a non-throwing movable deleter"
);
public:
using element_type = T;
using pointer = T*;
using deleter_type = Deleter;
constexpr unique_ptr() noexcept
requires std::default_initializable<Deleter>
= default;
explicit constexpr unique_ptr(pointer value) noexcept
requires std::default_initializable<Deleter>
: pointer_(value) {}
constexpr unique_ptr(pointer value, const Deleter& deleter) noexcept
: pointer_(value), deleter_(deleter) {}
constexpr unique_ptr(pointer value, Deleter&& deleter) noexcept
: pointer_(value), deleter_(std::move(deleter)) {}
unique_ptr(const unique_ptr&) = delete;
unique_ptr& operator=(const unique_ptr&) = delete;
constexpr unique_ptr(unique_ptr&& other) noexcept
: pointer_(other.release()), deleter_(std::move(other.deleter_)) {}
constexpr unique_ptr& operator=(unique_ptr&& other) noexcept {
if (this != &other) {
reset(other.release());
deleter_ = std::move(other.deleter_);
}
return *this;
}
constexpr ~unique_ptr() {
if (pointer_) deleter_(pointer_);
}
[[nodiscard]] constexpr pointer get() const noexcept { return pointer_; }
[[nodiscard]] constexpr Deleter& get_deleter() noexcept { return deleter_; }
[[nodiscard]] constexpr const Deleter& get_deleter() const noexcept {
return deleter_;
}
[[nodiscard]] explicit constexpr operator bool() const noexcept {
return pointer_ != nullptr;
}
[[nodiscard]] constexpr T& operator*() const noexcept { return *pointer_; }
[[nodiscard]] constexpr pointer operator->() const noexcept { return pointer_; }
[[nodiscard]] constexpr pointer release() noexcept {
return std::exchange(pointer_, nullptr);
}
constexpr void reset(pointer replacement = nullptr) noexcept {
pointer old = std::exchange(pointer_, replacement);
if (old) deleter_(old);
}
constexpr void swap(unique_ptr& other) noexcept {
using std::swap;
swap(pointer_, other.pointer_);
swap(deleter_, other.deleter_);
}
private:
pointer pointer_{};
[[no_unique_address]] Deleter deleter_{};
};
template<class T, class Deleter>
class unique_ptr<T[], Deleter> {
static_assert(
std::is_nothrow_move_constructible_v<Deleter>
&& std::is_nothrow_move_assignable_v<Deleter>,
"the teaching implementation requires a non-throwing movable deleter"
);
public:
using element_type = T;
using pointer = T*;
using deleter_type = Deleter;
constexpr unique_ptr() noexcept
requires std::default_initializable<Deleter>
= default;
explicit constexpr unique_ptr(pointer value) noexcept
requires std::default_initializable<Deleter>
: pointer_(value) {}
constexpr unique_ptr(pointer value, Deleter deleter) noexcept
: pointer_(value), deleter_(std::move(deleter)) {}
unique_ptr(const unique_ptr&) = delete;
unique_ptr& operator=(const unique_ptr&) = delete;
constexpr unique_ptr(unique_ptr&& other) noexcept
: pointer_(other.release()), deleter_(std::move(other.deleter_)) {}
constexpr unique_ptr& operator=(unique_ptr&& other) noexcept {
if (this != &other) {
reset(other.release());
deleter_ = std::move(other.deleter_);
}
return *this;
}
constexpr ~unique_ptr() {
if (pointer_) deleter_(pointer_);
}
[[nodiscard]] constexpr pointer get() const noexcept { return pointer_; }
[[nodiscard]] explicit constexpr operator bool() const noexcept {
return pointer_ != nullptr;
}
[[nodiscard]] constexpr T& operator[](std::size_t index) const noexcept {
return pointer_[index];
}
[[nodiscard]] constexpr pointer release() noexcept {
return std::exchange(pointer_, nullptr);
}
constexpr void reset(pointer replacement = nullptr) noexcept {
pointer old = std::exchange(pointer_, replacement);
if (old) deleter_(old);
}
constexpr void swap(unique_ptr& other) noexcept {
using std::swap;
swap(pointer_, other.pointer_);
swap(deleter_, other.deleter_);
}
private:
pointer pointer_{};
[[no_unique_address]] Deleter deleter_{};
};
template<class T, class... Args>
requires (!std::is_array_v<T>)
[[nodiscard]] unique_ptr<T> make_unique(Args&&... args) {
return unique_ptr<T>(new T(std::forward<Args>(args)...));
}
template<class T>
requires std::is_unbounded_array_v<T>
[[nodiscard]] unique_ptr<T> make_unique(std::size_t size) {
using element_type = std::remove_extent_t<T>;
return unique_ptr<T>(new element_type[size]());
}
} // namespace oc::handmade
统一测试覆盖移动、复制、数组、别名所有权、对象析构、过期观察者和失败提升;实现没有向 std 命名空间注入任何类型。
七、使用示例与输出
预期输出或状态:
移动后源指针为空;release 后资源仍存活;手工 delete 后存活计数回到 0;数组初始值为 0,values[1] 为 42。
示例使用断言验证状态,因此成功时不打印额外文本。析构计数必须回到 0,才能说明资源和控制块路径已经闭合。
八、复杂度与失效规则
| 操作 | 复杂度 | 说明 |
|---|---|---|
| get/operator*/operator-> | O(1) | 不延长资源生命周期 |
| 移动构造/移动赋值 | O(1) | 源对象变为空 |
| release | O(1) | 调用者接管裸指针 |
| reset/析构 | O(1)+deleter | 旧资源立即销毁 |
| make_unique<T[]> | O(n) | 值初始化 n 个元素 |
智能指针本身没有容器迭代器;这里的“失效”指资源销毁后,裸指针、引用或观察者能否继续安全使用。任何从 get()、operator* 或 operator-> 取得的非拥有访问,都不能延长对象生命周期。
九、异常安全与资源管理
make_unique把分配和所有权建立放在同一表达式中,构造失败时由new自动回收存储。reset先保存旧指针再提交新指针,避免 deleter 读取已覆盖状态。- 教学版要求 deleter 可无异常移动;带状态 deleter 的移动后语义必须由其类型保证。
release不销毁资源,调用者必须立即把返回值交给另一个 RAII 所有者。
析构和引用计数释放路径必须为 noexcept。用户 deleter 若从析构路径抛出,程序无法可靠恢复,因此教学实现要求 deleter 的调用和移动不抛异常。
十、常见错误
1. 把 release 当成 reset
release() 不会调用 deleter;忽略返回值会稳定地产生泄漏。
2. 混用 delete 与 delete[]
单对象和数组特化故意分开,分配与释放形式不匹配属于未定义行为。
3. 从 get() 建立第二个所有者
用同一裸指针构造另一个智能指针会导致两个所有者分别释放同一资源。
十一、面试追问
unique_ptr为什么可以零开销地表达所有权?[[no_unique_address]]对无状态 deleter 有什么作用?release、reset和移动构造的所有权变化分别是什么?- 为什么
unique_ptr<T[]>没有operator*与operator->?
回答时先说明所有权,再画出对象与控制块,最后给出原子计数、异常和并发边界。
十二、练习与自测
- 增加从
unique_ptr<Derived>到unique_ptr<Base>的受约束移动转换 - 实现函数指针 deleter,并比较对象大小
- 用一个第 N 次构造抛异常的数组元素验证 make_unique 回滚
自测标准:能够不看代码画出每次复制、移动、reset、lock 和析构后的强弱计数,并解释最后由谁销毁对象、由谁释放控制块。
十三、官方资料与延伸阅读
上一篇:手写 std::unordered_map | 下一篇:手写 std::shared_ptr