shared_ptr和unique_ptr之间的转换

3

我现在处于这样的情况,我不确定应该使用哪种类型的智能指针,因为我不确定我的类的所有用例。我可以使用shared_ptr,但当我不一定需要共享所有权时,我不喜欢在我的代码中到处传递shared_ptr。

Herb Sutter的这篇文章说,当你不确定时,使用unique_ptr,并在必要时转换为shared_ptr。这正是我想做的,但我不清楚应该如何做,考虑以下示例:

class Example
{
    public:
        Example(): _ptr(std::make_unique<Node>()) {}

        std::unique_ptr<Node>& getPtr() 
        {
            return _ptr;
        }

    private:
        // I am unsure if I will eventually need shared ownership or not
        std::unique_ptr<Node> _ptr;
};

Example* example = new Example();


// Some function somewhere
void f()
{
    // I've decided I need shared ownership, converting
    std::shared_ptr<Node> ptr(std::move(example->getPtr()));

    // Oops, example is no longer valid...
}

如果有更好的方法来处理这种情况,我很乐意听取建议。

2
unique_ptr 和 shared_ptr 用于拥有所需的内存,而不仅仅是简单地使用它。你不应频繁传递 shared_ptr -- 因为与普通指针或引用相比,它们相对较昂贵。 - xaxxon
1
从上面的例子中可以看出,你的指针应该是shared_ptr,因为你想要(潜在地)共享所有权。对于工厂可能会存在疑问。 - Jarod42
6
getPtr是一个糟糕的接口。你不应该像那样传递unique_ptr。如果有人想要访问指针,他们应该获取一个Node* - Nicol Bolas
@jxh,我并不一定期望这样。假设一个程序决定需要共享所有权,则将其转换为共享指针,然后就可以进行共享了。 - navark
似乎你问的是类似于“std::vector::resize是否可以是const?”的问题,回答是用户可以使用相同的大小,因此不修改向量...但是用户可能会修改它。对于您的指针似乎也是同样的情况。 - Jarod42
显示剩余8条评论
3个回答

2
我认为您正在询问一种优化问题。您希望Example使用unique_ptr,因为它具有更简单和更高效的语义(引用您参考的文章)。但是,当需要时,您希望允许指针转换为shared_ptrExample应该提供一个接口来实现这个功能,并且需要在其用户调用该接口时从unique_ptr转换为shared_ptr。您可以使用状态模式来捕获实例是否处于unique_ptr模式或shared_ptr模式。
class Example
{
    struct StateUnique;
    struct StateShared;
    struct State {
        State (std::unique_ptr<State> &s) : _state(s) {}
        virtual ~State () = default;
        virtual Node & getPtr () = 0;
        virtual std::shared_ptr<Node> & getShared() = 0;
        std::unique_ptr<State> &_state;
    };
    struct StateUnique : State {
        StateUnique (std::unique_ptr<State> &s)
            : State(s), _ptr(std::make_unique<Node>()) {}
        Node & getPtr () { return *_ptr.get(); }
        std::shared_ptr<Node> & getShared() {
            _state = std::make_unique<StateShared>(*this);
            return _state->getShared();
        }
        std::unique_ptr<Node> _ptr;
    };
    struct StateShared : State {
        StateShared (StateUnique &u)
            : State(u._state), _ptr(std::move(u._ptr)) {}
        Node & getPtr () { return *_ptr.get(); }
        std::shared_ptr<Node> & getShared() { return _ptr; }
        std::shared_ptr<Node> _ptr;
    };
public:
    Example(): _state(std::make_unique<StateUnique>(_state)) {}
    Node & getNode() { return _state->getPtr(); }
    std::shared_ptr<Node> & getShared() { return _state->getShared(); }
private:
    std::unique_ptr<State> _state;
};

如果状态机看起来很吓人(因为它被过度设计了),那么你可以在Example中维护两个指针,以及需要测试哪个指针需要使用的方法。

class Example
{
public:
    Example(): _u_node(std::make_unique<Node>()) {}
    Node & getNode() { return _u_node ? *_u_node.get() : *_s_node.get(); }
    std::shared_ptr<Node> & getShared() {
        if (_u_node) _s_node = std::move(_u_node);
        return _s_node;
    }
private:
    std::unique_ptr<Node> _u_node;
    std::shared_ptr<Node> _s_node;
};

谢谢,这基本上就是我想要做的,但我想在我的代码中经常使用这种方法(不仅仅是这个类),而这种模式对于那些想要这样做的人来说太复杂了。然而,对于想要这样做的人来说,这是正确的答案。 - navark
@navark:我在答案底部添加了一个简化版本。 - jxh
为什么在类中既要有unique_ptr又要有shared_ptr?相较于普通指针,shared_ptr的唯一额外成本只存在于创建时,因此没有理由不直接创建它 - 不要通过来回转换使代码复杂化。如果它将来可能会是shared_ptr,那就直接创建shared_ptr。从所有权的角度来看,你永远不能像对待unique_ptr那样对待它,你必须始终假定它是使用shared_ptr版本的,从静态分析的角度来看,你只是为了几乎没有好处而增加了复杂的失败情况。 - xaxxon
更高效,是的。但在处理非大量指针创建时显著提高效率?不太可能。是否值得为应用程序增加这种复杂性?几乎肯定不会,除非您有支持它的配置文件数据。请记住,解引用唯一和共享指针与普通指针相同。 - xaxxon
然后答案说明了“公平”的含义,但重要的是要理解为什么它可能被认为是不好的。 - xaxxon
显示剩余6条评论

1
如果您有一个类,比如说UniqueResourceHolder,它可以被实现为下面这样:
class UniqueResourceHolder{
public:
  std::unique_ptr<Widget> ptr;
};

如果您稍后想要从UniqueResourceHolder获取所需资源并将其放入以下SharedResourceHolder中:

class SharedResourceHolder{
public:
  std::shared_ptr<Widget> ptr;
};

这样做的代码可能如下所示:

the code to do so may look like this:

{
  UniqueResourceHolder urh;
  SharedResourceHolder srh;
  //initialization of urh.ptr

  srh.ptr = std::shared_ptr<Widget>(urh.release());
}
UniqueResourceHolder将不再拥有它曾经拥有的Widget,因此任何试图使用urh获取小部件的尝试都将无效(除非您重新填充一个新的小部件),但是srh现在将拥有该小部件并愿意共享。无论如何,这就是我理解您提供的链接中回答问题的方式。我的个人建议是,用std::shared_ptr替换std::unique_ptr的出现也是一件微不足道的事情;您的程序遵循的任何业务逻辑以确保资源的唯一性仍然有效,但是从利用std::shared_ptr的共享特性的业务逻辑转变为独特思维需要一些时间和精力来重新设计。

1
你可以简单地执行 srh.ptr = std::move(urh.ptr); - Jarod42
这并不完全是我想要做的,但我认为你是正确的,当需要时用shared_ptr替换unique_ptr是微不足道且可能是最佳解决方案。 - navark

0

共享指针用于共享资源。如果您想要共享资源,我建议从一开始就将所有内容声明为共享,而不是通过特殊转换过程。

如果您想使用独占指针,则意味着您只能存在一个指向您分配的资源的指针。这并不限制您使用这些对象的方式。为什么不使用独占指针,并通过类实例和函数“移动”您的对象... 如果必须转换为您的指针,为什么不将其作为唯一的...

关于性能:据说,与原始指针相比,共享指针大约慢6倍。独占指针大约慢两倍。当然,我谈论的是访问。如果您真正有性能关键部分的代码,仍然可以获得这些类的原始指针并进行操作。甚至不必是指针,仍然有C++引用。

我认为您应该决定您的对象是“共享”还是“移动”,并继续采用这种范式,而不是一直进行转换和更改。

std::unique_ptr<Node>& getPtr() 
{
   return _ptr;
}

不,不是这样的。如果你从不同的地方调用它,那么你会传递多个引用。相反,使用"move"运算符(例如使用类似于type& operator=(type&& other)的声明)并传递你的唯一指针。

你确定这就是unique pointers的正确使用方式吗?因为我的理解是,“独特性”在于它的所有权,但是你可以随意传递原始指针(或引用unique_ptr)来引用它。 - navark
不确定,我们是在谈论同一个吗?“移动”不叫“更改”,但毕竟它仍然是指针。我想象它像一个对象/指针在程序中移动,也许是我的幻想。如果你传递原始指针,你就是在“共享”,而没有真正使用这些类的优势。我认为获取原始指针更多是一种选择,因为你还有其他方法来获取这个指针。毕竟,你正在使用模板类。 - Gerhard Stein
取决于可以进行多少优化:https://dev59.com/PGEh5IYBdhLWcg3wjD_n - Gerhard Stein
@xaxxon:好的,让我来修复一下。 - Gerhard Stein
1
@Gerstrong,该运算符重载的名称为“移动赋值运算符”——就像有一个移动构造函数一样——这是一个重要的区别,因为没有任何运算符会导致对象被移动。这很微妙,但C++中的移动语义对许多人来说很难理解,因此保持术语的一致性是有帮助的。http://en.cppreference.com/w/cpp/language/move_assignment - xaxxon
显示剩余2条评论

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