【问题标题】:Convert a shared_ptr to regular a pointer将 shared_ptr 转换为常规指针
【发布时间】:2017-02-14 16:13:29
【问题描述】:

我们有一个函数,它返回一个新分配的对象作为输出参数(引用指针)。

MyFunc(MyObject*& obj)
{
    obj = new MyObject();
}

这样称呼:

Object* obj;
MyFunc(obj);

该函数在内部做了很多工作,并使用shared_ptr 进行内存管理。完成后,我们要返回的对象由shared_ptr 引用。我正在努力研究如何将新分配的对象作为常规指针返回。

我们希望在内部继续使用shared_ptr 以降低风险,但返回shared_ptr 似乎没有意义,因为调用者对返回的值拥有完全的所有权(被调用的函数或对象不再需要或保留对返回数据的引用),我们希望它们具有灵活性。

是否有人对允许我们在内部使用shared_ptr 但有一个常规指针接口有什么建议?谢谢

【问题讨论】:

  • 听起来你应该使用 unique_ptr 代替,然后返回 .get()
  • @AndersK。我也不希望调用者被要求使用 unique_ptr。
  • 不能换界面吗?您需要与C代码或其他东西交互吗?为什么不能像惯用的 C++ 那样只返回一个智能指针?对指针的引用太丑陋了。
  • @denver 如果您不共享变量,则在内部使用 unique_ptr,一旦您想通过 .release() 将所有权传递给调用者以获取指针
  • @denver,然后请编辑您的问题,因为尚不清楚您所指的 shered_ptr 的预期/想要/实际范围和生命周期是什么。这可能会在一个正式正确但不适合您的场景的答案与一个适用于您的案例的正确答案之间产生差异。

标签: c++ c++11 pointers shared-ptr


【解决方案1】:

如果由于某种原因您不能/不想使用std::unique_ptrstd::auto_ptr(例如,如果您在创建期间出于某种原因需要在内部拥有多个所有者,或者您的底层方法需要传递std::shared_ptr),您仍然可以通过使用自定义删除器使其与std::shared_ptr 一起使用,如下所述:https://stackoverflow.com/a/5995770/1274747

原则上,在return前完成后,将deleter切换为不实际删除实例(使deleter为“null”),然后通过shared_ptrget()返回。即使所有 shared_ptr 对象都被销毁,内存也不会被删除(因为空的删除器会跳过删除)。

cmets 中还有一个不太明显的链接,您可能会感兴趣: http://paste.ubuntu.com/23866812/ (不确定在所有情况下如果没有交换机的共享所有权,它是否真的可以工作,需要测试)


编辑

正如预期的那样,使用 pastebin 中链接的简单可解除删除器需要小心,因为删除器实际上被复制以存储在 std::shared_ptr 中。

但您仍然可以使用std::ref 使其工作:

MyFunc(MyObject*& obj)
{
    DisarmableDelete<MyObject> deleter;
    std::shared_ptr<MyObject> ptr(new MyObject(), std::ref(deleter));
    // do what is necessary to setup the object - protected by deleter
    // ...
    // disarm before return
    deleter._armed = false;
    obj = ptr.get();
    // deleter disarmed - object not freed
}

为了完整起见(并避免未来可能出现的断开链接),这里是来自http://paste.ubuntu.com/23866812/DisarmableDelete 的实现。

template <typename T, typename Deleter = typename std::default_delete<T> >                                                                                                                              
    struct DisarmableDelete : private Deleter {                                                                                                                                                         
        void operator()(T* ptr) { if(_armed) Deleter::operator()(ptr); }                                                                                                                                
        bool _armed = true;                                                                                                                                                                             
    };        

【讨论】:

  • 如果您使用 shared_ptr - 如 OP 所述 - 用于“内存管理”,然后有目的地提供一个“空”删除器,那么你最好只使用原始指针首先是“新”。
  • 是的,但是如果在您完成所有操作之前发生了不好的事情(例如异常),您仍然可能希望自动删除该对象。请注意,删除器从一开始就不是“null”,但只有在一切正常并且您确实想要返回(并丢弃所有权)之后才将其设为“null”。
  • 感谢您跟进该示例。
  • 为什么要打扰std::ref,而只是ptr.get_deleter()._armed = false;
  • 可以通过ptr.get_deleter() 访问_armed 吗? AFAIK shared_ptr 删除器是类型擦除的,因此访问_armed 可能涉及强制转换(真的,我们知道类型,所以我们可以static_cast)。
【解决方案2】:

这取决于谁“拥有”指针,一旦它暴露于“外部世界”。所有权本质上归结为:“谁负责稍后释放此内存?”

可以用一个简单的问题来回答:当MyFunc被调用时,调用者是否负责在完成后删除指针?

  1. 如果是,那么MyFunc 需要“释放”所有权,否则shared_ptr 会在超出范围时自动删除指针。这实际上不能使用shared_ptr 来完成。您需要改用unique_ptr,并调用unique_ptr::release()
  2. 如果不是 - 如果MyFunc 将简单地使用 结果指针并在没有delete-ing 的情况下忘记它 - 那么您可以简单地使用返回“原始”指针shared_ptr::get()。您必须小心,因为这意味着 shared_ptr 仍然存在于您的代码中的其他位置。

【讨论】:

  • 不会返回 shared_ptr::get() 返回一个无效的指针,因为当 MyFunc 结束并且 shared_ptr 超出范围时该对象将被删除。
  • 如果 shared_ptr 仍然存在于其他地方,则不会。这就是为什么你需要回答这个问题:谁“拥有”指针?
  • 调用者拥有所有权。
  • 那你就不能使用shared_ptr了。你必须使用 unique_ptr::release()。或者您必须更改 API,以便调用者也提供一个 shared_ptr。或者只是使用带有“new”的原始指针。
  • OP 专门将 shared_ptr 的使用描述为“函数内部”,这可能意味着它的范围仅限于该函数,因此它将在函数退出时调用删除器,而原始指针将是一个悬空指针...繁荣
【解决方案3】:

我可以看到四种选择,如下所示。它们都很可怕,没有将您的所有权切换到std::unique_ptr&lt;T&gt; 并通过obj = ptr.release(); 返回我只能提供一个技巧,其中参数在销毁时分配给指针,但您仍然需要捕获异常并测试是否指针已分配。

#include <iostream>
#include <memory>
#include <exception>

struct foo {
  void bar() const { std::cout << this << " foo::bar()\n"; }
  ~foo() { std::cout << this << " deleted\n"; }
};

void f1(foo*& obj) {
  obj = new foo;
  // do stuff... if an exception is thrown before we return we are
  // left with a memory leak
}

void f2(foo*& obj) {
  auto holder = std::make_shared<foo>();
  // do stuff.. if an exception is thrown the pointer will be
  // correclty deleted.

  obj = holder.get(); // awesome, I have a raw pointer!
} // oops, the destructor gets called because holder went out of
  // scope... my pointer points to a deleted object.

void f3(foo*& obj) {
  auto holder = std::make_unique<foo>();
  // do stuff.. if an exception is thrown the pointer will be
  // correclty deleted.

  obj = holder.release(); // awesome, I have a raw pointer!
} // no problem whem holder goes out of scope because it does not own the pointer

void f4(foo*& obj) {
  // a super-weird hack that assigns obj upon deletion
  std::shared_ptr<foo> holder(new foo, [&obj](foo*& p){  obj = p; });
  throw std::exception();
} // no problem whem holder goes out of scope because it does not own
  // the pointer... but if an execption is throw we need to delete obj

int main() {

  foo* p1;
  f1(p1);
  p1->bar();

  foo* p2;
  f2(p2);
  // p2->bar(); // error

  foo* p3;
  f3(p3);
  p3->bar();

  foo* p4;
  try {
    f4(p4);
  } catch(...) {
    std::cout << "caught an exception... test whether p4 was assigned  it\n";
  }
  p4->bar(); // I still need to delete this thing
}

【讨论】:

    【解决方案4】:

    shared_ptr 的意义在于表达共享所有权。

    任何不共享对象所有权的事物——无权延长对象生命周期,并且对象生命周期是共享所有者请求对象生命周期的联合——不应该使用shared_ptr

    在这里,您有一个shared_ptr,您将返回它。此时,您违反了shared_ptr 的假设;任何保留shared_ptr 副本的人都希望其内容可以持续多久。

    同时,调用代码认为它拥有您传递给它的 MyObject* 原始指针。

    这是一个滥用shared_ptr的例子。

    说“我们有内存管理问题,请使用shared_ptr”并不能解决内存管理问题。正确使用 shared_ptr 需要谨慎和设计,并且设计必须是当相关对象的生命周期结束时由 2 条或更多条数据和/或代码共享。

    如果内部代码不拥有指针,则应使用observer_ptr&lt;T&gt; 或原始T* 之类的东西(首先明确表示它不拥有该对象)。

    所有权应该是明确的,并且在unique_ptr 中。如果需要,它可以调用.release() 将所有权传递给原始指针;实际上,我会将您的签名更改为采用unique_ptr&amp;,或者让它返回unique_ptr

    当调用者想要使用其他对象生命周期管理系统时,调用者会调用.release(),或者该对象生命周期管理系统应该使用unique_ptrs(因此非常清楚获取事物的所有权)。


    使用非黑客解决方案。

    std::shared_ptr&lt;std::unique_ptr&lt;T&gt;&gt;。在这种情况下,您共享了唯一所有权的所有权。

    unique_ptr 可以从它那里获得所有权(通过.release())。当它这样做时,所有仍然存在的shared_ptrs 的unique_ptr 也将被清除。

    这会将共享的唯一所有权放在首位和中心位置,而不是将非删除器入侵到 shared_ptr 中,并让那些认为自己拥有数据所有权但没有所有权的 shared_ptr 悬空。

    【讨论】:

    • 感谢您的意见。问题是关于通过抽象接口(应用程序调用第三方库)返回对象。没有内存管理问题,也没有跨接口共享数据所有权,这是一种所有权被转移的情况。一侧正在生成数据供另一侧使用。
    • 这个问题是关于界面的每一面都能够以自己的方式管理内存。 unique_ptr 而不是原始指针将是传达关系的适当方式,但从库的角度来看,它不是对象生命周期的适当表示,并且表示需要发生变化才能通过这样的接口传回。
    • @denver 对象的生命周期是托管的还是不托管的。如果您有两个系统都在管理同一个对象的生命周期,那么您的设计就失败了。唯一的 ptr 实际上可以放弃对象的生命周期(或者,对于销毁器,可以任意定义生命周期结束的含义)。共享 ptr 从根本上不能做到这一点;这是尝试的类型错误。您可以破解它,但这是围绕共享 ptr 的设计进行破解,而不是正确使用它。设计的共享ptr不能很好地与其他人一起玩;强制它会导致错误和代码债务。
    • 调用者不在乎数据来自哪里或如何创建,从它的角度来看它只是获取它。类似地,数据的生成者应该能够管理它,但它是适当的(在这种情况下是某种共享表示,因为对象是由多个贡献者组装而成的),同时它拥有它的所有权,而不关心调用者如何管理它。此外,unique_ptr(和其他 STL 类型)通常不安全地通过库/应用程序边界,因为它可能在边界的每一侧都有不同的定义,特别是在使用不同的编译器时。
    • 我从这个角度理解在内部使用 shared_ptr 的局限性,问题实际上是关于如何绕过它。在这种情况下,绕过限制看起来更有利,然后推出某种自定义解决方案。
    【解决方案5】:

    最好的方法是在内部使用unique_ptr 并在返回原始指针之前调用它的 .release() 方法。

    如果你坚持在内部使用 shared_ptr,一个选项是创建它,指定一个自定义的“noop”删除器,它在 shared_ptr 被销毁时什么都不做(而不是在拥有的指针上调用 delete)。像往常一样从shared_ptr 获取原始指针(.get() 方法)。 可以在 Boost 库 (null_deleter) 中找到此类删除器的示例。
    但请注意,这样做有效地“禁用”了 shared_ptr 的用处......

    【讨论】:

    • 酷,不知道boost::null_deleter。我检查了它,不幸的是它在这种情况下可能没有那么有用,因为如果在返回之前发生了某些事情,OP显然仍然想释放指针,所以需要一个删除器,它在开始时不是空的,但可以是在从函数返回之前“设为空”。
    • @axalis 不是 null_deleter,但自定义删除器方法仍然有效,只需要一个更复杂的方法,可以在两种行为之间切换(deleter => null 删除器)
    • @roalz 感谢您帮助推动讨论。
    【解决方案6】:

    如果您在内部使用 std::shared_ptr 管理已分配对象的生命周期,并且正在返回一个原始指针以供访问并且不希望该指针影响引用计数,您可以通过调用 shared_ptr 返回原始指针。获取()。

    如果您使用 Swig 之类的工具为其他语言生成包装器,则返回智能指针可能会出现问题。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2012-10-24
      • 2012-11-27
      • 1970-01-01
      相关资源
      最近更新 更多