20、C++14 新特性:std::make_unique,更安全的创建 unique_ptr
说实话,C++11 刚出 unique_ptr 的时候,我挺高兴的。终于有个靠谱的智能指针了,不用整天惦记着 delete。但用着用着就发现一个问题——创建 unique_ptr 的姿势,怎么这么别扭?
你想想看,C++11 里我们只能这么写:
std::unique_ptr<Widget> p(new Widget(42));
嗯,这代码能跑。但我在项目里见过太多次因为这种写法翻车的情况了。C++14 终于补上了这个短板——std::make_unique。说白了,就是给 unique_ptr 配了个亲儿子级别的工厂函数。
为什么说 make_unique 更安全?
我直接给你看一个我在代码审查里抓到过的真实 bug:
void ProcessWidget(std::unique_ptr<Widget> p1, std::unique_ptr<Widget> p2);
// 危险写法!
ProcessWidget(
std::unique_ptr<Widget>(new Widget(1)),
std::unique_ptr<Widget>(new Widget(2))
);
这段代码有什么问题?C++ 对函数参数的求值顺序是未指定的。编译器可能先执行 new Widget(1),然后 new Widget(2),最后才构造两个 unique_ptr。如果第二个 new 抛了异常,第一个 new 分配的内存就泄漏了——因为 unique_ptr 还没构造出来呢!
核心问题:裸 new 和 unique_ptr 构造之间,存在异常安全缺口。
用 make_unique 就完全没这个烦恼:
ProcessWidget(
std::make_unique<Widget>(1),
std::make_unique<Widget>(2)
);
每个 make_unique 调用是原子的——要么成功创建完整的 unique_ptr,要么什么都不留下。异常安全,稳如老狗。
make_unique 的底层逻辑
它的实现其实不复杂,我带你看看核心思路:
template<typename T, typename... Args>
std::unique_ptr<T> make_unique(Args&&... args) {
return std::unique_ptr<T>(new T(std::forward<Args>(args)...));
}
完美转发参数,内部用 new 构造对象,然后包装成 unique_ptr 返回。就这么简单。但就是这层包装,堵上了异常安全的漏洞。
个人习惯:我现在写 C++14 及以上的代码,创建 unique_ptr 只用 make_unique。除非遇到下面说的特殊情况,否则绝不碰裸 new。
什么时候不能用 make_unique?
嗯,这里要注意。虽然 make_unique 是首选,但有几个场景它确实搞不定:
- 自定义删除器:比如你要用
fclose来释放文件句柄,make_unique不支持传入删除器。 - 需要聚合初始化:比如
struct Point { int x; int y; };,你想用make_unique<Point>({1, 2})?不行,花括号初始化列表没法完美转发。 - 私有构造函数:如果构造函数是私有的,但析构函数是公开的,
make_unique可能无法访问构造函数(取决于友元声明)。
这些时候,你还是得用 std::unique_ptr<T>(new T(...))。但记住,这是例外,不是常态。
性能对比:make_unique vs new
我做过实测,在主流编译器上,make_unique 和裸 new 加 unique_ptr 构造的性能基本一致。因为编译器会做优化,把临时对象消除掉。所以不用担心性能损失。
| 创建方式 | 异常安全 | 代码简洁度 | 性能 |
|---|---|---|---|
| make_unique | ✅ 安全 | ✅ 一行搞定 | ✅ 无额外开销 |
| new + unique_ptr | ⚠️ 有风险 | ❌ 啰嗦 | ✅ 无额外开销 |
和 make_shared 的对比
用过 make_shared 的朋友可能会问:make_unique 也能像 make_shared 那样减少内存分配吗?
答案是不能。make_shared 可以把控制块和对象分配在同一个内存块上,减少一次分配。但 unique_ptr 没有控制块,所以 make_unique 没有这个优化空间。它纯粹是为了安全和便利。
我曾经踩过的坑:有次在性能敏感代码里,我自作聪明地用 make_unique 替换了所有 new,结果发现内存分配次数没变。后来才想起来——unique_ptr 本来就没有控制块,哪来的合并分配?嗯,白忙活一场。
代码规范建议
我在团队里推行了这么几条规则,你可以参考:
- 默认使用
std::make_unique,不要用new。 - 只在需要自定义删除器或聚合初始化时,才退回到
unique_ptr<T>(new T(...))。 - 永远不要写
unique_ptr<T>(new T[10]),用make_unique<T[]>(10)更安全。 - 配合
auto使用:auto p = std::make_unique<Widget>(...);,类型推导,省心省力。
SVG 流程图:make_unique 的核心逻辑
总结
std::make_unique 是 C++14 给智能指针生态补上的最后一块拼图。它不复杂,但解决了实实在在的问题。我个人建议,从现在开始,把代码里所有的 unique_ptr<T>(new T(...)) 都换成 make_unique<T>(...)。你的代码会更安全,审查的人也会更省心。
嗯,就这么简单。一个函数,解决一个痛点,这就是好的设计。