std::make_unique¶
| 标准 | 难度 | 状态 | 分级 | 证据 | 审阅 | 最后校验 |
|---|---|---|---|---|---|---|
| C++14 | 2/5 | final | A | 3 条(manual 3) | 单审(zyuhao7) | 2026-10-27 |
编译器支持:GCC 4.9+ · Clang 3.4+ · MSVC 18.0+。版本查询:cppreference 编译器支持。
主张与证据¶
| 主张 | 支持证据 |
|---|---|
| 用 std::make_unique 在工厂代码中创建派生段对象 | ev-mold-make-unique-section-001 |
| 用 std::make_unique 创建由框架唯一持有的线程池成员 | ev-drogon-make-unique-thread-pool-002 |
| 用 std::make_unique 从工厂函数返回多态引擎 | ev-abseil-make-unique-engine-003 |
本站证据页:ev-abseil-make-unique-engine-003 · ev-drogon-make-unique-thread-pool-002 · ev-mold-make-unique-section-001
解决什么问题¶
std::unique_ptr 解决了所有权问题,但它的构造函数仍然要求显式 new:std::unique_ptr<T>(new T(args...))。类型名写了两遍,new 与智能指针的构造分散在同一个表达式里。更麻烦的是函数实参求值顺序:f(std::unique_ptr<T>(new T), g()) 中如果 g() 先执行并抛出异常,裸指针就泄漏了。
std::make_unique 把分配和所有权包装成一个函数调用,类型只写一次,构造参数直接转发。它在 C++14 通过 N3656 进入标准库,补上了 C++11 只有 std::make_shared 的空缺。
基础用法¶
std::make_unique<T>(args...) 在堆上构造 T,返回管理它的 std::unique_ptr<T>。参数按完美转发传给构造函数。
#include <iostream>
#include <memory>
#include <string>
#include <utility>
#include <vector>
struct Session {
explicit Session(std::string name) : name(std::move(name)) {}
~Session() { std::cout << "close " << name << '\n'; }
std::string name;
};
int main() {
auto session = std::make_unique<Session>("alpha");
auto value = std::make_unique<int>(42);
auto moved = std::move(session);
std::cout << moved->name << ' ' << *value << ' '
<< (session ? "held" : "moved") << '\n';
auto digits = std::make_unique<int[]>(3);
digits[0] = 1;
digits[1] = 2;
digits[2] = 3;
std::cout << digits[0] + digits[2] << '\n';
std::vector<std::unique_ptr<Session>> sessions;
sessions.push_back(std::make_unique<Session>("beta"));
std::cout << sessions.front()->name << ' ' << sessions.size() << '\n';
}
输出 alpha 42 moved、4、beta 1,析构时再输出 close beta 与 close alpha。示例覆盖所有权移动、数组重载和容器持有;session 被移走后为空,只有 moved 负责释放原对象。完整版本见 examples/cpp14/make_unique.cpp。
数组也有对应重载:std::make_unique<int[]>(n) 创建 n 个元素并值初始化,返回 std::unique_ptr<int[]>。
进阶用法¶
工厂函数是最常见的使用场景。返回 std::unique_ptr<Base> 时,用 std::make_unique<Derived>(args...) 构造具体类型,再由 unique_ptr 的转换构造转成基类指针;不要写 std::make_unique<Base>(args...),那会尝试直接构造抽象基类。
把 unique_ptr 存进容器或成员时,make_unique 让“分配 + 转移所有权”成为一个表达式。容器扩容只移动指针,不复制对象;成员初始化列表里也能直接写 member_(std::make_unique<T>(...))。
需要自定义删除器时,make_unique 帮不上忙。删除器是 unique_ptr 类型的一部分,必须显式构造:std::unique_ptr<FILE, decltype(&fclose)> file(fopen(...), &fclose);。需要共享所有权时改用 std::make_shared,它把控制块和对象放在一次分配里。
C++20 增加 std::make_unique_for_overwrite,用于“分配后立即覆盖、不需要值初始化”的场景。普通 make_unique<T[]>(n) 会对每个元素值初始化,处理大块缓冲区时这一步可能不便宜。
真实项目证据¶
| 项目 | 位置 | 用法 | 为什么典型 |
|---|---|---|---|
| mold | src/input-files.cc#L371-L374 |
在数组槽位中用 make_unique 创建派生段对象 |
工厂式构造,所有权立即交给容器 |
| drogon | lib/src/HttpAppFrameworkImpl.cc#L612-L614 |
创建只由框架持有的线程池成员 | 分配与赋值合并为单个表达式 |
| abseil | absl/crc/internal/crc_memcpy_fallback.cc#L68-L71 |
从工厂函数返回多态引擎 | 返回 unique_ptr 表达唯一所有权 |
三条证据均为 manual 级:作者复核,尚未引入第二名审核者。三处都在生产路径上,分别对应容器槽位、长期成员和工厂返回值。
陷阱与边界¶
不支持自定义删除器。 需要 fclose、munmap 等删除动作时,必须显式构造带删除器类型的 unique_ptr。
花括号初始化列表不能直接推导。 std::make_unique<std::vector<int>>({1, 2, 3}) 无法推导 Args,应写成 std::make_unique<std::vector<int>>(std::initializer_list<int>{1, 2, 3})。
数组重载会值初始化。 std::make_unique<int[]>(n) 把每个元素置零;只想要未初始化缓冲区时,C++20 起用 make_unique_for_overwrite。
不要先 new 再交给 make_unique。 std::make_unique<T>(*new T) 会先分配再复制,既泄漏又失去意义。
多态构造要写具体类型。 make_unique<Base> 只会构造 Base;需要派生行为时写 make_unique<Derived>,再让返回值转换成基类指针。
异常安全的前提是一次表达式。 只要分配和所有权转移在同一个完整表达式里,后续实参抛异常也不会泄漏;把裸指针先存进局部变量再包装,就又回到了手动管理。
库联动¶
std::unique_ptr:make_unique是它的首选工厂,二者共同表达唯一所有权。- 标准容器:
std::vector<std::unique_ptr<T>>存放多态对象,扩容时只移动指针。 - 工厂与插件系统:返回
unique_ptr<Interface>隐藏具体类型,调用方无需知道派生类。 - mold / drogon / abseil:链接器、Web 框架和 CRC 引擎都用它把分配点集中在一处。
横向对比¶
| 方案 | 类型名重复 | 异常安全 | 自定义删除器 | 主要限制 |
|---|---|---|---|---|
std::unique_ptr<T>(new T) |
两次 | 需小心 | 支持 | 实参求值顺序可能泄漏 |
std::make_unique<T> |
一次 | 是 | 不支持 | 花括号列表需显式类型 |
std::make_shared<T> |
一次 | 是 | 不支持 | 共享所有权,控制块与对象同分配 |
裸 new / delete |
两次 | 否 | 手动 | 容易忘记释放或重复释放 |
跨标准演进:C++11 提供 unique_ptr 与 make_shared;C++14 通过 N3656 补上 make_unique;C++20 增加 make_unique_for_overwrite,避免不必要的值初始化。