现代C++中推荐使用
std::make_unique来创建
unique_ptr,这主要是因为它能有效提升代码的异常安全性,同时让代码更简洁、易读。直接使用
new来构造
unique_ptr,在某些复杂的表达式中,可能会引入难以察觉的资源泄露风险。 解决方案
当我们谈论
unique_ptr的创建,很多人可能习惯性地写成
std::unique_ptr<MyClass> ptr(new MyClass());。这在大多数简单场景下看起来没问题,但实际上,这种写法在某些特定情况下存在一个微妙但重要的缺陷,那就是潜在的异常安全性问题。
考虑一个函数调用,其中包含多个参数,例如:
some_function(std::unique_ptr<MyClass>(new MyClass()), another_function_that_might_throw());
C++标准对函数参数的求值顺序并没有严格规定,只知道在调用
some_function之前,所有的参数都必须被求值完毕。这意味着,编译器可能会以以下某种顺序执行操作:
- 调用
new MyClass()
分配内存并构造对象。 - 调用
another_function_that_might_throw()
。 - 调用
std::unique_ptr<MyClass>(...)
构造智能指针,接管MyClass
对象的管理。
如果执行顺序是1 -> 2 -> 3,并且
another_function_that_might_throw()在步骤2中抛出了异常,那么步骤3(
unique_ptr的构造)将永远不会发生。此时,步骤1中通过
new MyClass()分配的内存和构造的对象将无人管理,从而导致内存泄露。因为
new操作已经完成,但
unique_ptr还没来得及接管。
而
std::make_unique的设计,正是为了解决这个问题。它将对象的分配和
unique_ptr的构造封装成一个原子操作。当你写
std::make_unique<MyClass>()时,
MyClass对象的创建和
unique_ptr对它的接管,要么一起成功,要么一起失败,中间不会留下悬空的原始指针。这保证了在存在异常的情况下,资源能够得到妥善管理,避免了上述的泄露风险。它强制了“分配和拥有”这两个动作的紧密耦合。 std::make_unique是如何提供异常安全性的?
std::make_unique提供异常安全性的核心机制在于它将内存分配(
new)和
unique_ptr的构造视为一个单一的、不可分割的操作。我们之前提到的潜在泄露场景,其根本原因在于C++标准允许编译器在求值函数参数时,对子表达式的求值顺序有一定自由度。
例如,对于
func(A(), B(), C())这样的调用,A、B、C的求值顺序是不确定的。如果A是
new MyObject(),B是
another_risky_call(),C是
std::unique_ptr<MyObject>(...),那么在
new MyObject()执行完毕后,
another_risky_call()可能在
std::unique_ptr构造前抛出异常,导致
MyObject泄露。
std::make_unique通过在内部完成
new操作并直接返回一个已构造好的
unique_ptr实例,绕开了这个“参数求值顺序不确定”的陷阱。当调用
std::make_unique<MyClass>(args...)时,它会:
- 在内部调用
new MyClass(args...)
来分配内存并构造对象。 - 立即将这个新创建的原始指针传递给
unique_ptr
的构造函数。 整个过程被封装在一个函数调用中,使得从外部看来,make_unique
要么返回一个有效的unique_ptr
,要么在内部抛出异常(例如,如果MyClass
的构造函数抛出),但绝不会在new
了一个对象后,又因为外部其他操作的异常而导致该对象无人管理。
这使得像下面这样的代码变得安全:
void process_data(std::unique_ptr<Data> p, int value); void potentially_failing_operation(); // 不安全的方式 // process_data(std::unique_ptr<Data>(new Data()), potentially_failing_operation()); // 如果 Data 的 new 完成了,但 potentially_failing_operation() 抛出,Data 会泄露。 // 安全的方式 process_data(std::make_unique<Data>(), potentially_failing_operation()); // make_unique 确保 Data 对象要么被 unique_ptr 拥有,要么在创建过程中失败,不会出现中间状态的泄露。
这种设计极大地简化了对异常安全性的考量,让开发者能更专注于业务逻辑,而不是底层内存管理的复杂性。
除了异常安全,使用std::make_unique还有哪些实际好处?除了异常安全性这个核心优势,
std::make_unique在日常编码中还带来了不少其他实际的好处,让代码更具可读性和维护性。

全面的AI聚合平台,一站式访问所有顶级AI模型


首先,代码的简洁性和可读性得到了显著提升。对比一下两种创建方式:
// 旧方式 std::unique_ptr<MyComplexType> ptr1(new MyComplexType(arg1, arg2, arg3)); // 推荐方式 std::unique_ptr<MyComplexType> ptr2 = std::make_unique<MyComplexType>(arg1, arg2, arg3);
很明显,第二种方式更加简洁。它避免了重复写入类型名称
MyComplexType,减少了冗余,也使得代码的意图——“创建一个
MyComplexType的唯一所有权智能指针”——更加清晰。这种“不重复自己”(DRY原则)的体现,在类型名称较长或模板类型时尤其明显。
其次,它与
std::make_shared保持了风格上的一致性。在现代C++中,
std::make_shared是创建
std::shared_ptr的推荐方式,其原因也包括异常安全性和潜在的性能优化(通过一次内存分配同时为对象和控制块分配内存)。
std::make_unique的引入,使得智能指针的创建方式趋于统一,降低了学习曲线,也让代码库看起来更加协调和专业。
虽然对于
unique_ptr而言,
make_unique在性能上的优势通常不如
make_shared那么显著(
unique_ptr没有独立的控制块),但编译器和库实现者仍然有可能在某些情况下,通过优化
make_unique的内部实现,实现微小的性能提升,例如减少内存分配器的调用次数。但这通常不是选择
make_unique的主要驱动因素,其主要价值仍在异常安全性和代码清晰度上。 在哪些情况下,我可能仍然需要直接使用new来创建unique_ptr?
尽管
std::make_unique是创建
unique_ptr的首选方式,但在某些特定场景下,我们仍然需要或更适合直接使用
new操作符来构造
unique_ptr。这些情况通常涉及更高级的内存管理需求或与C风格API的交互。
最常见的情况是当需要使用自定义deleter时。
std::make_unique不提供直接指定自定义deleter的接口。
unique_ptr的构造函数有一个重载版本,允许你传入一个原始指针和一个deleter对象或函数指针。 例如,如果你想管理一个C语言的
FILE*,并确保它被
fclose正确关闭:
#include <cstdio> #include <memory> // 自定义 deleter 函数 void file_closer(FILE* f) { if (f) { fclose(f); } } int main() { // 无法使用 make_unique // std::unique_ptr<FILE, decltype(&file_closer)> log_file = std::make_unique<FILE>(fopen("log.txt", "w"), &file_closer); // 错误 // 必须直接使用 new (或者这里是 fopen 返回的指针) std::unique_ptr<FILE, decltype(&file_closer)> log_file(fopen("log.txt", "w"), &file_closer); if (log_file) { fprintf(log_file.get(), "Hello from unique_ptr!\n"); } // log_file 在离开作用域时会自动关闭文件 return 0; }
这里,
fopen返回的是一个原始
FILE*指针,我们需要
unique_ptr来接管它,并指定
file_closer作为其析构时的操作。
另一个场景是当需要从一个已存在的原始指针接管所有权时。这可能发生在与遗留C++代码库交互,或者当一个工厂函数返回一个
new出来的原始指针时。
// 假设这是一个C风格的API,返回一个 new 出来的对象 MyClass* create_my_class_raw() { return new MyClass(); } int main() { MyClass* raw_ptr = create_my_class_raw(); // 此时不能用 make_unique,因为它会再次 new 一个对象 std::unique_ptr<MyClass> managed_ptr(raw_ptr); // managed_ptr 现在拥有了 raw_ptr 指向的对象 return 0; }
在这种情况下,我们不是要“创建”一个新的对象,而是要“接管”一个已经存在的对象的所有权,所以直接将原始指针传递给
unique_ptr的构造函数是唯一的选择。
最后,当处理
unique_ptr管理数组且需要自定义deleter时。
std::make_unique有一个重载版本用于创建数组(
std::make_unique<T[]>(size)),它会使用
delete[]来释放内存。但如果你需要一个特殊的数组deleter,比如一个内存池的释放函数,你就需要直接
new T[size]并结合自定义deleter来构造
unique_ptr<T[], MyCustomArrayDeleter>。
这些情况虽然相对小众,但确实存在,提醒我们
std::make_unique并非万能,理解其局限性与优势同样重要。选择哪种方式,最终还是取决于具体的编程需求和上下文。
以上就是为什么现代C++推荐使用std::make_unique来创建unique_ptr的详细内容,更多请关注知识资源分享宝库其它相关文章!
相关标签: c语言 ai c++ 作用域 new操作符 为什么 red c语言 封装 构造函数 fopen fclose 指针 接口 delete 对象 性能优化 大家都在看: C++如何使用模板实现迭代器类 C++如何处理复合对象中的嵌套元素 C++内存模型与编译器优化理解 C++如何使用ofstream和ifstream组合操作文件 C++循环与算法优化提高程序执行效率
发表评论:
◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。