C++14 之 std::make_unique
C++14 之 std::make_unique
C++11 引入了智能指针三件套,却唯独漏掉了一个关键角色——
make_unique。结果就是你得在代码里到处写new,一不小心就埋下异常安全的定时炸弹。C++14 把这个缺口补上了:一个函数调用,搞定内存分配、对象构造和智能指针绑定,干净利落。
一、C++11 智能指针的遗留问题
1.1 有 make_shared,却没 make_unique
C++11 提供了 std::make_shared,让你一行代码创建 shared_ptr:
auto sp = std::make_shared<Widget>(args...); // ✅ 安全、高效、优雅
但 unique_ptr 呢?没有。你只能手动 new:
auto up = std::unique_ptr<Widget>(new Widget(args...)); // 😐 能用,但不够优雅
看似小事,实则暗藏隐患。
1.2 隐式 new 的异常安全问题
看这个看似无害的函数调用:
void processWidget(std::unique_ptr<Widget> pw, int priority);
当你这样调用时:
processWidget(std::unique_ptr<Widget>(new Widget), computePriority());
C++ 标准不保证参数的求值顺序。编译器可能按以下顺序执行:
① new Widget // 堆分配 + 构造对象
② computePriority() // 计算优先级
③ unique_ptr 构造 // 接管裸指针
如果步骤②抛出异常(比如内存不足),步骤①分配的内存已经泄漏了——因为 unique_ptr 还没接管这个裸指针。
这不是理论上的极端情况。在大型项目中,computePriority() 可能涉及 I/O、网络请求或复杂的计算,抛异常的概率并不低。
1.3 代码冗余
手动 new 还带来了不必要的冗余:
// 你明明知道类型是 Widget,却得写两遍
std::unique_ptr<Widget> pw(new Widget(42));
// Widget ↑ ↑ Widget 又写了一遍
重复即错误之源。类型名出现两次,意味着改类型时要改两处,漏改就会编译失败。
std::make_unique 一次性解决了所有这些问题。
二、make_unique 详解
2.1 核心语法
auto ptr = std::make_unique<T>(args...);
就这么简单。一行搞定类型声明、内存分配、对象构造和智能指针绑定。
等价于:
std::unique_ptr<T> ptr(new T(args...));
但更安全、更简洁。
2.2 为什么更安全
make_unique 内部会先完成内存分配和对象构造,然后直接将结果包装为 unique_ptr 返回。整个过程没有裸指针中间态,不存在"裸指针悬空"的风险:
make_unique 的执行流程:
make_unique<Widget>(42)
│
▼
┌──────────────────┐
│ new Widget(42) │ ← 分配 + 构造
└────────┬─────────┘
│
▼
┌──────────────────┐
│ unique_ptr 包装 │ ← 立即接管
└──────────────────┘
│
▼
返回给调用者
没有裸指针中间态 → 无泄漏风险 ✅
即使 make_unique 内部的构造过程抛出异常,new 分配的内存也会被正确释放(因为异常在 new 返回之前发生)。
2.3 与直接 new 的对比
| 对比项 | make_unique<T>(args...) |
unique_ptr<T>(new T(args...)) |
|---|---|---|
| 异常安全 | ✅ 安全 | ❌ 可能泄漏 |
| 类型冗余 | ✅ 只写一次 | ❌ 类型写两遍 |
| 代码简洁度 | ⭐⭐⭐ | ⭐⭐ |
| 可读性 | 高 | 中 |
| 裸指针暴露 | ❌ 无 | ⚠️ 有中间态 |
三、使用场景
3.1 创建独占所有权的对象
最基本的用法——需要一个独占管理资源的对象时:
#include <iostream>
#include <memory>
#include <string>
class Connection {
public:
Connection(const std::string& url) : url_(url) {
std::cout << "连接到 " << url_ << std::endl;
}
~Connection() {
std::cout << "断开 " << url_ << std::endl;
}
void execute(const std::string& cmd) {
std::cout << "[" << url_ << "] 执行: " << cmd << std::endl;
}
private:
std::string url_;
};
int main() {
// 一行创建,自动管理生命周期
auto conn = std::make_unique<Connection>("db://localhost:5432");
conn->execute("SELECT * FROM users");
// 离开作用域时自动断开连接
return 0;
}
输出:
连接到 db://localhost:5432
[db://localhost:5432] 执行: SELECT * FROM users
断开 db://localhost:5432
无需手动 delete,无需担心异常路径上的资源泄漏。
3.2 工厂模式
工厂函数返回 unique_ptr 是最常见的场景。make_unique 让工厂函数变得极其简洁:
#include <iostream>
#include <memory>
#include <string>
#include <vector>
// 基类
class Shape {
public:
virtual ~Shape() = default;
virtual void draw() const = 0;
virtual double area() const = 0;
};
// 派生类
class Circle : public Shape {
public:
explicit Circle(double r) : radius_(r) {}
void draw() const override {
std::cout << "画圆(r=" << radius_ << ")" << std::endl;
}
double area() const override { return 3.14159265 * radius_ * radius_; }
private:
double radius_;
};
class Rectangle : public Shape {
public:
Rectangle(double w, double h) : width_(w), height_(h) {}
void draw() const override {
std::cout << "画矩形(" << width_ << "x" << height_ << ")" << std::endl;
}
double area() const override { return width_ * height_; }
private:
double width_, height_;
};
// 工厂函数
std::unique_ptr<Shape> createShape(const std::string& type, double a, double b = 0) {
if (type == "circle") return std::make_unique<Circle>(a);
if (type == "rectangle") return std::make_unique<Rectangle>(a, b);
return nullptr;
}
int main() {
std::vector<std::unique_ptr<Shape>> shapes;
shapes.push_back(createShape("circle", 5.0));
shapes.push_back(createShape("rectangle", 4.0, 3.0));
shapes.push_back(createShape("circle", 2.5));
double totalArea = 0;
for (const auto& shape : shapes) {
shape->draw();
totalArea += shape->area();
}
std::cout << "总面积: " << totalArea << std::endl;
return 0;
}
工厂函数只关心创建逻辑,完全不需要手动管理内存。调用者拿到 unique_ptr 后,所有权转移清晰明确。
3.3 容器中存储智能指针
当你需要在一个容器里存储多态对象时,make_unique + unique_ptr 是标准做法:
#include <iostream>
#include <memory>
#include <vector>
#include <string>
class Task {
public:
Task(std::string name, int priority)
: name_(std::move(name)), priority_(priority) {}
virtual ~Task() = default;
virtual void execute() const {
std::cout << "[" << priority_ << "] " << name_ << std::endl;
}
int priority() const { return priority_; }
private:
std::string name_;
int priority_;
};
class UrgentTask : public Task {
public:
using Task::Task;
void execute() const override {
std::cout << "⚠️ 紧急! ";
Task::execute();
}
};
int main() {
std::vector<std::unique_ptr<Task>> taskQueue;
taskQueue.push_back(std::make_unique<Task>("写周报", 3));
taskQueue.push_back(std::make_unique<UrgentTask>("修复线上Bug", 1));
taskQueue.push_back(std::make_unique<Task>("代码审查", 2));
// 按优先级排序(升序,数字越小越紧急)
std::sort(taskQueue.begin(), taskQueue.end(),
[](const auto& a, const auto& b) {
return a->priority() < b->priority();
});
std::cout << "=== 任务队列 ===" << std::endl;
for (const auto& task : taskQueue) {
task->execute();
}
return 0;
}
注意 std::make_unique<UrgentTask>("修复线上Bug", 1) 直接构造了派生类对象,但返回的是 unique_ptr<Task>——所有权和多态性完美结合。
四、进阶用法
4.1 数组版本:make_unique<T[]>
make_unique 还支持创建动态数组:
#include <iostream>
#include <memory>
int main() {
// 创建一个包含 10 个 int 的数组,值初始化为 0
auto arr = std::make_unique<int[]>(10);
// 使用下标访问
for (int i = 0; i < 10; ++i) {
arr[i] = i * i;
}
for (int i = 0; i < 10; ++i) {
std::cout << "arr[" << i << "] = " << arr[i] << std::endl;
}
return 0;
}
注意数组版本的语法:std::make_unique<int[]>(10),模板参数是 int[] 而非 int。
⚠️
make_unique<T[]>会值初始化数组元素(int初始化为 0),而new T[10]不会(元素值未定义)。这是一个重要的区别。
4.2 make_unique 与 make_shared 的区别
make_unique 和 make_shared 虽然都是工厂函数,但有一个关键区别:
| 特性 | make_unique<T> |
make_shared<T> |
|---|---|---|
| 控制块分配 | 无控制块 | 对象 + 控制块一次分配 |
| 引用计数 | 无 | 有 |
| 自定义删除器 | ❌ 不支持 | ❌ 不支持 |
| 内存分配次数 | 1 次 | 1 次(合并优化) |
make_shared 的一大优势是内存合并——对象和引用计数控制块在同一次 new 中分配,减少了一次内存分配和一次释放,同时对 CPU 缓存更友好。
make_unique 因为 unique_ptr 本身没有控制块,所以没有这个优化需求。它的优势纯粹在于安全性和简洁性。
五、注意事项
5.1 不能用于自定义删除器
make_unique 不支持自定义删除器。如果你需要自定义释放逻辑,只能手动构造:
// ❌ make_unique 不支持自定义删除器
// auto fd = std::make_unique<FILE, decltype(&fclose)>(fopen("test.txt", "r"), &fclose);
// ✅ 只能手动构造
auto fileDeleter = [](FILE* fp) { if (fp) fclose(fp); };
std::unique_ptr<FILE, decltype(fileDeleter)> fp(fopen("test.txt", "r"), fileDeleter);
这是 make_unique 唯一的"硬伤"。不过在实际项目中,需要自定义删除器的场景相对较少(资源句柄通常有现成的 RAII 封装),所以影响不大。
5.2 不能用于 std::make_shared 的场景
如果你需要 weak_ptr 共享同一个对象,或者想利用内存合并优化,应该用 make_shared 而非 make_unique:
// 需要 weak_ptr?用 make_shared
auto shared = std::make_shared<Widget>();
std::weak_ptr<Widget> weak = shared;
// 只需要独占?用 make_unique
auto unique = std::make_unique<Widget>();
// std::weak_ptr<Widget> weak = unique; // ❌ unique_ptr 没有 weak_ptr
5.3 何时应该使用 unique_ptr 而非 shared_ptr
一个简单的判断标准:
- 所有权明确、单一:用
unique_ptr(轻量、零开销) - 所有权需要共享:用
shared_ptr(有控制块开销)
unique_ptr 的独特优势是它可以零开销地转换为 shared_ptr:
std::unique_ptr<Widget> uw = std::make_unique<Widget>();
// 需要共享时,无缝升级
std::shared_ptr<Widget> sw = std::move(uw);
// uw 现在为空,sw 接管了所有权
但反向转换——从 shared_ptr 降级为 unique_ptr——是不可能的。所以在设计 API 时,优先返回 unique_ptr,让调用者自己决定是否需要共享。
六、性能对比
make_unique 本身没有任何运行时开销。编译器会对它进行内联优化,最终生成的机器码与手动 new 完全相同。
验证一下:
#include <iostream>
#include <memory>
#include <chrono>
struct HeavyObject {
int data[1024];
HeavyObject() { data[0] = 42; }
};
int main() {
constexpr int N = 1000000;
// 测试 make_unique
auto start1 = std::chrono::high_resolution_clock::now();
for (int i = 0; i < N; ++i) {
auto p = std::make_unique<HeavyObject>();
(void)p;
}
auto end1 = std::chrono::high_resolution_clock::now();
// 测试手动 new
auto start2 = std::chrono::high_resolution_clock::now();
for (int i = 0; i < N; ++i) {
std::unique_ptr<HeavyObject> p(new HeavyObject());
(void)p;
}
auto end2 = std::chrono::high_resolution_clock::now();
auto ms1 = std::chrono::duration_cast<std::chrono::milliseconds>(end1 - start1).count();
auto ms2 = std::chrono::duration_cast<std::chrono::milliseconds>(end2 - start2).count();
std::cout << "make_unique: " << ms1 << " ms" << std::endl;
std::cout << "手动 new: " << ms2 << " ms" << std::endl;
return 0;
}
在我的测试环境中(GCC 13, -O2),两者耗时基本一致,差异在测量误差范围内。
结论:make_unique 是零开销抽象——安全性和简洁性不要你付出任何性能代价。
七、编译器支持
std::make_unique 是 C++14 标准库的一部分,需要 C++14 或更高版本。
| 编译器 | 最低版本 |
|---|---|
| GCC | 4.9+(2014 年 4 月) |
| Clang | 3.5+(2014 年 9 月) |
| MSVC | VS 2017 15.5+(2017 年 12 月) |
编译时需要启用 C++14 标志:
g++ -std=c++14 main.cpp -o main
clang++ -std=c++14 main.cpp -o main
cl /std:c++14 main.cpp
MSVC 在 C++11 模式下也可能提供
make_unique(作为早期实现),但 GCC 和 Clang 的标准库严格要求 C++14 模式。为可移植性考虑,建议显式使用-std=c++14。
总结
std::make_unique 虽然只是 C++14 标准库中"一个小函数",但它解决的问题却很实在:
- ✅ 异常安全:消除了手动
new带来的裸指针泄漏风险 - ✅ 代码简洁:类型只写一次,无需重复声明
- ✅ 零运行时开销:编译器内联后与手动
new生成的机器码完全相同 - ✅ 工厂模式的最佳搭档:返回
unique_ptr是 C++ 中管理多态对象的标准做法
使用建议很简单:在 C++14 及以上,永远用 std::make_unique<T>(args...) 替代 std::unique_ptr<T>(new T(args...))。唯一的例外是需要自定义删除器的场景。
这个小函数体现了 C++ 标准委员会的一个理念:好的工具应该让安全的写法比不安全的写法更简单。当你发现"正确的做法"反而更省事时,你自然就不会犯错了。
📌 下一篇预告: C++20 的重磅特性之一——Modules(模块)。告别
#include头文件的编译时间噩梦,用模块化的方式组织你的 C++ 代码。敬请期待!
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐



所有评论(0)