传递unique_ptr给函数

47

我正在尝试“现代化”一些现有代码。

  • 我有一个类,目前有一个成员变量“Device * device_”。
  • 在某些初始化代码中,它使用new创建实例,并在析构函数中使用“delete device_”。
  • 该类的成员函数调用许多需要Device *作为参数的其他函数。

这个方法很好,但为了“现代化”我的代码,我认为我应该将变量定义为“std :: unique_ptr< Device > device_”,并删除对delete的显式调用,这样可以使代码更安全,更好。

我的问题是 -

  • 然后我应该如何将device_变量传递给所有需要它作为参数的函数?

我可以在每个函数调用中调用.get来获取原始指针。但是这似乎很丑陋,并且浪费了使用unique_ptr的一些原因。

或者我可以更改每个函数,使其不再采用“Device *”类型的参数,而是采用“std :: unique_ptr& ”类型的参数。这使得函数原型有点难以理解,并难以阅读。

这种情况下的最佳实践是什么?我错过了其他选项吗?


3
为什么这种改变可以使代码更安全、更好?从你的描述来看,我认为恰恰相反。 - James Kanze
@James Kanze 我同意,unique_ptr 在这种情况下似乎没有什么用处。 - juanchopanza
2
你说得很对。我喜欢unique_ptr的“安全性”,因为你不必记得删除它,但在这种情况下似乎确实有一些代价。 - jcoder
1
unique_ptr 的真正安全在于,您不需要将指针用 try 块括起来以确保在出现异常情况下将其删除。这意味着,除非在构造函数中有特定的限制条件,否则它在具有析构函数的对象中并不能提供太多帮助。(无论您是在析构函数中放置 delete,还是使用 unique_ptr,在需要记住要做什么方面,差别不大。) - James Kanze
@DavidRodríguez-dribeas 没有简单的、一刀切的答案。从他的描述中,我看不出哪里需要用到 std::unique_ptr 来解决问题。这只会增加复杂性和混淆。在更复杂的情况下,这取决于具体情况。大多数情况下,更清晰的解决方案是委托给一个负责处理这个对象的基类。(std::vector 的 g++ 实现就是一个很好的例子。)在其他情况下,使用 unique_ptr 成员可能是有意义的,但根据我的经验,它们并不常见。 - James Kanze
显示剩余2条评论
5个回答

45
在现代C++风格中,有两个关键概念:
  • 所有权(Ownership)
  • 空指针(Nullity)
所有权(Ownership)表示持有某个对象/资源的所有者(在这种情况下,是一个Device类的实例)。各种std::unique_ptrboost::scoped_ptrstd::shared_ptr与所有权(Ownership)有关。
而空指针(Nullity)则非常简单:它仅表示给定对象是否可能为空指针,并且不关心其他任何事情,当然也不关心所有权(Ownership)!
你将你的类的实现向unique_ptr(通常)进行了移动是正确的,尽管如果你的目标是实现PIMPL,则可能需要具有深度复制语义的智能指针。
这清楚地表明了你的类是这块内存唯一的责任人,并且可以很好地处理各种内存泄漏的方式。
另一方面,大多数资源的用户可能并不关心它的所有权(Ownership)。 只要函数不保留对对象的引用(将其存储在映射之类的数据结构中),则重要的是对象的生命周期超过函数调用的时间。因此,选择如何传递参数取决于它的可能的空指针(Nullity):
  • 从不为空?传递引用
  • 有可能为空?传递指针,一个简单的裸指针或类似指针的类(例如带空指针判断的类)

嗯,引用……我可以让我的函数接受一个“Device& device”参数,然后在调用代码中使用*device_,因为它仍然可以与unique_ptr一起使用,而且我知道device_不可能为空(并且可以通过断言进行测试)。现在我需要考虑一下这是否优雅或者丑陋! - jcoder
2
@JohnB:嗯,我的偏见观点是,在编译时消除空值的可能性要安全得多 :) - Matthieu M.
6
完全同意Matthieu的观点,甚至我还要更进一步,为什么您想要动态分配一个对象,其生命周期受持有指针的对象的生命周期所绑定?为什么不声明它为具有自动存储的普通成员变量呢? - David Rodríguez - dribeas

14

这要看情况而定。如果一个函数必须接管unique_ptr的所有权,那么它的签名应该采用 unique_ptr<Device> 值的方式传递,而调用者应该使用std::move移动指针。如果所有权不是问题,那么我会保留原始指针的签名,并使用get() 传递指针 unique_ptr。如果涉及的函数不需要接管所有权,则此方法不会很丑陋。


是的,我不想拥有所有权,只想在函数中使用指向对象的指针。 - jcoder
2
如果所有权不是问题,您还可以传递一个const引用到unique_ptr。一些人使用指针-引用二分法来模拟Pascal的in-out形式参数语义(Google C++风格指南)。 - user755921

8
我会使用 std::unique_ptr const&。使用非const引用会给被调用函数重置指针的可能性。
我认为这是表达你所调用函数可以使用指针但不能使用其他内容的好方法。
因此,对我来说,这将使接口更易读。我知道我不必操纵传递给我的指针。

@JohnB:是的,const 防止被调用的函数调用例如 reset 在你的 unique_ptr 上。但只要内部指针不是 const,你仍然可以调用非 const 函数。 - mkaes
2
@mkaes,我认为在这种情况下传递一个常量引用到唯一指针比传递(可能是cv限定的)裸指针不够清晰。 - juanchopanza
2
@juanchopanza:我认为使用原始指针时,很难明确所有权。unique_ptr以一种好的方式解决了这个问题。 - mkaes
如果设计不清晰,那么使用unique_ptr也无济于事。如果你理解类的角色和责任,那么是否需要在析构函数中使用delete应该是清楚的。如果你不知道类的作用是什么,那么unique_ptr也无法告诉你。 - James Kanze
6
也许我错了,但在我看来,接受类型为“const std::unique_ptr<T>&”的参数始终是次优的。使用“const”表示您不会从调用者那里夺取对象(所有权)。但是由于“std :: unique_ptr”部分,您正在说您必须是所指对象的(直接)所有者。但是,鉴于我(被调用的函数)不会假定所有权,我无需知道谁拥有它。请注意,如果调用者“租用”指针(具有访问权限但没有所有权),则无法从中构建“unique_ptr”而不创建双重所有权。 - Marc van Leeuwen
显示剩余2条评论

2
在这种情况下,最好的做法可能是不使用std::unique_ptr,但这取决于具体情况。(通常来说,在类中不应该有超过一个指向动态分配对象的原始指针,但这也要看情况而定。)在这种情况下,你不想做的一件事就是传递std::unique_ptr(正如你已经注意到的那样,std::unique_ptr<> const&有点笨重和难以理解)。如果这是对象中唯一的动态分配指针,我会坚持使用原始指针,并在析构函数中使用delete。如果有多个这样的指针,我会考虑将它们各自放在单独的基类中(在那里它们仍然可以是原始指针)。

1

这对你来说可能不可行,但是将每个Device*的出现替换为const unique_ptr<Device>&是一个很好的开始。

显然,您不能复制unique_ptr,也不想移动它。通过引用unique_ptr来替换可以使现有函数体继续工作。

现在有一个陷阱,您必须通过const &传递,以防止被调用者执行unique_ptr.reset()unique_ptr().release()。请注意,这仍然传递了一个可修改的指向设备的指针。使用此解决方案,您没有轻松的方法来传递指向const Device的指针或引用。


由于 const unique_ptr<Device>& 这个写法有些笨重,我会将其 typedef 为 Device_t 或类似的名称。 - Goswin von Brederlow

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