我啥时候想从原始指针构造一个共享指针

Posted

技术标签:

【中文标题】我啥时候想从原始指针构造一个共享指针【英文标题】:When would I want to construct a shared pointer from a raw pointer我什么时候想从原始指针构造一个共享指针 【发布时间】:2015-07-24 08:11:27 【问题描述】:

感谢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创建的,因为对象首先应该被创建为“共享”或“唯一”对象。

【问题讨论】:

如果你想要一个shared-pointer-to-base-class,并且你想用一个raw-pointer-to-derived-class来初始化它呢? @TheParamagneticCroissant:这可以通过make_shared 实现,不是吗? 我不认为盲目使用make_shared 是一个正确的建议。请记住,当共享 弱计数器都达到零时,使用 make shared 分配的内存被释放。这是对对象和控制块使用单一分配的代价。 @sbabbi 是的,但是,公平地说,过期的弱指针存在足够长的时间来解决这个问题本身就是一种代码味道。 【参考方案1】:

首先想到的用例是删除器不是默认的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 的要求越来越少。即使是上述情况,也可以封装到实用程序分配和管理功能中,从主要业务逻辑代码中删除。

【讨论】:

对,完全忘记了可定制的删除器。所以它只是可能应该避免的单个 Argument 构造函数。 @MikeMB。作为一般情况,是的。但话虽如此,在某些情况下需要“通常”删除,但执行分配的代码是不可修改的——你只是得到一个已经分配的对象并需要管理它。我认为建议更喜欢make_shared,但仍然需要构造函数,因为make_shared 并不总是涵盖所有用例。 std::allocate_shared 提供了另一种选择,避免了不受保护的指针和默认的删除器。 @Potatoswatter。确实,如果创建涉及内存分配,但我不确定这是否适用于“句柄”类型分配(等文件句柄)。 @Niall 你的意思是operator(T* p) 还是operator()(T* p)【参考方案2】:

Scott Meyers 在Effective Modern C++ 中列出了一些例外情况

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

第二个是如果你在一个有内存问题的系统上并且你正在分配一个非常大的对象然后使用make_shared 为对象和包含 控制块 分配一个块em>弱引用计数。如果您有任何 weak_ptrs 指向该对象,则无法释放内存。另一方面,如果您没有使用过make_shared,那么一旦最后一个shared_ptr 被删除,那么非常大的对象的内存就可以被释放。

【讨论】:

【参考方案3】:

• 还有其他合法的用例吗?

是的:有一种情况是资源没有映射到新/删除:

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

在这种情况下,您将希望直接使用构造函数。

• 避免使用该构造函数是否合理?

当功能与 make_shared 重叠时,是的。

• 相同的准则(无论它们是什么)是否应该适用于 shared_ptr::reset(T*)?

我认为他们不应该这样做。如果你真的想要,你可以用一个带有 std::make_shared 调用结果的赋值替换 reset 调用。不过,我更愿意看到重置调用——它的意图会更明确。

关于代码检查:我知道与遗留/库代码的接口很常见

考虑将第三方库包装到一个接口中,以确保返回的值是 unique_ptr-wrapped。这将为您提供集中点和其他便利/安全优化的机会。

说 [...] 相对容易(我认为这是正确的建议?)。但我想知道这是否不是一般的设计气味

使用它不是设计气味;仅在 std::make_shared/std::make_unique 也能正常工作时使用它。

编辑:解决问题的重点:您可能无法为此添加静态分析规则,除非您还向其中添加约定/例外列表(即“除了自定义删除器和第三方库 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

【讨论】:

【参考方案4】:

尽可能使用std::make_shared

您可能无法摆脱它的原因有两个:

您想为您的对象使用自定义删除器。如果您使用不使用 RAII 的代码进行交互,则可能会出现这种情况。其他答案提供了更多详细信息。

您有一个要调用的自定义新运算符。 std::make_shared 不能为您调用它,因为它的全部意义在于分配内存一次(尽管标准没有要求)。见http://ideone.com/HjmFl1。

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

【讨论】:

您对make_shared 的最初原因是正确的,但避免使用new 关键字不仅是为了使“语法更漂亮”,它还可以使代码更加安全。而 thatmake_unique 被包含在 C++14 中的主要原因。【参考方案5】:

除了其他答案之外,如果构造函数是私有的,例如来自工厂函数,则不能使用 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();
;

【讨论】:

【参考方案6】:

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

您可能不想要/可能无法使用make_shared 的原因是:

您必须使用自定义删除器和/甚至自定义分配器/新运算符。 这可能是最常见的原因,尤其是在与 3rd 方库交互时。 std::allocate_shared 在某些情况下可能会有所帮助。 如果内存是一个问题,并且您经常有比对象寿命更长的弱指针。 由于托管对象是在与控制块相同的内存块上创建的,因此在最后一个弱指针也被销毁之前无法释放内存。 如果您有私有构造函数,例如只是一个公共工厂函数。 在这种情况下,让make_shared 成为朋友不是一个可行的解决方案,因为实际的构造可能发生在某个辅助函数/类中。

关于代码检查,上面列出的异常可能太多了,无法自动检查“尽可能使用make_shared准则,以及调用违反该准则的行为设计气味本身。

作为即将到来的亮点,即使库需要自定义分配器和释放器函数并且我们不能/不想使用std::allocate_shared,我们也可以制作自己的 make shared 版本,至少封装分配和删除调用并增加异常安全性(尽管它很可能不提供单一分配优势)。

【讨论】:

以上是关于我啥时候想从原始指针构造一个共享指针的主要内容,如果未能解决你的问题,请参考以下文章

是否可以定义指向构造函数的函数指针?

共享指针初始化器列表构造函数及其类型构造函数的更改

07. this指针,构造和析构,new和delete

07. this指针,构造和析构,new和delete

我啥时候应该明确使用 `this` 指针?

返回 boost::shared_ptr 和从返回的原始指针构造 boost::shared_ptr 有啥区别?