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 还没构造出来呢!

核心问题:newunique_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 和裸 newunique_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 本来就没有控制块,哪来的合并分配?嗯,白忙活一场。

代码规范建议

我在团队里推行了这么几条规则,你可以参考:

  1. 默认使用 std::make_unique,不要用 new
  2. 只在需要自定义删除器或聚合初始化时,才退回到 unique_ptr<T>(new T(...))
  3. 永远不要写 unique_ptr<T>(new T[10]),用 make_unique<T[]>(10) 更安全。
  4. 配合 auto 使用auto p = std::make_unique<Widget>(...);,类型推导,省心省力。

SVG 流程图:make_unique 的核心逻辑

std::make_unique 核心逻辑 调用 make_unique<T>(args...) 完美转发参数 std::forward<Args>(args)... 内部调用 new T(args...) 分配内存 + 构造对象(原子操作) ✅ 成功:返回 unique_ptr<T> 异常安全,无泄漏风险 ❌ 失败:new 抛出异常 无内存泄漏(new 失败不会分配)

总结

std::make_unique 是 C++14 给智能指针生态补上的最后一块拼图。它不复杂,但解决了实实在在的问题。我个人建议,从现在开始,把代码里所有的 unique_ptr<T>(new T(...)) 都换成 make_unique<T>(...)。你的代码会更安全,审查的人也会更省心。

嗯,就这么简单。一个函数,解决一个痛点,这就是好的设计。

专注资料整理