何时需要从原始指针构造共享指针?

17
由于 std::make_shared 的存在,我想知道,除了与传统/库代码交互(例如存储工厂的输出)时,使用指向原始指针的 std::shared_ptr 构造函数是否有任何价值。
  • 还有其他合法的用例吗?
  • 避免使用该构造函数是合理的建议吗?
  • 甚至需要在代码中添加检查,以警告程序员每当使用它时?
  • 相同的准则(无论是什么)是否适用于 shared_ptr<T>::reset(T*)

关于代码检查: 我知道与传统/库代码交互很常见,因此自动化代码检查可能会有问题,但在我遇到的大多数情况下,我更愿意使用 unique_ptr,而且我也不是在谈论编译器警告,在 -Wall 下弹出,而是在代码审查期间进行静态代码分析的规则。


我的动机:
相对容易地说出“不要使用 std::shared_ptr<T>(new T(...)),总是优先使用 std::make_shared<T>(...)"(我认为这是正确的建议?)。但我想知道,如果必须从原始指针创建一个 shared_ptr,即使 - 或特别是 - 该对象不仅是通过 new 创建的,是否总体上存在设计问题,因为对象应该在第一次创建时就作为“共享”或“唯一”对象创建。


2
如果您想要一个指向基类的共享指针,并且希望使用指向派生类的原始指针初始化它,该怎么办? - The Paramagnetic Croissant
@TheParamagneticCroissant:可以用make_shared实现,不是吗? - MikeMB
2
我不认为盲目使用make_shared是正确的建议。请记住,使用make_shared时,只有当共享弱引用计数器都达到零时,分配的内存才会被释放。这是使用单个分配对象和控制块的代价。 - sbabbi
@sbabbi 确实,但公平地说,让过期的弱指针在足够长的时间内保持存在以至于这个问题变得重要本身就是一种代码异味。 - David Schwartz
6个回答

14
首先想到的用例是当删除器不是默认的delete时。例如,在Windows环境中,有时必须使用COM对象,这些对象的释放必须通过对象本身进行,通过Release实现。当然,可以使用ATL,但并不是每个人都想使用它。
struct ReleaseCom {
  template <class T>
  void operator() (T* p) const
  {
    p->Release();
  }
};
IComInterface* p = // co created or returned as a result
std::share_ptr<IComInterface> sp(p, ReleaseCom());

一种更不常见的情况 - 但仍然有效 - 是当对象(句柄甚至原始内存)在dll、操作系统或库中自定义分配,并具有自己的关联清理函数必须被调用(该函数可能会调用delete,也可能不会)。如果涉及内存分配,则std::allocate_shared提供了更加强大的控制分配器的功能,而不会暴露出一个原始指针。
我个人认为,鉴于std::make_sharedstd::allocate_shared,使用原始指针构造shared_ptr的要求正在变得越来越少。即使是上述情况,也可以包装成实用程序分配和管理函数,从主业务逻辑代码中移除。

对啊,我完全忘记了可定制的删除器。所以只需要单个参数构造函数,但可能应该避免使用它。 - MikeMB
@MikeMB。一般情况下是这样的,但也有一些情况需要使用“通常”的删除方式,但是分配内存的代码无法修改 - 你只能得到一个已经分配好的对象并需要管理它。我认为建议使用make_shared,但构造函数仍然是必需的,因为make_shared并不总是覆盖所有用例。 - Niall
std::allocate_shared 提供了另一种选择,避免了不受保护的指针和默认删除器。 - Potatoswatter
2
@Potatoswatter。如果创建涉及到内存分配,那么这是正确的,但我不确定对于“句柄”类型的分配(例如文件句柄)是否适用。 - Niall
@Niall 你是指 operator(T* p) 还是 operator()(T* p) - nonsensation
@serthy。调用运算符,这是一个打字错误。 - Niall

9

现代C++效能中,Scott Meyers提到了几个例外情况。

第一个已经提过。如果你需要提供自定义删除器,就不能使用make_shared

第二个例外是,如果你的系统存在内存问题,并且要分配一个非常大的对象,那么使用make_shared会为对象和包含引用计数的控制块分配一个块。如果你有任何指向对象的weak_ptr,则内存无法被释放。另一方面,如果你没有使用make_shared,则在最后一个shared_ptr被删除时,非常大的对象的内存可以被释放。


5

• 还有其他合法的使用情况吗?

是的:当资源不映射到new/delete时,存在这样一种情况:

handle_type APIXCreateHandle(); // third party lib
void APIXDestroyHandle(handle_type h); // third party lib

在这种情况下,您将需要直接使用构造函数。
• 避免使用该构造函数是合理的建议吗?
当功能与make_shared重叠时,是的。
• shared_ptr :: reset(T *)应遵循相同的准则(无论它们是什么)吗?
我认为不应该。如果您真的想要,可以用std :: make_shared调用的结果替换reset调用的赋值。但我更喜欢看到reset调用-这样会更明确地表达意图。
关于代码检查:我知道与遗留/库代码进行接口很常见
考虑将第三方库封装成一个接口,以确保返回的值是unique_ptr包装的。这将为您提供集中化点和其他便利/安全优化的机会。
相对容易说...(我相信这是正确的建议?)。但我想知道是否总体上不是一种设计问题。
只有在std :: make_shared / std :: make_unique 同样适用时才使用它,这并不是设计问题。 编辑:为了解决问题的要点:您可能无法为此添加静态分析规则,除非您还添加了一个约定/异常列表(即“除自定义删除器和第三方lib API适配层外,应始终使用make_shared和make_unique”)。这样的规则可能会跳过某些文件。
但是,当您直接使用构造函数时,最好将它们放在专门用于此目的的函数中(类似于make_unique和make_shared所做的)。
namespace api_x
{
    std::shared_ptr<handle_type> make_handle(); // calls APIXCreateHandle internally
                                                // also calls the constructor
                                                // to std::shared_ptr
}

3

尽可能使用std::make_shared

然而,有两个原因可能不适用:

  • 您想为对象使用自定义删除器。如果您与不使用RAII的代码进行接口,则可能是这种情况。其他答案提供了更多细节。

  • 您有一个自定义的new运算符需要调用。std::make_shared无法为您调用它,因为它的整个目的是分配内存一次(虽然标准不要求)。请参见http://ideone.com/HjmFl1

std::make_shared的理由不是为了避免使用new关键字,而是为了避免内存分配。共享指针需要设置一些控制数据结构。因此,在没有使用make_shared时设置一个shared_ptr,您必须进行两个分配:一个用于对象,另一个用于控制结构。 make_shared(通常)只分配一个(较大的)内存块,并在原地构造包含的对象和控制结构。 make_shared背后的原因(最初)是它在大多数情况下更有效率,而不是语法更漂亮。这也是为什么在C++11中没有std::make_unique的原因。(正如ChrisDrew所指出的那样,我建议添加std::make_unique只是为了对称/更漂亮的语法。但它可以帮助以更紧凑的方式编写异常安全代码,请参见Herb Sutter's GotW #102this question。)

2
你对 make_shared 的原始目的是正确的,但避免使用 new 关键字不仅仅是为了使“语法更漂亮”,它还可以使代码更加安全。而这正是 make_unique 被包含在 C++14 中的主要原因。 - Chris Drew

3
除了其他答案之外,如果构造函数是私有的,例如来自工厂函数,您不能使用make_shared
class C
{
public:
    static std::shared_ptr<C> create()
    {
        // fails
        // return std::make_shared<C>();

        return std::shared_ptr<C>(new C);
    }
private:
    C();
};

0

许多答案都提供了至少一个独特的方面,因此我决定做一个总结回答。感谢@Niall、@Chris Drew、@utnapistim、@isanae、@Markus Mayr和(通过代理)Scott Meyers。

你可能不想/不能使用make_shared的原因有:

  • 你必须使用自定义删除器和/或甚至是自定义分配器/新运算符。
    这可能是最常见的原因,特别是在与第三方库进行交互时。std::allocate_shared可以在某些情况下提供帮助。
  • 如果内存是一个问题,并且你经常有弱指针超过对象的生命周期。
    由于管理对象是在与控制块相同的内存块上创建的,因此直到最后一个弱指针被销毁之前,内存才不能被释放。
  • 如果你有一个私有构造函数,例如只有一个公共工厂函数。
    在这种情况下,使make_shared成为友元不是可行的解决方案,因为实际的构造可能发生在某个辅助函数/类中。
关于代码检查,上述的异常情况可能对于自动检查“尽可能使用make_shared”指南以及将违反该指南称为设计问题而言过多。
然而,值得一提的是,即使库需要自定义分配器和释放器函数,我们也可以制作自己的版本的make shared,至少封装了分配和删除调用,并增加了异常安全性(虽然它很可能不提供单个分配优势)。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接