C++11 中引入了智能指针, 同时还有一个模板函数 std::make_shared
可以返回一个指定类型的 std::shared_ptr
, 那与 std::shared_ptr
的构造函数相比它能给我们带来什么好处呢 ?
优点
效率更高
shared_ptr
需要维护引用计数的信息,
- 强引用, 用来记录当前有多少个存活的 shared_ptrs 正持有该对象. 共享的对象会在最后一个强引用离开的时候销毁( 也可能释放).
- 弱引用, 用来记录当前有多少个正在观察该对象的 weak_ptrs. 当最后一个弱引用离开的时候, 共享的内部信息控制块会被销毁和释放 (共享的对象也会被释放, 如果还没有释放的话).
如果你通过使用原始的 new 表达式分配对象, 然后传递给 shared_ptr (也就是使用 shared_ptr 的构造函数) 的话, shared_ptr 的实现没有办法选择, 而只能单独的分配控制块:
|
如果选择使用 make_shared
的话, 情况就会变成下面这样:
|
内存分配的动作, 可以一次性完成. 这减少了内存分配的次数, 而内存分配是代价很高的操作.
关于两种方式的性能测试可以看这里 Experimenting with C++ std::make_shared
异常安全
看看下面的代码:
|
C++ 是不保证参数求值顺序, 以及内部表达式的求值顺序的, 所以可能的执行顺序如下:
- new Lhs(“foo”))
- new Rhs(“bar”))
- std::shared_ptr
- std::shared_ptr
好了, 现在我们假设在第 2 步的时候, 抛出了一个异常 (比如 out of memory, 总之, Rhs 的构造函数异常了), 那么第一步申请的 Lhs 对象内存泄露了. 这个问题的核心在于, shared_ptr 没有立即获得裸指针.
我们可以用如下方式来修复这个问题.
|
当然, 推荐的做法是使用 std::make_shared
来代替:
|
缺点
构造函数是保护或私有时,无法使用 make_shared
make_shared
虽好, 但也存在一些问题, 比如, 当我想要创建的对象没有公有的构造函数时, make_shared
就无法使用了, 当然我们可以使用一些小技巧来解决这个问题, 比如这里 How do I call ::std::make_shared on a class with only protected or private constructors?
对象的内存可能无法及时回收
make_shared
只分配一次内存, 这看起来很好. 减少了内存分配的开销. 问题来了, weak_ptr
会保持控制块(强引用, 以及弱引用的信息)的生命周期, 而因此连带着保持了对象分配的内存, 只有最后一个 weak_ptr
离开作用域时, 内存才会被释放. 原本强引用减为 0 时就可以释放的内存, 现在变为了强引用, 若引用都减为 0 时才能释放, 意外的延迟了内存释放的时间. 这对于内存要求高的场景来说, 是一个需要注意的问题. 关于这个问题可以看这里 make_shared, almost a silver bullet
本章所有内容均从C++ Primer摘录总结
1.为什么使用make_shared?
make_shared函数的主要功能是在动态内存中分配一个对象并初始化它,返回指向此对象的shared_ptr;由于是通过shared_ptr管理内存,因此一种安全分配和使用动态内存的方法。
如下为make_shared的使用:
-
//p1指向一个值为"9999999999"的string
-
shared_ptr<string> p1 = make_shared<string>(10, '9');
-
shared_ptr<string> p2 = make_shared<string>("hello");
-
shared_ptr<string> p3 = make_shared<string>();
从上述例子我们可以看出以下几点:
1)make_shared是一个模板函数;
2)make_shared模板的使用需要以“显示模板实参”的方式使用,如上题所示make_shared<string>(10, 9),如果不传递显示 模板实参string类型,make_shared无法从(10, '9')两个模板参数中推断出其创建对象类型。
3)make_shared在传递参数格式是可变的,参数传递为生成类型的构造函数参数,因此在创建shared_ptr<T>对象的过程中调用了类型T的某一个构造函数。
2.make_shared模板实现
如下为make_shared的库函数实现版本:
-
template<typename _Tp, typename... _Args>
-
inline shared_ptr<_Tp>
-
make_shared(_Args&&... __args)
-
{
-
typedef typename std::remove_const<_Tp>::type _Tp_nc;
-
return std::allocate_shared<_Tp>(std::allocator<_Tp_nc>(),
-
std::forward<_Args>(__args)...);
-
}
-
template<typename _Tp, typename _Alloc, typename... _Args>
-
inline shared_ptr<_Tp>
-
allocate_shared(const _Alloc& __a, _Args&&... __args)
-
{
-
return shared_ptr<_Tp>(_Sp_make_shared_tag(), __a,
-
std::forward<_Args>(__args)...);
-
}
我们依次分析上述的关键代码
-
//关键行1
-
template<typename _Tp, typename... _Args>
-
inline shared_ptr<_Tp> make_shared(_Args&&... __args)
-
//关键行2
-
std::forward<_Args>(__args)...
-
//关键行3
-
return shared_ptr<_Tp>(_Sp_make_shared_tag(), __a,
-
std::forward<_Args>(__args)...);
从上述关键代码可以看出:make_shared是组合使用可变参数模板与forward(转发)机制实现将实参保持不变地传递给其他函数。如最开始的string例子。
1)使用可变参数:是因为string有多个构造函数,且参数各不相同;
2)Args参数为右值引用(Args&&)和std::forward:是为了保持实参中类型信息的传递。这样当传递一个右值string&& 对象给make_shared时,就可以使用string的移动构造函数进行初始化。注意,两者必须结合使 用,缺一不可;
此外std::forward<_Args>(__args)...是采用包扩展形式调用的,原理如下:
-
shared_ptr<string> p1 = make_shared<string>(10, '9');
-
//扩展如下,对两个参数分别调用std::forward
-
return shared_ptr<string>(_Sp_make_shared_tag(), _a ,
-
std::forward<int>(10),
-
std::forward<char>(c));
补充说明:
①模板参数为右值引用,采用引用折叠原理:
-
1)参数为左值时,实参类型为普通的左值引用; T& &, T&& &,T& && =>T&
-
2)参数为右值时,实参类型为右值: T&& && => T&&
②std::forward:是一个模板,通过显示模板实参来调用,调用后forward返回显示实参类型的右值引用。即,forward<T>的返回类型为T&&,在根据上述引用折叠原理即可保存参数是左值还是右值类型;比如:
-
int i = 0;
-
std::forward<int>(i), i将以int&传递
-
std::forward<int>(42), 42将以int&&传递
std::forward实现代码:
-
/**
-
* @brief Forward an lvalue.
-
* @return The parameter cast to the specified type.
-
*
-
* This function is used to implement "perfect forwarding".
-
*/
-
template<typename _Tp>
-
constexpr _Tp&&
-
forward(typename std::remove_reference<_Tp>::type& __t) noexcept
-
{ return static_cast<_Tp&&>(__t); }
-
/**
-
* @brief Forward an rvalue.
-
* @return The parameter cast to the specified type.
-
*
-
* This function is used to implement "perfect forwarding".
-
*/
-
template<typename _Tp>
-
constexpr _Tp&&
-
forward(typename std::remove_reference<_Tp>::type&& __t) noexcept
-
{
-
static_assert(!std::is_lvalue_reference<_Tp>::value, "template argument"
-
" substituting _Tp is an lvalue reference type");
-
return static_cast<_Tp&&>(__t);
-
}