std::make_shared

C++11 中引入了智能指针, 同时还有一个模板函数 std::make_shared 可以返回一个指定类型的 std::shared_ptr, 那与 std::shared_ptr 的构造函数相比它能给我们带来什么好处呢 ?

优点

效率更高

shared_ptr 需要维护引用计数的信息,

  • 强引用, 用来记录当前有多少个存活的 shared_ptrs 正持有该对象. 共享的对象会在最后一个强引用离开的时候销毁( 也可能释放).
  • 弱引用, 用来记录当前有多少个正在观察该对象的 weak_ptrs. 当最后一个弱引用离开的时候, 共享的内部信息控制块会被销毁和释放 (共享的对象也会被释放, 如果还没有释放的话).

如果你通过使用原始的 new 表达式分配对象, 然后传递给 shared_ptr (也就是使用 shared_ptr 的构造函数) 的话, shared_ptr 的实现没有办法选择, 而只能单独的分配控制块:

 
  1. auto p = new widget();

  2. shared_ptr sp1{ p }, sp2{ sp1 };

  3.  

如果选择使用 make_shared 的话, 情况就会变成下面这样:

 
auto sp1 = make_shared(), sp2{ sp1 };

内存分配的动作, 可以一次性完成. 这减少了内存分配的次数, 而内存分配是代价很高的操作.

关于两种方式的性能测试可以看这里 Experimenting with C++ std::make_shared

异常安全

看看下面的代码:

 
  1. void F(const std::shared_ptr<Lhs>& lhs, const std::shared_ptr<Rhs>& rhs) { /* ... */ }

  2.  
  3. F(std::shared_ptr<Lhs>(new Lhs("foo")),

  4. std::shared_ptr<Rhs>(new Rhs("bar")));

  5.  

C++ 是不保证参数求值顺序, 以及内部表达式的求值顺序的, 所以可能的执行顺序如下:

  1. new Lhs(“foo”))
  2. new Rhs(“bar”))
  3. std::shared_ptr
  4. std::shared_ptr

好了, 现在我们假设在第 2 步的时候, 抛出了一个异常 (比如 out of memory, 总之, Rhs 的构造函数异常了), 那么第一步申请的 Lhs 对象内存泄露了. 这个问题的核心在于, shared_ptr 没有立即获得裸指针.

我们可以用如下方式来修复这个问题.

 
  1. auto lhs = std::shared_ptr<Lhs>(new Lhs("foo"));

  2. auto rhs = std::shared_ptr<Rhs>(new Rhs("bar"));

  3. F(lhs, rhs);

  4.  

当然, 推荐的做法是使用 std::make_shared 来代替:

 
F(std::make_shared<Lhs>("foo"), std::make_shared<Rhs>("bar"));

缺点

构造函数是保护或私有时,无法使用 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的使用:

  1. //p1指向一个值为"9999999999"的string

  2. shared_ptr<string> p1 = make_shared<string>(10, '9');

  3.  
  4. shared_ptr<string> p2 = make_shared<string>("hello");

  5.  
  6. 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的库函数实现版本:

  1. template<typename _Tp, typename... _Args>

  2. inline shared_ptr<_Tp>

  3. make_shared(_Args&&... __args)

  4. {

  5. typedef typename std::remove_const<_Tp>::type _Tp_nc;

  6. return std::allocate_shared<_Tp>(std::allocator<_Tp_nc>(),

  7. std::forward<_Args>(__args)...);

  8. }

  9.  
  10. template<typename _Tp, typename _Alloc, typename... _Args>

  11. inline shared_ptr<_Tp>

  12. allocate_shared(const _Alloc& __a, _Args&&... __args)

  13. {

  14. return shared_ptr<_Tp>(_Sp_make_shared_tag(), __a,

  15. std::forward<_Args>(__args)...);

  16. }

我们依次分析上述的关键代码

  1. //关键行1

  2. template<typename _Tp, typename... _Args>

  3. inline shared_ptr<_Tp> make_shared(_Args&&... __args)

  4.  
  5. //关键行2

  6. std::forward<_Args>(__args)...

  7.  
  8. //关键行3

  9. return shared_ptr<_Tp>(_Sp_make_shared_tag(), __a,

  10. std::forward<_Args>(__args)...);

从上述关键代码可以看出:make_shared是组合使用可变参数模板与forward(转发)机制实现将实参保持不变地传递给其他函数。如最开始的string例子
1)使用可变参数:是因为string有多个构造函数,且参数各不相同;

2)Args参数为右值引用(Args&&)和std::forward:是为了保持实参中类型信息的传递。这样当传递一个右值string&& 对象给make_shared时,就可以使用string的移动构造函数进行初始化。注意,两者必须结合使 用,缺一不可;

此外std::forward<_Args>(__args)...是采用包扩展形式调用的,原理如下:

  1. shared_ptr<string> p1 = make_shared<string>(10, '9');

  2.  
  3. //扩展如下,对两个参数分别调用std::forward

  4. return shared_ptr<string>(_Sp_make_shared_tag(), _a ,

  5. std::forward<int>(10),

  6. std::forward<char>(c));

补充说明:
①模板参数为右值引用,采用引用折叠原理:

  1. 1)参数为左值时,实参类型为普通的左值引用; T& &, T&& &,T& && =>T&

  2. 2)参数为右值时,实参类型为右值: T&& && => T&&

std::forward:是一个模板,通过显示模板实参来调用,调用后forward返回显示实参类型的右值引用。即,forward<T>的返回类型为T&&,在根据上述引用折叠原理即可保存参数是左值还是右值类型;比如:

  1. int i = 0;

  2. std::forward<int>(i), i将以int&传递

  3. std::forward<int>(42), 42将以int&&传递

std::forward实现代码:

  1. /**

  2. * @brief Forward an lvalue.

  3. * @return The parameter cast to the specified type.

  4. *

  5. * This function is used to implement "perfect forwarding".

  6. */

  7. template<typename _Tp>

  8. constexpr _Tp&&

  9. forward(typename std::remove_reference<_Tp>::type& __t) noexcept

  10. { return static_cast<_Tp&&>(__t); }

  11.  
  12. /**

  13. * @brief Forward an rvalue.

  14. * @return The parameter cast to the specified type.

  15. *

  16. * This function is used to implement "perfect forwarding".

  17. */

  18. template<typename _Tp>

  19. constexpr _Tp&&

  20. forward(typename std::remove_reference<_Tp>::type&& __t) noexcept

  21. {

  22. static_assert(!std::is_lvalue_reference<_Tp>::value, "template argument"

  23. " substituting _Tp is an lvalue reference type");

  24. return static_cast<_Tp&&>(__t);

  25. }

猜你喜欢

转载自blog.csdn.net/zhgeliang/article/details/81237053
今日推荐