1 make 函数
- 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);
- 使用 new 的版本重复了正在创建的类型, make 函数不会。
- 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 将不会被调用
- 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);
- 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);
- 对于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 要点速记
- 与使用 new 相比,make 函数消除了源代码重复,提高了异常安全性,并且对于 std::make_shared 和 std::allocate_shared,生成的代码更小、更快。
- 不适合使用 make 函数的情况包括,需要指定自定义删除器和希望传递花括号初始化器。
- 对于 std::shared_ptr,不建议使用 make 函数的其他情况包括
(1)具有自定义内存管理的类。
(2)注重内存效率的系统,具有非常大的对象以及比相应的 std::shared_ptr 存活时间更长的 std::weak_ptr 。