为什么现代C++推荐使用std::make_unique来创建unique_ptr(推荐使用.创建.make_unique.std.unique_ptr...)

wufei123 发布于 2025-09-11 阅读(6)
推荐使用std::make_unique创建unique_ptr,因其将对象构造与智能指针创建封装为原子操作,避免因函数参数求值顺序不确定导致的异常安全问题,同时提升代码简洁性与可读性。

为什么现代c++推荐使用std::make_unique来创建unique_ptr

现代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
之前,所有的参数都必须被求值完毕。这意味着,编译器可能会以以下某种顺序执行操作:
  1. 调用
    new MyClass()
    分配内存并构造对象。
  2. 调用
    another_function_that_might_throw()
  3. 调用
    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...)
时,它会:
  1. 在内部调用
    new MyClass(args...)
    来分配内存并构造对象。
  2. 立即将这个新创建的原始指针传递给
    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
在日常编码中还带来了不少其他实际的好处,让代码更具可读性和维护性。 PIA PIA

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

PIA226 查看详情 PIA

首先,代码的简洁性和可读性得到了显著提升。对比一下两种创建方式:

// 旧方式
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++循环与算法优化提高程序执行效率

标签:  推荐使用 创建 make_unique 

发表评论:

◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。