在C++14引入std::make_unique之后,很多老项目仍然习惯用new来创建std::unique_ptr。从实际排查经验看,这两者的差异主要不在性能,而在异常安全、代码维护和边界处理上。下面从几个角度拆解,帮助你判断哪些场景必须用make_unique,哪些场景仍然需要保留手动new。
使用 make_unique 可以避免重复书写类型名称,并消除因参数列表变更导致的手动 new 不同步问题。例如 auto p = make_unique(a, b); 比 unique_ptr(new Widget(a, b)) 更简洁。当 Widget 的构造函数参数增加或修改时,只需改动一处即可保持一致性。
异常安全:make_unique如何避免泄漏
直接使用 new 构造 unique_ptr 时,如果构造函数在 new 之后、unique_ptr 接管之前抛出异常,就会导致内存泄漏。例如:f(unique_ptr(new int(42)), g()),若 g() 先执行且抛出异常,则 new 分配的内存无法释放。而 make_unique 将 new 和构造封装为原子操作,即使后续表达式抛出异常,已分配的内存也会在栈展开时正确释放。
这个特性在复杂表达式中尤其重要。比如在一个函数调用中同时构造多个unique_ptr,或者参数本身会抛出异常。我曾遇到过几次线上内存泄漏,排查后发现都是因为new出来的裸指针在异常路径中没有被智能指针接管。如果统一用make_unique,这类问题根本不会出现。建议将make_unique作为默认选项,只在确实需要自定义删除器或数组时才考虑手动new。
代码简洁与维护:减少重复和同步成本
使用 make_unique 可以避免重复书写类型名称,并消除因参数列表变更导致的手动 new 不同步问题。例如 auto p = make_unique<Widget>(a, b); 比 unique_ptr<Widget>(new Widget(a, b)) 更简洁。当 Widget 的构造函数参数增加或修改时,只需改动一处即可保持一致性。
这种一致性在重构时特别有用。比如Widget的构造函数新增一个参数,如果代码中所有创建位置都用make_unique,那么只需要在模板参数后追加参数即可。而如果有些地方用了new,你得一个个去改构造函数的调用,漏一个就是编译错误或未定义行为。虽然这类问题通常能被编译器发现,但在大型代码库中,减少人为漏改的风险同样重要。
性能考量:没有可测量的损失
make_unique 通常与 make_shared 类似,但不会像 make_shared 一样额外分配控制块的开销。对于单个对象,make_unique 内部调用 new 并直接构造,与手动 new 性能相当。区别仅在于异常安全和代码整洁度,没有可测量的性能损失。
但需要注意:make_unique和make_shared不同,它不会导致控制块与对象分配在同一块内存中。所以对于unique_ptr,性能完全等价。如果你的代码对性能要求极其苛刻,并且已经经过测量确认手动new有微小优势,那么可以维持原状。但绝大多数情况下,这种差异可以忽略。
数组与自定义删除器的限制
std::make_unique 只支持单个对象的默认删除器,不支持数组或自定义删除器。若需要 unique_ptr<T[]>,应使用 unique_ptr<T[]>(new T[n]);对于自定义释放器,仍需手动构造。此时异常安全需手动保证,例如通过 RAII 封装后再赋值。
这里有一个常见误区:很多人以为make_unique可以用于数组,但实际上C++14版本不支持,C++20才引入了std::make_unique_default_init类似功能。所以在C++14下,遇到数组必须写手动new。建议在这个边界处加上注释,并确保new与unique_ptr构造函数在同一语句内,避免因后续异常导致泄漏。另外,对于自定义删除器,比如需要调用fclose或自定义内存管理器,只能手动new后传入删除器。此时可以考虑封装一个工厂函数,将异常安全逻辑集中管理。
常见误用与检查点
实际代码中容易出现的误用包括:
- 在
reset中直接用new:p.reset(new T(*p));这种写法在new失败或拷贝抛出异常时,p可能已被置空,导致原对象丢失。建议改用p = make_unique<T>(*p);。 - 与
make_shared混淆:make_unique不会分配控制块,所以不能直接转换成shared_ptr,如果需要共享所有权,还是得用make_shared或显式构造。 - 忘记包含头文件:
std::make_unique定义在<memory>中,且是C++14才加入。如果项目还处于C++11标准,需要自己实现简易版本或升级编译器。
在审查代码时,可以先扫一遍所有new关键词,看看是否可以替换成make_unique。一个实用的检查点是:如果new出现在unique_ptr的构造函数或reset的参数中,且没有自定义删除器或数组需求,就可以直接替换。替换后编译通过,且原来可能存在的异常路径泄漏问题就自动修复了。
最后提醒一句:make_unique虽然更安全,但并不是银弹。当代码涉及多线程或复杂生命周期时,仍然需要配合std::move和右值引用正确传递所有权。如果你还在使用裸new,不妨从当前模块开始逐步迁移,每次重构后运行测试验证行为没有变化。