我应该使用std::shared指针来传递指针吗?

22
假设我有一个由 管理的对象。 我的代码的其他部分需要访问此对象。 传递指针的正确解决方案是什么? 我只应该通过std::unique_ptr::get传递原始指针,还是应该完全不使用std::unique_ptr而使用和传递std::shared_ptr
我对有一些偏好,因为该指针的所有者实际上负责清除。 如果我使用共享指针,则可能由于共享指针而使对象保持活动,即使它实际上应该被销毁。
编辑:不幸的是,我忘记提到指针不仅仅是函数调用的参数,而且将存储在其他对象中以建立对象网络结构。 我不喜欢使用共享指针,因为这样就不再清楚谁实际拥有对象了。

1
为什么不传递unique_ptr对象的引用? - Zaiborg
3
将引用传递给被引用的对象,是一个好主意。 - Cheers and hth. - Alf
@Zaiborg,对于unique_ptr的引用只有在您想要重新设置unique_ptr时才有用。如果您只想观察指针,则原始指针或引用就可以了。对于unique_ptr的引用并不更安全。请参见http://herbsutter.com/2013/06/05/gotw-91-solution-smart-pointer-parameters/。 - Chris Drew
你是否也需要重新设置它们?如果您从不更改对象网络引用的实例,请使用引用。如果您需要能够稍后将现有对象指向不同的实例,则需要某种指针。 - Useless
我不需要重新设置,但我不能保证在构造时设置引用,这将是一个类成员。 - Michael
显示剩余3条评论
5个回答

19
如果管理对象的所有权不会转移(因为它是一个unique_ptr,所以无法共享所有权),那么更正确的方法是将被调用函数中的逻辑与所有权概念分离。我们可以通过引用调用来实现这一点。
这个说法有些拐弯抹角,其实就是说:
假设:
std::unique_ptr<Thing> thing_ptr;

更改物品:

// declaration
void doSomethingWith(Thing& thing); 

// called like this
doSomethingWith(*thing_ptr);

使用该物品而不修改它。

// declaration
void doSomethingWith(const Thing& thing); 

// called like this
doSomethingWith(*thing_ptr);

只有在转移所有权时,您才需要在函数签名中提到unique_ptr

// declaration
void takeMyThing(std::unique_ptr<Thing> p);

// call site
takeMyThing(std::move(thing_ptr));

您永远不需要这样做:
void useMyThing(const std::unique_ptr<Thing>& p);

这样做的不好之处在于它会混淆useMyThing的逻辑和所有权的概念,因此缩小了重用的范围。
比如说:
useMyThing(const Thing& thing);

Thing x;
std::unique_ptr<Thing> thing_ptr = makeAThing();
useMyThing(x);
useMyThing(*thing_ptr);

更新:

注意到问题的更新 - 存储(非拥有)对此对象的引用。

一种方法是确实存储指针。然而,指针可能存在逻辑错误的可能性,因为它们可以合法地为空。指针的另一个问题是它们与std::算法和容器不兼容 - 需要自定义比较函数等。

有一种符合std::标准的方法来做到这一点 - std::reference_wrapper<>

所以,不是这样做:

std::vector<Thing*> my_thing_ptrs;

请执行以下操作:

std::vector<std::reference_wrapper<Thing>> my_thing_refs;

由于std::reference_wrapper<T>定义了一个T&运算符,因此您可以在任何需要T的表达式中使用reference_wrapped对象。

例如:

std::unique_ptr<Thing> t1 = make_thing();
std::unique_ptr<Thing> t2 = make_thing();
std::unique_ptr<Thing> t3 = make_thing();

std::vector<std::reference_wrapper<const Thing>> thing_cache;

store_thing(*t1);
store_thing(*t2);
store_thing(*t3);

int total = 0;
for(const auto& t : thing_cache) {
  total += value_of_thing(t);
}

其中:

void store_thing(const Thing& t) {
  thing_cache.push_back(std::cref(t));
}

int value_of_thing(const Thing& t) {
  return <some calculation on t>;
}

这是一个非常好的答案,非常感谢!不幸的是,我忘记指出一个重要的事实,现在我已经编辑到我的帖子中了。 - Michael
@Michael 我认为即使代码的其他部分希望保留该引用,这个建议仍然适用。只要你能确保在 unique_ptr 被销毁后,代码的其他部分不会再使用它。如果你不能确保这一点,那么 unique_ptr 就不合适。 - Chris Drew

6
通常情况下,您只需要将对象的引用或普通指针传递给希望观察该对象的代码的其他部分。
按引用传递:
void func(const Foo& foo);

std::unique_ptr<Foo> ptr;

// allocate ptr...

if(ptr)
    func(*ptr);

使用裸指针传递参数:

void func(const Foo* foo);

std::unique_ptr<Foo> ptr;

// allocate ptr...

func(ptr.get());

选择取决于需要传递空指针的情况。
您有责任通过设计确保观察者在unique_ptr被销毁后不再使用指针或引用。如果您无法保证,那么您必须使用shared_ptr而不是unique_ptr。观察者可以持有一个weak_ptr来表示他们没有所有权。
编辑:即使观察者希望保留指针或引用,这也没关系,但这确实使得难以确保在unique_ptr被销毁后不会再使用它。

我认为你在这里触及了“你的责任”问题。持有可能已经失效的对象的引用是不良通信的标志(数据结构应该被告知元素已被删除)。而且,weak_ptr可能是一种廉价的解决方法。 - Peter - Reinstate Monica

4

不要过于关注调用者的要求。函数需要传入什么?如果它只在调用期间使用对象,那么使用引用。

void func(const Foo& foo);

如果需要在对象的生命周期结束后仍然保留所有权,则传递一个 shared_ptr
void func(std::shared_ptr<Foo> foo);

1
最后一部分可能需要更清晰,因为问题是以“假设我有一个由std :: unique_ptr管理的对象”开始的。 - Cheers and hth. - Alf

3
自从问题更新后,我更倾向于以下几种方法:
  1. 使用Richard Hodges建议的std::reference_wrapper<>,只要您不需要NULL值选项。

    请注意,您的用例似乎需要一个默认构造函数,除非您有一些公共静态实例可用作默认值。

  2. 使用一些自定义的not_your_pointer<T>类型,具有所需的语义:可以默认构造、可复制和可赋值、可以从unique_ptr转换而来并且是非拥有型的。但是,编写新的智能指针类需要一些思考,因此只有在频繁使用时才值得这样做。

  3. 如果您需要优雅地处理悬空引用,请使用std::weak_ptr并将所有权转移到shared_ptr(也就是说,只存在一个shared_ptr:不存在所有权的歧义,对象的生命周期也不受影响)


在问题更新之前,我更喜欢:
  1. indicate that ownership is not transferred by passing a reference:

    void foo(T &obj); // no-one expects this to transfer ownership
    

    called as:

    foo(*ptr);
    

    you do lose the ability to pass nullptr though, if that matters.

  2. indicate that ownership is not transferred by explicitly prohibiting it:

    void foo(std::unique_ptr<T> const &p) {
        // can't move or reset p to take ownership
        // still get non-const access to T
    }
    

    this is clear, but does expose your memory management policy to the callee.

  3. pass a raw pointer and document that ownership should not be transferred. This is the weakest and most error prone. Don't do it.


const unique_ptr& 相对于裸指针有什么优势?正如你所说,缺点是它会暴露你的内存管理策略。 - Chris Drew
const-ref-to-unique_ptr 使代码明确地记录并保证了契约。调用者不太可能认为他们应该(或允许)拥有所有权,他们必须使用 const_cast 或其他一些技巧来获取它。与原始引用不同,您仍然可以传递 nullptr - Useless
@Useless:在 const unique_ptr& 规范中没有阻止通过 p.get() 获取底层原始指针,并对其进行任何想要的操作(将其存储在数据结构中是 OP 的意图)。而传递原始指针或引用的默认假设是不转移所有权(考虑所有那些使用 const char* 的函数),因此选项 2 真的没有为您带来太多好处。最重要的是,在存储指针的地方非常清楚地记录指针不是所有者。 - Marc van Leeuwen
我同意@Useless的观点。任何功能都可以被规避,但这并不意味着我们应该停止使用它们。加1。 - Cheers and hth. - Alf
我认为一个原始指针应该已经说明了所有权不应该被转移。如果我想要说明所有权应该被转移,我会传递一个unique_ptr - Chris Drew
显示剩余2条评论

0

这个问题已经被 Herb Sutter 在 GotW#91 - 智能指针参数 中讨论过了。它也涉及到性能方面,而其他答案虽然很好,但是更侧重于内存管理。

上面有人建议将智能指针作为const引用传递 - 但后来改变了主意。我们有一些代码滥用了这种方式——它使签名更加“智能”——即更加复杂而没有好处。最糟糕的是当一个初级开发者接手时——他不会像@Michael那样质疑,并从一个坏的例子中“学习”如何做。它可以工作,但是......

对我来说,智能指针参数传递问题足够严重,应该在解释什么是智能指针之后立即在任何书籍/文章/帖子中突出提到。

现在想知道像lint程序是否应该将通过const引用传递标记为风格要素。


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