条款21:优先选用std::make_unique、std::make_shared,而非直接new

1 make 函数

  1. std::make_shared 是 C++11 的一部分,但遗憾的是,std::make_unique 不是。它是在 C++14 中加入标准库的。如果使用的是 C++11,std::make_unique 也不难编写:
cpp 复制代码
template<typename T, typename... Ts>
std::unique_ptr<T> make_unique(Ts&&... params){
    return std::unique_ptr<T>(new T(std::forward<Ts>(params)...));
}
//make_unique 只是将其参数完美转发给要创建的对象的构造函数,从 new 产生的原始指针构造一个 std::unique_ptr,并返回创建的 std::unique_ptr。这种形式的函数不支持数组或自定义删除器

1)std::make_unique 和 std::make_shared 是三个 make 函数中的两个:这些函数接受任意一组参数,将它们完美转发给动态分配对象的构造函数,并返回指向该对象的智能指针。

2)第三个 make 函数是 std::allocate_shared。它的行为就像 std::make_shared 一样,只是它的第一个参数是用于动态内存分配的分配器对象。

cpp 复制代码
auto upw1(std::make_unique<Widget>());
std::unique_ptr<Widget> upw2(new Widget);
cpp 复制代码
auto spw1(std::make_shared<Widget>());
std::shared_ptr<Widget> spw2(new Widget);
  1. 使用 new 的版本重复了正在创建的类型, make 函数不会。
  2. make函数的异常安全性更好。
cpp 复制代码
void processWidget(std::shared_ptr<Widget> spw, int priority);
int computePriority();
processWidget(std::shared_ptr<Widget>(new Widget), computePriority()); // 可能资源泄漏

在运行时,函数的参数必须在调用函数之前进行求值:

1)计算 new Widget 表达式,即必须在堆上创建一个 Widget。

2)执行负责管理 new产生的指针的 std::shared_ptr的构造函数。

3)运行computePriority。

编译器不需要生成按照这个顺序执行的代码。new Widget必须在 std::shared_ptr构造函数被调用之前执行即可,编译器可能会生成按照以下顺序执行操作的代码:

1、执行 new Widget;

2、执行 computePriority;

3、运行 std::shared_ptr 构造函数。如果运行时,computePriority 产生异常,那么步骤 1 中动态分配的 Widget 将泄漏。

使用 std::make_shared 可以避免这个问题:

cpp 复制代码
processWidget(std::make_shared<Widget>(), computePriority()); // 不会发生内存泄露
//如果std::make_shared首先调用 ,那么原始指针将在调用 computePriority 之前安全地存储在返回的 std::shared_ptr 中;如果首先调用 computePriority 并产生异常,std::make_shared 将不会被调用
  1. std::make_shared与直接使用 new 相比还提高了效率。
    使用 std::make_shared允许编译器生成更小、更快的代码,并使用更精简的数据结构。
cpp 复制代码
std::shared_ptr<Widget> spw(new Widget);
//这段代码执行了两次内存分配。直接使用 new,需要一次为 Widget 分配内存,第二次为控制块分配内存

如果使用 std::make_shared代替则一次分配就足够了:

cpp 复制代码
auto spw = std::make_shared<Widget>();
//std::make_shared分配了一个单独的内存块来容纳 Widget 对象和控制块,这种优化减少了程序的静态大小,因为代码中只包含一个内存分配调用,并且它提高了可执行代码的速度,因为只分配了一次内存。此外,使用 std::make_shared 不需要控制块中的一些簿记信息,可能会减少程序的总内存占用。 以上分析同样适用于 std::allocate_shared

但没有任何make函数允许指定自定义删除器,而是 std::unique_ptr 和 std::shared_ptr 都有构造函数可以这样做:

cpp 复制代码
auto widgetDeleter = [](Widget* pw) { ... };
std::unique_ptr<Widget, decltype(widgetDeleter)> upw(new Widget, widgetDeleter);
std::shared_ptr<Widget> spw(new Widget, widgetDeleter);
  1. make 函数的第二个限制源于其实现的语法细节,make 函数将其参数完美转发到对象的构造函数,但它们是使用圆括号还是大括号这样做的呢?
cpp 复制代码
auto upv = std::make_unique<std::vector<int>>(10, 20);
auto spv = std::make_shared<std::vector<int>>(10, 20);
//类型重载了带有和不带有 std::initializer_list 参数的构造函数的对象时,使用大括号创建对象会优先使用 std::initializer_list 构造函数

得到的智能指针是指向 具有10 个20 的 std::vector;还是指向具有两个元素 10 和 20 的 std::vector?

这两个调用都创建了大小为 10 且所有值都设置为 20 的 std::vector。这意味着在 make 函数内部,完美转发代码使用的是圆括号,而不是大括号。

如果想使用大括号初始化器来构造指向的对象,则必须直接使用 new。

可以使用auto从大括号初始化器创建一个 std::initializer_list 对象,然后将自动创建的对象通过 make 函数传递:

cpp 复制代码
//大括号初始化器不能完美转发
auto initList = { 10, 20 }; // 创建 std::initializer_list
// 使用 std::initializer_list 参数的 ctor 创建 std::vector  
auto spv = std::make_shared<std::vector<int>>(initList);
  1. 对于std::unique_ptr来说,自定义删除器和带大括号的初始化器是它的make函数的所有问题。但对于std::shared_ptr的make函数,还有两个特殊情况需要考虑:
    1、当类有自己的 operator new 和 operator delete 实现时,与 std::shared_ptr 的自定义分配和释放支持可能不兼容,因此使用 make 函数创建这些类的对象可能不合适。这可能会导致内存管理方面的问题。
    2、std::shared_ptr 的控制块与管理对象放在同一块内存中。当该对象的引用计数为零时,该对象将被销毁(即,调用其析构函数)。但是,在控制块也被销毁之前,它所占用的内存不能被释放。
    由 std::shared_ptr make 函数分配的内存只有在最后一个 std::shared_ptr 和最后一个引用它的 std::weak_ptr 被销毁后才能被释放。对象销毁和它占用的内存被释放之间可能会出现滞后。
cpp 复制代码
class ReallyBigType{...};
auto pBigObj = std::make_shared<ReallyBigType>(); // 通过std::make_shared创建非常大的对象
... // 创建 std::shared_ptrs 和 std::weak_ptrs 到大对象,使用它们与之合作
... // 此处销毁对象的最后一个 std::shared_ptr,但对它的 std::weak_ptr 仍然存在
... // 在这段时间内,以前被大对象占用的内存仍然被分配
... // 此处销毁对象的最后一个 std::weak_ptr;
// 控制块和对象的内存被释放

通过直接使用 new,ReallyBigType 对象的内存可以在最后一个对它的 std::shared_ptr 被销毁后立即释放:

cpp 复制代码
class ReallyBigType{...}; // 与之前一样
std::shared_ptr<ReallyBigType> pBigObj(new ReallyBigType);
// 通过 new 创建非常大的对象
... // 与之前一样,创建对象的 std::shared_ptrs 和 std::weak_ptrs,与之一起使用
... // 此处销毁对象的最后一个 std::shared_ptr,但对它的 std::weak_ptrs 仍然存在;
// 对象的内存已被释放
... // 在这段时间内,只有控制块的内存仍然有效
... // 此处销毁对象的最后一个 std::weak_ptr;
// 控制块的内存被释放

如果不适合使用 std::make_shared ,又希望免受异常安全问题的影响。最好的方法是确保当直接使用 new 时,立即将结果传递给智能指针构造函数:

cpp 复制代码
void processWidget(std::shared_ptr<Widget> spw, int priority);// 像以前一样
void cusDel(Widget *ptr); // 自定义删除器
// 潜在的资源泄漏
processWidget( std::shared_ptr<Widget>(new Widget, cusDel), computePriority() );

std::shared_ptr<Widget> spw(new Widget, cusDel);
processWidget(spw, computePriority()); // 正确,但不是最佳的,参数变为了左值
processWidget(std::move(spw), computePriority());// 既高效又异常安全

2 要点速记

  1. 与使用 new 相比,make 函数消除了源代码重复,提高了异常安全性,并且对于 std::make_shared 和 std::allocate_shared,生成的代码更小、更快。
  2. 不适合使用 make 函数的情况包括,需要指定自定义删除器和希望传递花括号初始化器。
  3. 对于 std::shared_ptr,不建议使用 make 函数的其他情况包括
    (1)具有自定义内存管理的类。
    (2)注重内存效率的系统,具有非常大的对象以及比相应的 std::shared_ptr 存活时间更长的 std::weak_ptr 。
相关推荐
scx201310043 小时前
20251019状压DP总结
c++
消失的旧时光-19433 小时前
Kotlin 高阶函数在回调设计中的最佳实践
android·开发语言·kotlin
LucianaiB4 小时前
掌握 Rust:从内存安全到高性能服务的完整技术图谱
开发语言·安全·rust
m0_748240254 小时前
C++ 游戏开发示例:简单的贪吃蛇游戏
开发语言·c++·游戏
Summer_Uncle4 小时前
【C++学习】指针
c++·学习
兰亭妙微5 小时前
2026年UX/UI五大趋势:AI、AR与包容性设计将重新定义用户体验
开发语言·ui·1024程序员节·界面设计·设计趋势
蜗牛沐雨5 小时前
详解C++中的字符串流
c++·1024程序员节
蜗牛沐雨5 小时前
详解C++中的流
c++·1024程序员节
懒羊羊不懒@5 小时前
Java—枚举类
java·开发语言·1024程序员节