std::make_shared
是C++11标准,std::make_unique
是C++14标准。一个基础版本的std::make_unique
很容易自己写出的
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
。这种形式的函数不支持数组和自定义析构,但它给出了一个示范:只需一点努力就能写出你想要的make_unique
函数。
std::make_unique
和std::make_shared
是三个make函数 中的两个:接收任意的多参数集合,完美转发到构造函数去动态分配一个对象,然后返回这个指向这个对象的指针。
第三个make
函数是std::allocate_shared
。它行为和std::make_shared
一样,只不过第一个参数是用来动态分配内存的allocator对象。
使用std::make_unique和std::make_shared函数来创建智能指针
1. 避免重复代码
使用make函数可以避免在代码中重复写类型名称,这符合DRY(Don't Repeat Yourself)原则。
// 使用 make 函数
auto upw1 = std::make_unique<Widget>();
auto spw1 = std::make_shared<Widget>();
// 不使用 make 函数
std::unique_ptr<Widget> upw2(new Widget);
std::shared_ptr<Widget> spw2(new Widget);
在上面的代码中,upw1 和 spw1 的定义只提到了一次Widget类型,而upw2 和 spw2 的定义则需要两次。
2. 异常安全性
直接使用new可能会导致资源泄漏,尤其是在构造函数抛出异常的情况下。make函数可以确保即使在异常情况下也能正确管理资源。
void processWidget ( std::shared_ptr<Widget> spw,int priority);
int computePriority();
// 潜在的资源泄漏
processWidget(std::shared_ptr<Widget>(new Widget), computePriority());
// 使用 make 函数,保证异常安全
processWidget(std::make_shared<Widget>(), computePriority());
如果computePriority()抛出异常,在第一种情况下,new Widget分配的内存不会被释放。
假设computePriority()抛出了异常,那么执行流程如下:new Widget被执行,一个Widget对象在堆上被创建。然后尝试调用computePriority()来计算优先级。如果computePriority()抛出异常,在这个异常被捕获之前,std::shared_ptr<Widget>(new Widget)还没有机会完成其构造函数,因此它还不能管理刚刚创建的Widget对象。异常处理机制会跳过剩余的代码(包括std::shared_ptr的构造),直接进入异常处理代码。结果是new Widget分配的内存没有被任何std::shared_ptr管理,因此无法自动释放,造成内存泄漏。
使用 std::make_shared 的情况
在这种情况下,std::make_shared<Widget>()是一个单一操作,它同时负责创建Widget对象和初始化std::shared_ptr。这意味着即使在computePriority()抛出异常的情况下,也不会发生内存泄漏。
std::make_shared<Widget>()开始执行,它会在堆上分配一块内存用于Widget对象及其控制块。std::make_shared已经完成了对Widget对象的构造,并且std::shared_ptr也已经被正确初始化,可以管理这块内存。接着调用computePriority()来计算优先级。如果computePriority()抛出异常,由于std::make_shared已经成功创建了std::shared_ptr,并且这个智能指针已经开始管理Widget对象,所以在异常处理过程中,std::shared_ptr的析构函数会被调用,从而释放Widget对象占用的内存。因此,即使有异常抛出,Widget对象的内存也会被正确释放,避免了内存泄漏。
3. 性能提升
std::make_shared可以通过一次性分配内存来减少内存分配次数,从而提高效率。控制块和对象本身都存储在同一块内存区域中,这减少了内存碎片,并且提高了内存访问速度。
// 可能进行两次内存分配
std::shared_ptr<Widget>spw1(new Widget);
// 只进行一次内存分配
auto spw2 = std::make_shared<Widget>();
4. make函数的限制
自定义删除器:make函数不支持传递自定义删除器。如果需要指定自定义删除器,则必须直接使用new。
auto widgetDeleter = [](Widget* pw) { /* ... */ };
std::unique_ptr<Widget, decltype(widgetDeleter)> upw(new Widget, widgetDeleter);
花括号初始化:make函数不能直接处理花括号初始化。如果要使用花括号初始化,你需要先创建一个std::initializer_list,然后将其传递给make函数。
// 花括号初始化
auto initList = { 10, 20 };
auto spv = std::make_shared<std::vector<int>>(initList); // 通过 initializer_list 创建
当类重载了operator new和operator delete时,这些自定义的内存管理函数通常只处理特定大小的内存块(通常是对象本身的大小)。这与std::shared_ptr通过std::allocate_shared进行的内存分配不同,后者需要额外的空间来存放控制块。因此,在这种情况下使用std::make_shared可能会导致问题,因为std::make_shared尝试一次性分配足够的内存来容纳对象本身及其控制块。
重载 operator new 和 operator delete 的类
假设有一个类Widget重载了operator new和operator delete,并且这些操作符只处理sizeof(Widget)大小的内存块。
class Widget {
public:
void* operator new(size_t size) {
// 自定义内存分配逻辑
return malloc(sizeof(Widget));
}
void operator delete(void* p) noexcept {
// 自定义内存释放逻辑
free(p);
}
};
在这种情况下,如果使用std::make_shared<Widget>(),它会尝试分配比sizeof(Widget)更大的内存块,这可能与Widget的自定义内存分配逻辑不兼容。
使用 new 创建对象并传递给 std::shared_ptr
为了确保异常安全,并且在使用自定义删除器的情况下,可以将对象的创建和智能指针的构造分开,以避免潜在的内存泄漏。例如:
void processWidget(std::shared_ptr<Widget> spw, int priority);
int computePriority();
void cusDel(Widget *ptr); // 自定义删除器
// 非异常安全调用
processWidget(std::shared_ptr<Widget>(new Widget, cusDel), computePriority());
// 异常安全调用
std::shared_ptr<Widget> spw(new Widget, cusDel);
processWidget(spw, computePriority());
在这个例子中,spw的构造函数在单独的语句中执行,确保即使computePriority()抛出异常,Widget对象也会被正确地销毁。
性能优化:使用 std::move
为了提高性能,可以使用std::move将spw转换为右值,从而允许processWidget函数内部进行移动而非拷贝。
processWidget(std::move(spw), computePriority()); // 高效且异常安全
这里性能提高了多少?
大型对象的内存延迟释放
对于大型对象,如果使用std::make_shared,则对象的内存会在最后一个std::shared_ptr和最后一个std::weak_ptr都被销毁后才释放。这意味着在对象被销毁和内存实际释放之间可能会有一段延迟。
class ReallyBigType { /* ... */ };
auto pBigObj = std::make_shared<ReallyBigType>(); // 通过std::make_shared创建大对象
// ... 使用pBigObj
// 最后一个std::shared_ptr在这里销毁,但std::weak_ptrs还在
// 在这个阶段,原来分配给大对象的内存还分配着
// 最后一个std::weak_ptr在这里销毁;控制块和对象的内存被释放
相比之下,直接使用new创建对象时,一旦最后一个std::shared_ptr被销毁,对象的内存就会立即释放,而只有控制块的内存保持分配状态直到最后一个std::weak_ptr也被销毁。
class ReallyBigType { /* ... */ };
std::shared_ptr<ReallyBigType> pBigObj(new ReallyBigType); // 通过new创建大对象
// ... 使用pBigObj
// 最后一个std::shared_ptr在这里销毁,但std::weak_ptrs还在;对象的内存被释放
// 在这个阶段,只有控制块的内存仍然保持分配
// 最后一个std::weak_ptr在这里销毁;控制块内存被释放
总结:
- 和直接使用
new
相比,make
函数消除了代码重复,提高了异常安全性。对于std::make_shared
和std::allocate_shared
,生成的代码更小更快。 - 不适合使用
make
函数的情况包括需要指定自定义删除器和希望用花括号初始化。 - 对于
std::shared_ptr
s,其他不建议使用make
函数的情况包括(1)有自定义内存管理的类;(2)特别关注内存的系统,非常大的对象,以及std::weak_ptr
s比对应的std::shared_ptr
s活得更久。