在这种情况下,我应该使用unique_ptr还是shared_ptr?

11

在我的QT应用程序的主窗口中,我使用一个 std::shared_ptr 来持有指向我的网络服务实例的指针,该服务管理所有与多个客户端的连接。现在,我必须将此指针传递给多个子窗口,以便它们可以与客户端通信。

我应该在主窗口和子窗口中使用一个 std::shared_ptr 成员变量,并在创建子窗口时传递其副本,还是使用一个 std::unique_ptr 并向子窗口传递原始指针,因为主窗口无论如何都将比子窗口存在时间更久?

非常感谢!


1
我认为std::shared_ptr会是一个更好的选择,因为你正在谈论子窗口。 - michaeltang
4个回答

8
主要的实际区别在于当主窗口销毁时,子窗口仍然存在并且正在使用网络服务时会发生什么:
- 如果你使用unique_ptr并传递原始指针,则行为未定义。 - 如果你使用shared_ptr,则网络服务将一直保留到所有子窗口都被销毁。
如果这个条件从设计上来说是不可能的,那么未定义的行为本质上不是一个问题。如果由于错误导致该条件仍然发生,如果网络服务与主窗口一起销毁,它可能会帮助你检测错误。使用unique_ptr表达了主窗口是唯一拥有网络服务的东西,其他人仅按照主窗口的指示使用它们。另一方面,如果稍后更改设计,并希望使条件合法,或者想以不同的方式使用子窗口,意味着没有单个网络服务对象,它们全部使用并超过了它们的寿命,那么从一开始就使用shared_ptr会更容易。使用shared_ptr表示所有窗口共享对网络服务的所有权。我认为通常不能一般地说你应该尝试使你的代码在"不可能的情况"下继续工作。在这种情况下,这样做非常便宜(当然,相对于创建一个子窗口,shared_ptr的复制更昂贵,但与创建子窗口相比便宜,代码的结构也是一样的)。在某些情况下,有灵活性使"不可能的情况"发生可能是有用的,例如在单元测试子窗口代码时。因此,可能使用shared_ptr以获得灵活性。如果由于其他原因强制执行生命周期约束是一个大问题,那么你可能不想要任何灵活性,因此避免编写只会隐藏错误的代码。

6
实际上,您还没有探索第三种选择。
传递shared_ptr允许您拥有多个所有者,但在您的情况下似乎这是不必要的;
使用unique_ptr并传递原始指针强制执行单个所有者,但在出现错误的情况下,您将面临未定义的行为(崩溃)。
第三种选择是结合使用weak_ptr:
主窗口使用shared_ptr拥有设备;
子窗口使用weak_ptr;
当子窗口需要使用设备进行通信时,它们将在weak_ptr上调用lock()以暂时获得shared_ptr。然后,您可以断言或抛出异常,如果shared_ptr为空(这是一个错误),否则使用它与设备通信,然后将其释放。
编辑:正如Steve Jessop所指出的,另一个断言是有用的(也是可实现的):确保当主窗口销毁shared_ptr时,它是最后一个所有者,并且设备已被释放(以防止意外泄漏所有权)。
天真的方法是在销毁之前断言它是唯一的,但不幸的是,它会遇到竞态条件;在调用unique和实际销毁之间,对weak_ptr::lock的调用可能会创建新的所有者。
但是,这可以很简单地完成:
1.从shared_ptr创建一个名为monitor的新weak_ptr;
2.重置shared_ptr;
3.调用monitor.lock(),如果返回非空的shared_ptr,则有一个所有者在外面。

不可能原子地测试shared_ptr的引用计数并销毁它,对吗?我问这个问题是因为有一个潜在有用的第二个断言可以做到,即当子窗口销毁其shared_ptr时,它不是最后一个所有者。当然,您可以只断言不是unique(),然后销毁。 - Steve Jessop
@SteveJessop:确实很有趣,我会在答案中详细阐述这个论点。 - Matthieu M.

4
关于使用 std::shared_ptr 还是 std::unique_ptr 的问题,主要取决于所有权。对象是否一次只有一个所有者?还是对象将有多个所有者?
在您的情况下,似乎您需要多个所有者,因此使用 std::shared_ptr 是正确的选择。 不要 使用 std::unique_ptr,然后传递原始封装指针。当您忘记它并且 std::unique_ptr 对象超出范围时,仍然有人可以访问原始指针,这会给您带来麻烦。

2
我认为,网络服务应该是程序的一个对象,其生命周期应该与程序的生命周期一致。在这种情况下,不能确定是否需要使用任何类型的智能指针;最明显的解决方案是在main中创建局部变量。不管怎样,首先要问自己的是该对象应该具有什么寿命期。如果寿命期对应于main (或任何其他函数)的作用域,则应选择局部变量。如果该对象应该与主窗口的生命周期相对应,则主窗口的成员变量应该是适当的解决方案。所有这些都取决于应用程序如何指定对象的生命周期(这是一个设计问题,不能通过低级编程技术来解决)。
唯一可能需要使用指针的情况是,如果类型是多态的,并且实际类型在运行时未知(例如,因为它由配置文件中的条目确定)。在这种情况下,您必须自己管理生命周期,但如果它确实对应于作用域,则std::unique_ptr可能是一个好的解决方案,因为如果没有移动,则它精确地模拟了作用域变量的生命周期。(我对std::shared_ptr非常怀疑;寿命应该是确定性的,而不依赖于是否有人持有指向它的指针。我也可以想象出网络服务器将保存其客户端的指针的情况,存在循环依赖的风险。)

谢谢您指出这一点。但是,我如何从QT窗口访问main()中的局部变量呢?我曾经考虑过将服务设置为“单例”,但是我不想这样做,尽管我不记得原因了。 - user66875

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