如何管理原始指针和 unique_ptr 在不同 class 中的混合使用? (例外?)

How to manage mix use of raw pointers and unique_ptr in different class ? (Exceptions ?)

我有一个用 unique_ptr 存储的对象容器,为简单起见,假设我只有一个对象:

class Container { std::unique_ptr<A> ptrA; }

我也有 class 使用该对象。这些 class 在构造时采用指向这些对象的原始指针:

class B { A* a;
B(*A param) : a(param) }

它们是用 : B b = B(Container.ptrA.get() );

创建的

容器 class 的寿命应该比 class B 长。但是,如果我的 class 容器中出现问题或错误并且 unique_ptr 超出范围并被删除,我希望我的整个程序不会崩溃。

我的问题是关于管理这 1% 案例的设计,以便我的程序可以尝试重新加载数据并避免突然崩溃,您会使用异常吗?如果是这样,你会去哪里 try/catch ?

谢谢!

如果您希望您的程序不崩溃,请对两个指针使用 std::shared_ptr

那将是最简单的解决方案。

否则,您将需要引入某种机制,通过该机制 Container class 跟踪 B class 的实例数,即使用相同的指针,然后如果 Container 被销毁而某处仍有 B 的实例,则在析构函数中抛出异常。如果它的 unique_ptr 由于某些其他原因被炸毁,而不是析构函数被调用,同样的检查也适用于那里。

这是假设您希望抛出异常来处理这种边缘情况。不清楚你的意思 "can try to reload the data",但作为应用程序的设计者和实施者,你需要决定如何处理这种情况。没有其他人可以为您打电话,您比其他任何人都更了解您的整体应用程序。这里没有适用于每种情况下每种应用程序的最佳通用单一答案。

但是无论您决定什么,都应该采取适当的行动:抛出异常;或者创建对象的新实例,将其填充到 unique_ptr 中,然后以某种方式更新您正在跟踪的所有 B classes 中的所有本机指针;那将是你的电话。最好的方法是主观判断。该部分没有 objective 答案。

现在,回到技术方面,跟踪 B class 的实例数量就像在容器中保留一个计数器一样简单,并且 B 的构造函数和析构函数相应地更新它。或者让 Container 保留一个容器,其中包含指向 B 的所有实例的指针。无论哪种情况,都不要忘记在复制构造函数和赋值运算符中做正确的事情。

但我认为在两个 class 中都使用 std::shared_ptr 更容易,不用担心这些。即使做这种 class 簿记不是火箭科学,但只要让 std::shared_ptr 为您做这件事,为什么还要费心呢。

当您使用 std::unique_ptr 时,您正在做出设计决定:Container 拥有指针。试图解决这个事实只会让你的生活更加艰难。

但事实上你说 ContainerB 长寿。你为什么不强制执行它,而不是过度防御可能会以其他几种方式破坏你的程序的错误?

我会说不要使用 shared_ptr 来隐藏错误。如果您的 unique_ptr 设计为比 原始指针 寿命更长,那么我希望程序在出现错误时崩溃。那我有事要解决。如果错误未被检测到,情况会更糟,因为它们对您是隐藏的。请记住,崩溃会给您一个 故障点 来进行调查。但是,如果错误未被发现,您可能无法找到导致错误的原因。

哲学上:这不是一个好主意,至少在 C++ 中是这样。

The Container class is supposed to outlive the class B. However I'd like my whole program not to crash in the case there is an issue or a bug ...

听起来你想要一种“更安全”的语言。

你可以编写“应该”工作但对 ownership/lifetime 错误具有鲁棒性的代码的想法是......对于具有显式生命周期管理的低级语言(如 C++)的目标来说,这是非常令人厌恶的,我想想。

如果您真的想编写一个 不会崩溃的程序 ,请使用具有为您管理内存和生命周期的运行时的语言,即垃圾-收集的语言,如 Java 或 Python。可以这么说,这些语言旨在“保护您免受自己的伤害”。从理论上讲,它们通过为您管理内存 来防止您遇到您描述的各种错误。

但是使用低级语言的部分 要点 是利用显式内存管理。与使用托管语言编写的软件相比,使用 C++,您可以(理论上)编写运行速度更快、内存占用更小并更快释放系统资源(例如文件句柄)的软件。

C++ 中的正确 方法是您已经在使用的方法。

明确让您的容器class拥有底层对象并使用unique_ptr表示此所有权完全正确 在现代 C++ 中,如果您的系统经过精心设计,没有理由认为这种方法不起作用。

不过,关键问题是 您如何保证 您的 container class 会一直存活并在整个过程中保持您拥有的对象存活“用户”对象的生命周期(在本例中为 class B 个实例)?您的问题没有提供足够的关于您的体系结构的详细信息来让我们回答这个问题,因为不同的设计将需要不同的方法。但是,如果您可以解释您的系统(理论上)如何提供这种保证,那么您可能就走在了正确的轨道上。

如果您仍有顾虑,一些方法可以解决这些问题。

有很多理由对 C++ 中的生命周期管理有合理的担忧;一个主要的问题是,如果您继承了一个遗留代码库并且您不确定它是否适当地管理了生命周期。

即使使用 unique_ptr 等现代 C++ 功能,也可能会发生这种情况 。我正在从事一个去年才开始的项目,我们一直在使用 C++14 功能,包括 <memory>,从一开始,我 肯定 将其视为“遗留”项目:

  • 多名参与该项目的工程师现已离职; 60,000 多行是“无主”的,因为它们的原作者不再参与该项目
  • 单元测试很少
  • 偶尔会出现段错误:D

请注意,生命周期管理中的错误可能不会导致崩溃;如果是这样,那将是 fantastic,因为正如 Galik 在他们的回答中所说,这会让您无法进行调查。不幸的是,没有办法保证取消引用陈旧的指针会导致崩溃,因为这是(显然)未定义的行为。因此你的程序可以保持 运行 并默默地做一些完全灾难性的事情。

信号捕捉

但是,崩溃——特别是段错误——是您描述的错误最可能的结果,因为段错误是您可以(某种程度上)通过编程解决的问题。

就您可以实施的故障处理行为而言,这是最薄弱的方法:只需捕获 SEGFAULT 信号并尝试从中恢复。信号捕获函数有一些非常严重的局限性,一般来说,如果你的生命周期管理搞砸了,可能无法合理保证哪些内存是你可以信任的,哪些内存是你不能信任的,所以你的程序无论如何都可能失败当你捕捉到信号时你会做什么。

不是“修复”损坏软件的好方法;然而,这是为不可恢复的错误提供干净的退出路径的一种非常合理的方法(例如,它将允许您模拟 classic“内存错误”错误消息)。此外,如果您只想重新启动整个应用程序并希望获得最好的结果,您可以使用信号捕捉器来实现这一点,尽管更好的方法可能是实现第二个“观察者”应用程序,它会在以下时间重新启动您的软件它崩溃了。

std::shared_ptr

Joachim Pileborg 是正确的,std::shared_ptr 在这种情况下可以工作,但是 (1) shared_ptr 与原始指针相比有一些开销(如果你关心的话)和 (2) 它需要更改整个生命周期管理方案。

此外,正如 Galik 在评论中指出的那样,当存在生命周期管理错误时,拥有对象的生命周期将 延长 ;如果您的 B class 实例中的任何 shared_ptr 仍处于活动状态,则在从容器中删除 shared_ptr 后,该对象仍将存在。

std::weak_ptr

您最好的选择可能是 weak_ptr。这 需要更改您的生命周期管理方案以使用 shared_ptr,但它的好处是不会仅仅因为旧对象存在 shared_ptr 在生命周期管理容器之外的某处

不过,并不是所有的低级语言都这么无情。

我有点偏见,因为我喜欢 Rust 语言背后的哲学,所以这有点塞子。 Rust 在编译时强制执行正确的生命周期管理 Rust 与 C++ 一样低级,因为它可以完全控制内存管理、内存访问等,但它是一种“现代”高级语言,因为它更接近于重新设计的 C++ 版本,而不是 C。

但我们的目的的关键点是 限制 Rust 在它认为的“所有权”或生命周期管理错误方面给你带来更好的 保证 程序正确性比 任何 可能的 C 或 C++ 程序静态分析都可以。