ChatGPT解决这个技术问题 Extra ChatGPT

为什么 C++ 没有垃圾收集器?

我之所以问这个问题,首先是因为垃圾收集的优点。我问这个的主要原因是我知道 Bjarne Stroustrup 说过 C++ 将在某个时间点有一个垃圾收集器。

说了这么多,怎么还没加呢?已经有一些用于 C++ 的垃圾收集器。这只是那些“说起来容易做起来难”的事情之一吗?还是有其他原因没有添加(并且不会在 C++11 中添加)?

交叉链接:

C++ 的垃圾收集器

澄清一下,我理解 C++ 在首次创建时没有垃圾收集器的原因。我想知道为什么无法添加收集器。

这是仇恨者总是提起的关于 C++ 的十大神话之一。垃圾收集不是“内置”的,但有几种简单的方法可以做到 C++。发表评论是因为其他人的回答比我在下面的回答更好:)
但这就是不是内置的全部意义,你必须自己做。真实性从高到低:内置、库、自制。我自己使用 C++,绝对不讨厌它,因为它是世界上最好的语言。但是动态内存管理很痛苦。
@Davr - 我不是 C++ 的仇恨者,我什至也不想争辩说 C++ 需要一个垃圾收集器。我之所以问,是因为我知道 Bjarne Stroustrup 已经说过它将被添加,并且只是好奇不实施它的原因是什么。
这篇文章 The Boehm Collector for C and C++ from Dr. Dobbs 介绍了一种可用于 C 和 C++ 的开源垃圾收集器。它讨论了将垃圾收集器与 C++ 析构函数以及 C 标准库一起使用时出现的一些问题。
@rogerdpack:但是现在它还没有那么有用(请参阅我的答案...),因此实现不太可能投资拥有一个。

1
10 revs, 3 users 90%

可以添加隐式垃圾收集,但它没有成功。可能不仅是因为实施的复杂性,还因为人们无法足够快地达成普遍共识。

Bjarne Stroustrup 本人的一句话:

我曾希望可以选择启用的垃圾收集器成为 C++0x 的一部分,但是有足够的技术问题我必须解决这个收集器如何与语言的其余部分集成的详细规范,如果提供。与基本上所有 C++0x 功能的情况一样,存在实验性实现。

对主题 here 进行了很好的讨论。

总体概述:

C++ 非常强大,几乎可以让你做任何事情。出于这个原因,它不会自动将许多可能影响性能的东西推给您。垃圾收集可以很容易地用智能指针(用引用计数包装指针的对象,当引用计数达到 0 时自动删除自己)实现。

C++ 在构建时考虑了没有垃圾收集的竞争对手。与 C 和其他语言相比,效率是 C++ 必须抵御批评的主要问题。

有两种类型的垃圾收集...

显式垃圾收集:

C++0x 通过 shared_ptr 创建的指针进行垃圾收集

如果你想要它,你可以使用它,如果你不想要它,你不会被迫使用它。

对于 C++0x 之前的版本, boost:shared_ptr 存在并用于相同目的。

隐式垃圾回收:

虽然它没有透明的垃圾收集。不过,它将成为未来 C++ 规范的焦点。

为什么 Tr1 没有隐式垃圾回收?

C++0x 的 tr1 应该有很多东西,Bjarne Stroustrup 在之前的采访中表示 tr1 没有他想要的那么多。


如果 C++ 强制对我进行垃圾收集,我会成为仇恨者!为什么人们不能使用 smart_ptr's?您将如何使用垃圾收集器进行低级 Unix 风格的分叉?其他事情会受到影响,例如线程。 Python 有它的全局解释器锁,主要是因为它的垃圾收集(参见 Cython)。把它放在 C/C++ 之外,谢谢。
@unixman83:引用计数垃圾回收(即std::shared_ptr)的主要问题是循环引用,这会导致内存泄漏。因此,您必须小心使用 std::weak_ptr 来打破循环,这很混乱。 Mark and sweep 式 GC 没有这个问题。线程/分叉和垃圾收集之间没有内在的不兼容性。 Java 和 C# 都具有高性能抢占式多线程和垃圾收集器。实时应用程序和垃圾收集器存在一些问题,因为大多数垃圾收集器必须停止世界运行。
“引用计数垃圾收集(即 std::shared_ptr)的主要问题是循环引用”和具有讽刺意味的糟糕性能,因为更好的性能通常是使用 C++ 的理由... flyingfrogblog.blogspot.co.uk/2011/01/…
“你将如何进行低级 Unix 风格的分叉”。像 OCaml 这样的 GC 语言已经这样做了大约 20 年或更长时间。
“Python 有它的全局解释器锁主要是因为它的垃圾收集”。稻草人论证。 Java 和 .NET 都有 GC,但都没有全局锁。
M
MaksymB

在这里加入辩论。

垃圾收集存在已知问题,理解它们有助于理解为什么 C++ 中没有。

1.性能?

第一个抱怨通常是关于性能的,但大多数人并没有真正意识到他们在说什么。如 Martin Beckett 所示,问题可能不是性能本身,而是性能的可预测性。

目前有 2 个 GC 系列被广泛部署:

Mark-And-Sweep 类型

引用计数类型

Mark And Sweep 速度更快(对整体性能的影响较小),但它存在“冻结世界”综合症:即当 GC 启动时,其他一切都停止,直到 GC 完成清理。如果您希望构建一个可以在几毫秒内回答的服务器......有些事务将无法达到您的期望:)

Reference Counting 的问题不同:引用计数增加了开销,尤其是在多线程环境中,因为您需要原子计数。此外,还有参考周期的问题,因此您需要一个聪明的算法来检测这些周期并消除它们(通常也通过“冻结世界”来实现,尽管频率较低)。一般来说,到今天为止,这种(尽管通常反应更快,或者更确切地说,冻结的频率更低)比 Mark And Sweep 慢。

我看到 Eiffel 实现者的一篇论文试图实现一个 Reference Counting 垃圾收集器,它在没有“冻结世界”方面的情况下具有与 Mark And Sweep 类似的全局性能。它需要一个单独的 GC 线程(典型)。算法有点吓人(最后),但这篇论文很好地一次介绍了一个概念,并展示了算法从“简单”版本到成熟版本的演变。推荐阅读,如果我能把我的手重新放在 PDF 文件上……

2. 资源获取即初始化(RAII)

C++ 中的一个常见习惯用法是将资源的所有权包装在一个对象中以确保它们被正确释放。它主要用于内存,因为我们没有垃圾收集,但它在许多其他情况下也很有用:

锁(多线程,文件句柄,...)

连接(到数据库,另一台服务器,...)

这个想法是正确控制对象的生命周期:

只要你需要它就应该活着

完成后应该将其杀死

GC 的问题在于,如果它对前者有所帮助并最终保证以后……这个“最终”可能还不够。如果你释放一个锁,你真的很想现在就释放它,这样它就不会阻塞任何进一步的调用!

带有 GC 的语言有两种解决方法:

当堆栈分配足够时不要使用 GC:它通常用于性能问题,但在我们的例子中它确实有帮助,因为范围定义了生命周期

using 构造...但它是显式(弱)RAII,而在 C++ 中 RAII 是隐式的,因此用户不能无意中犯错误(通过省略 using 关键字)

3. 智能指针

智能指针通常显示为处理 C++ 中内存的灵丹妙药。我经常听到:毕竟我们不需要 GC,因为我们有智能指针。

大错特错了。

智能指针确实有帮助:auto_ptrunique_ptr 使用 RAII 概念,确实非常有用。它们非常简单,您可以很容易地自己编写它们。

但是,当需要共享所有权时,它会变得更加困难:您可能会在多个线程之间共享,并且在处理计数方面存在一些微妙的问题。因此,自然而然地走向shared_ptr

这很棒,毕竟这就是 Boost 的用途,但它不是灵丹妙药。实际上,shared_ptr 的主要问题是它模拟了 Reference Counting 实现的 GC,但您需要自己实现循环检测... Urg

当然有这个 weak_ptr 东西,但不幸的是,尽管使用了 shared_ptr,但由于这些循环,我已经看到内存泄漏......而且当您处于多线程环境中时,很难检测到!

4. 解决办法是什么?

没有灵丹妙药,但一如既往,绝对可行。在没有 GC 的情况下,需要明确所有权:

如果可能的话,更喜欢在某个特定时间拥有一个所有者

如果没有,请确保您的类图没有任何与所有权有关的循环,并通过弱点的微妙应用来打破它们

因此,确实,拥有 GC 会很棒……但这不是一个小问题。与此同时,我们只需要卷起袖子。


我希望我能接受两个答案!这太棒了。需要指出的一点是,在性能方面,在单独的线程中运行的 GC 实际上很常见(它用于 Java 和 .Net)。当然,这在嵌入式系统中可能是不可接受的。
只有两种?抄袭收藏家怎么样?世代收藏家?各种并发收集器(包括贝克的硬实时跑步机)?各种混合收藏家?伙计,这个领域行业的纯粹无知有时让我感到惊讶。
我说只有两种类型吗?我说有 2 个被广泛部署。据我所知,Python、Java 和 C# 现在都使用 Mark 和 Sweep 算法(Java 曾经有一个引用计数算法)。更准确地说,在我看来,C# 使用 Generational GC 处理次要循环,使用 Mark And Sweep 处理主要循环并使用 Copying 来消除内存碎片;尽管我认为算法的核心是 Mark And Sweep。你知道任何使用另一种技术的主流语言吗?我总是乐于学习。
您刚刚命名了一种使用三种的主流语言。
主要区别在于,分代和增量 GC 不需要停止世界工作,您可以通过在访问 GC 指针时偶尔执行树遍历的迭代(因素可以由新节点的数量以及需要收集的基本预测来确定)。您可以通过在代码中包含有关节点创建/修改发生的位置的数据来进一步使用 GC,这可以让您改进预测,并免费获得 Escape Analysis。
M
Martin Beckett

哪种类型?是否应该针对嵌入式洗衣机控制器、手机、工作站或超级计算机进行优化?它应该优先考虑gui响应还是服务器加载?它应该使用大量内存还是大量 CPU?

C/c++ 用于太多不同的情况。我怀疑像 boost smart pointers 这样的东西对于大多数用户来说就足够了

编辑 - 自动垃圾收集器不是性能问题(您总是可以购买更多服务器),而是可预测性能的问题。不知道 GC 何时启动就像雇用一个嗜睡症的航空公司飞行员一样,大多数时候他们都很棒——但当你真的需要响应时!


我肯定明白你的意思,但我不得不问:Java 不是用在同样多的应用程序中吗?
不,Java 不适合高性能应用程序,原因很简单,它没有 C++ 那样的性能保证。所以你会在手机中找到它,但你不会在手机开关或超级计算机中找到它。
你总是可以购买更多的服务器,但你不能总是为已经在客户口袋里的手机购买更多的 CPU!
Java 在 CPU 效率方面做了很多性能追赶。真正棘手的问题是内存使用,Java 本质上比 C++ 内存效率低。效率低下是由于它是垃圾收集的事实。垃圾收集不能既快速又节省内存,如果您研究一下 GC 算法的工作速度,这一事实就会变得显而易见。
@Zathrus java 可以在优化 jit 的吞吐量 b/c 上获胜,但不是延迟(boo 实时),当然也不是内存占用。
G
Greg Rogers

C++ 没有内置垃圾收集的最大原因之一是让垃圾收集与析构函数配合使用非常非常困难。据我所知,没有人真正知道如何完全解决它。有很多问题需要处理:

对象的确定性生命周期(引用计数为您提供了这一点,但 GC 没有。虽然它可能没什么大不了的)。

如果在对象被垃圾回收时抛出析构函数会发生什么?大多数语言都会忽略此异常,因为实际上没有可以将其传输到的 catch 块,但这可能不是 C++ 可接受的解决方案。

如何启用/禁用它?自然,这可能是一个编译时间决定,但是为 GC 编写的代码与为 NOT GC 编写的代码将非常不同并且可能不兼容。你如何调和这个?

这些只是面临的一些问题。


GC 和析构函数是一个已解决的问题,这是 Bjarne 的一个很好的回避。析构函数不会在 GC 期间运行,因为那不是 GC 的重点。 C++ 中的 GC 的存在是为了创建无限内存的概念,而不是无限的其他资源。
如果析构函数不运行,那将完全改变语言的语义。我想至少你需要一个新的关键字“gcnew”或其他东西,这样你才能明确地允许这个对象被 GC'ed(因此你不应该用它来包装除了内存之外的资源)。
这是一个虚假的论点。由于 C++ 具有显式内存管理,因此您需要确定何时必须释放每个对象。使用 GC,情况不会更糟;相反,问题归结为确定何时释放某些对象,即那些在删除时需要特殊考虑的对象。使用 Java 和 C# 进行编程的经验表明,绝大多数对象不需要特殊考虑,可以安全地留给 GC。事实证明,C++ 中析构函数的主要功能之一是释放子对象,GC 会自动为您处理这些子对象。
@NateC-K:在 GC 与非 GC 中改进的一件事(也许是最大的事情)是一个可靠的 GC 系统能够保证只要引用存在,每个引用都将继续指向同一个对象。对一个对象调用 Dispose 可能会使其不可用,但在该对象处于活动状态时指向该对象的引用将在该对象死亡后继续这样做。相比之下,在非 GC 系统中,可以在引用存在时删除对象,并且如果使用其中一个引用可能造成的破坏很少有任何限制。
J
Jerry Coffin

虽然这是一个老问题,但我仍然没有看到有人解决过一个问题:几乎不可能指定垃圾收集。

特别是,C++ 标准非常小心地根据外部可观察行为来指定语言,而不是实现如何实现该行为。然而,在垃圾收集的情况下,几乎没有外部可观察的行为。

垃圾收集的一般思想是它应该做出合理的尝试来确保内存分配成功。不幸的是,基本上不可能保证任何内存分配都会成功,即使您确实有一个垃圾收集器在运行。在任何情况下这在某种程度上都是正确的,但在 C++ 的情况下尤其如此,因为(可能)不可能使用复制收集器(或任何类似的东西)在收集周期中移动内存中的对象。

如果你不能移动对象,你就不能创建一个单一的、连续的内存空间来进行分配——这意味着你的堆(或自由存储,或者你喜欢叫它的任何东西)可以并且可能会,随着时间的推移变得支离破碎。反过来,这会阻止分配成功,即使可用内存多于请求的内存量也是如此。

虽然可能会提出一些保证(本质上),如果您重复完全相同的分配模式,并且第一次成功,它将在后续迭代中继续成功,前提是分配的内存在迭代之间变得无法访问。这是一个如此薄弱的保证,它基本上是无用的,但我看不到任何加强它的合理希望。

即便如此,它还是比为 C++ 提出的要强。 previous proposal [警告:PDF](已删除)根本不保证任何事情。在 28 页的提案中,阻碍外部可观察行为的是一个单一的(非规范性的)注释,上面写着:

[注意:对于垃圾收集程序,高质量的托管实现应该尝试最大化它回收的不可达内存量。 ——尾注]

至少对我来说,这引发了一个关于投资回报的严肃问题。我们要破坏现有的代码(没有人确切知道多少,但肯定不少),对实现提出新的要求和对代码的新限制,而我们得到的回报很可能什么都没有?

即使充其量,我们得到的是基于 testing with Java 的程序,可能需要大约六倍的内存才能以与现在相同的速度运行。更糟糕的是,垃圾收集从一开始就是 Java 的一部分——C++ 对垃圾收集器施加了足够多的限制,以至于几乎可以肯定它的成本/收益比会更更糟糕(即使我们超出了保证提案并假设会有一些好处)。

我会以数学方式总结情况:这是一个复杂的情况。任何数学家都知道,复数有两部分:实数和虚数。在我看来,我们这里的成本是真实的,但收益(至少大部分)是想象的。


我会假设即使有人指定为了正确操作必须删除所有对象,并且只有已删除的对象才有资格被收集,编译器对引用跟踪垃圾收集的支持仍然有用,因为这样一种语言可以确保使用已删除的指针(引用)将保证捕获,而不是导致未定义的行为。
即使在 Java 中,GC 也没有真正指定做任何有用的 AFAIK。它可能会为您调用 free(我的意思是 free 类似于 C 语言)。但是 Java 从不保证调用终结器或类似的东西。事实上,C++ 在执行数据库写入、刷新文件句柄等方面比 Java 做得更多。 Java 号称有“GC”,但 Java 开发者必须时刻小心地调用 close() 并且必须非常注意资源管理,注意不要太早或太晚调用 close()。 C++ 让我们摆脱了这种束缚。 ...(继续)
..我刚才的评论并不是要批评Java。我只是观察到“垃圾收集”这个词是一个非常奇怪的词——它的含义比人们想象的要少得多,因此如果不弄清楚它的含义就很难讨论它。
@AaronMcDaid 确实,GC 对非内存资源没有帮助。幸运的是,与内存相比,此类资源很少被分配。此外,超过 90% 的它们可以在分配它们的方法中释放,因此 try (Whatever w=...) {...} 解决了它(当你忘记时会收到警告)。其余的对于 RAII 也有问题。 “一直”调用 close() 意味着可能每数万行调用一次,所以这还不错,而几乎每条 Java 行都分配了内存。
A
Andriy Makukha

如果你想要自动垃圾收集,有很好的 C++ 商业和公共领域的垃圾收集器。对于适合垃圾收集的应用程序,C++ 是一种出色的垃圾收集语言,其性能可与其他垃圾收集语言相媲美。有关 C++ 中自动垃圾收集的讨论,请参阅 The C++ Programming Language (4rd Edition)。另见,汉斯-J。 Boehm 的 C 和 C++ 垃圾收集站点(存档)。此外,C++ 支持允许内存管理安全且隐式的编程技术,而无需垃圾收集器。我认为垃圾收集是最后的选择,也是处理资源管理的一种不完美的方式。这并不意味着它永远不会有用,只是在许多情况下都有更好的方法。

来源:http://www.stroustrup.com/bs_faq.html#garbage-collection

至于为什么它没有内置它,如果我没记错的话,它是在 GC 出现之前发明的,而且我不相信该语言可能有 GC 有几个原因(IE 与 C 的向后兼容性)

希望这可以帮助。


“具有与其他垃圾收集语言相媲美的性能”。引文?
我的链接坏了。我在 5 年前写了这个答案。
好的,我希望对这些声明进行一些独立的验证,即不是由 Stroustrup 或 Boehm 进行的。 :-)
A
Aaron McDaid

Stroustrup 在 2013 年 Going Native 会议上对此做了一些很好的评论。

只需在 this video 中跳到大约 25 分 50 秒。 (我建议实际上观看整个视频,但这会跳到有关垃圾收集的内容。)

当您拥有一种非常出色的语言,可以轻松(并且安全、可预测、易于阅读和易于教授)以直接方式处理对象和值时,避免(显式)使用堆,那么你甚至不需要垃圾收集。

对于现代 C++,以及我们在 C++11 中拥有的东西,垃圾收集不再是可取的,除非在有限的情况下。事实上,即使在主要的 C++ 编译器之一中内置了一个好的垃圾收集器,我认为它也不会经常使用。避免 GC 会更容易,而不是更难。

他展示了这个例子:

void f(int n, int x) {
    Gadget *p = new Gadget{n};
    if(x<100) throw SomeException{};
    if(x<200) return;
    delete p;
}

这在 C++ 中是不安全的。但它在 Java 中也是不安全的!在 C++ 中,如果函数提前返回,则永远不会调用 delete。但是如果你有完整的垃圾收集,比如在 Java 中,你只会得到一个建议,即对象将在“将来的某个时间点”被破坏(更新:这更糟糕。Java 确实承诺永远调用终结器——它可能永远不会被调用)。如果 Gadget 拥有打开的文件句柄、与数据库的连接或您为稍后写入数据库而缓冲的数据,这还不够好。我们希望小工具在完成后立即销毁,以便尽快释放这些资源。您不希望您的数据库服务器为成千上万不再需要的数据库连接而苦苦挣扎——它不知道您的程序已经完成工作。

那么解决方案是什么?有几种方法。您将对绝大多数对象使用的显而易见的方法是:

void f(int n, int x) {
    Gadget p = {n};  // Just leave it on the stack (where it belongs!)
    if(x<100) throw SomeException{};
    if(x<200) return;
}

这需要更少的字符来键入。它没有 new 妨碍。它不需要您键入 Gadget 两次。对象在函数结束时被销毁。如果这是您想要的,这是非常直观的。 Gadget 的行为与 intdouble 相同。可预测、易读、易教。一切都是“价值”。有时价值很大,但价值更容易教授,因为您没有通过指针(或引用)获得的这种“远距离行动”的东西。

您创建的大多数对象仅在创建它们的函数中使用,并且可能作为输入传递给子函数。程序员在返回对象时不必考虑“内存管理”,或者在软件的广泛分离的部分之间共享对象。

范围和生命周期很重要。大多数情况下,如果生命周期与范围相同,则更容易。它更容易理解,也更容易教授。当您想要一个不同的生命周期时,阅读您正在执行此操作的代码应该很明显,例如使用 shared_ptr。 (或按值返回(大)对象,利用移动语义或 unique_ptr

这似乎是一个效率问题。如果我想从 foo() 返回一个小工具怎么办? C++11 的移动语义使得返回大对象变得更容易。只需写 Gadget foo() { ... },它就可以工作,而且工作很快。您不需要自己弄乱&&,只需按值返回内容,该语言通常能够进行必要的优化。 (甚至在 C++03 之前,编译器在避免不必要的复制方面做得非常好。)

正如 Stroustrup 在视频其他地方所说(释义):“只有计算机科学家才会坚持复制一个对象,然后破坏原件。(观众笑)。为什么不直接将对象移动到新位置?这就是人类(不是计算机科学家)期待。”

当您可以保证只需要一个对象的副本时,就更容易理解对象的生命周期。你可以选择你想要的生命周期策略,如果你愿意,垃圾收集就在那里。但是当您了解其他方法的好处时,您会发现垃圾收集在您的偏好列表的底部。

如果这对您不起作用,您可以使用 unique_ptr,否则,您可以使用 shared_ptr。在内存管理方面,与许多其他语言相比,编写良好的 C++11 更短、更易于阅读和更易于教学。


GC 应该只用于不获取资源的对象(即要求其他实体代表他们做事“直到另行通知”)。如果 Gadget 不要求其他任何东西代表它做任何事情,那么如果删除了(对 Java 而言)无意义的 delete 语句,则原始代码在 Java 中将是完全安全的。
@supercat,带有无聊析构函数的对象很有趣。 (我没有定义“无聊”,但基本上不需要调用析构函数,除了释放内存)。当 T “无聊”时,单个编译器可能会特别对待 shared_ptr<T>。它可以决定不实际管理该类型的引用计数器,而是使用 GC。这将允许在无需开发人员注意的情况下使用 GC。 shared_ptr 可以简单地看作是一个 GC 指针,用于合适的 T。但这有局限性,它会使许多程序变慢。
一个好的类型系统对于 GC 和 RAII 管理的堆对象应该有不同的类型,因为一些使用模式对一个很好,而对另一个很差。在 .NET 或 Java 中,无论字符串的长度如何,语句 string1=string2; 都会非常快速地执行(它实际上只不过是寄存器加载和寄存器存储),并且不需要任何锁定来确保上述语句是否在写入 string2 时执行,string1 将保存旧值或新值,没有未定义的行为)。
在 C++ 中,shared_ptr<String> 的赋值需要大量的幕后同步,如果同时读取和写入变量,String 的赋值可能会表现得很奇怪。想要同时写入和读取 String 的情况并不常见,但如果某些代码希望将持续的状态报告提供给其他线程,则可能会出现这种情况。在 .NET 和 Java 中,这些东西只是“工作”。
@curiousguy 没有任何改变,除非您采取正确的预防措施,否则 Java 仍然允许在构造函数完成后立即调用终结器。这是一个现实生活中的例子:“finalize() called on strongly reachable objects in Java 8”。结论是永远不要使用此功能,几乎每个人都同意这是该语言的历史设计错误。当我们遵循该建议时,该语言提供了我们喜欢的确定性。
e
einpoklum

tl; dr:因为现代 C++ 不需要垃圾收集。

Bjarne Stroustrup 的常见问题解答 answer on this matter says

我不喜欢垃圾。我不喜欢乱扔垃圾。我的理想是通过不产生任何垃圾来消除对垃圾收集器的需求。现在这是可能的。

对于这些天编写的代码(C++17 并遵循官方 Core Guidelines),情况如下:

大多数与内存所有权相关的代码都在库中(尤其是那些提供容器的)。

大多数涉及内存所有权的代码使用都遵循 CADRe 或 RAII 模式,因此在构造时进行分配,在销毁时进行释放,这发生在退出分配某些内容的范围时。

您不会直接显式分配或取消分配内存。

原始指针不拥有内存(如果您遵循了指南),因此您不能通过传递它们来泄漏。

如果您想知道如何在内存中传递值序列的起始地址 - 您可以而且应该更喜欢 span,从而避免了对原始指针的需求。你仍然可以使用这样的指针,它们只是非拥有的。

如果你真的需要一个拥有的“指针”,你可以使用 C++ 的标准库智能指针——它们不会泄漏,而且效率相当高(尽管 ABI 可能会妨碍它)。或者,您可以使用“所有者指针”跨范围边界传递所有权。这些是不常见的,必须明确使用;但是当被采用时 - 它们允许对泄漏进行很好的静态检查。

“哦,是吗?但是……

......如果我只是按照我们过去编写 C++ 的方式编写代码?”

实际上,您可以忽略所有指南并编写泄漏的应用程序代码 - 它会像往常一样编译和运行(和泄漏)。

但这不是“只是不这样做”的情况,在这种情况下,开发人员被期望有德行并进行大量的自我控制;编写不符合标准的代码并不简单,编写起来也不是更快,性能也不是更好。逐渐地,它也将变得更加难以编写,因为您将面临越来越多的“阻抗不匹配”与符合代码提供和期望的内容。

...如果我重新诠释_演员?还是做复杂的指针运算?还是其他类似的黑客?”

事实上,如果你下定决心,你可以编写出把事情搞得一团糟的代码,尽管你可以很好地遵守这些准则。但:

您很少会这样做(就代码中的位置而言,不一定就执行时间的一部分而言)您只会故意这样做,而不是偶然这样做。这样做将在符合准则的代码库中脱颖而出。无论如何,这是一种可以绕过另一种语言的 GC 的代码。

……图书馆发展?”

如果您是 C++ 库开发人员,那么您确实会编写涉及原始指针的不安全代码,并且您需要仔细且负责任地编写代码 - 但这些是由专家编写的自包含代码(更重要的是,由专家审查)。

所以,就像 Bjarne 说的:一般来说,确实没有收集垃圾的动机,因为你们都确保不产生垃圾。 GC 正在成为 C++ 的一个非问题。

这并不是说 GC 对于某些特定应用程序来说不是一个有趣的问题,当您想要使用自定义分配和取消分配策略时。对于那些你想要自定义分配和取消分配的人,而不是语言级别的 GC。


好吧,如果您正在研磨字符串,它确实(需要 GC).. 想象一下,您有大型字符串数组(想想数百兆字节),您正在零碎构建,然后处理和重建为不同长度,删除未使用的数组,组合其他数组等。我知道是因为我不得不切换到高级语言来应对。 (当然你也可以构建自己的 GC)。
@user1863152:在这种情况下,自定义分配器会很有用。它仍然不需要语言集成的 GC ......
对 einpoklum :是的。这只是课程的马。我的要求是处理动态变化的运输乘客信息加仑数。引人入胜的主题..真正归结为软件哲学。
Java 和 .NET 世界发现 GC 最终存在一个大问题 - 它无法扩展。当您在内存中拥有数十亿个活动对象时,就像我们现在使用任何非平凡的软件所做的那样,您将不得不开始编写代码来隐藏 GC 中的东西。在 Java 和 .NET 中使用 GC 是一种负担。
@ZachSaw:有多少程序会在内存中拥有十亿个活动对象?您是说所有没有更多功能的程序都是微不足道的?
U
Uri

C++ 背后的想法是,您不会为不使用的功能支付任何性能影响。因此,添加垃圾收集意味着让一些程序像 C 那样直接在硬件上运行,而另一些程序则在某种运行时虚拟机中运行。

没有什么可以阻止您使用某种形式的智能指针,这些指针绑定到某些第三方垃圾收集机制。我似乎记得微软在 COM 上做过类似的事情,但效果并不好。


我认为 GC 不需要 VM。编译器可以将代码添加到所有指针操作以更新全局状态,同时在后台运行一个单独的线程根据需要删除对象。
我同意。您不需要虚拟机,但是当您开始像在后台那样为您管理内存时,我的感觉是您已经离开了实际的“电线”并且有某种虚拟机情况。
N
Nemanja Trifunovic

要回答有关 C++ 的大多数“为什么”问题,请阅读Design and Evolution of C++


s
supercat

原始 C 语言背后的基本原则之一是内存由一系列字节组成,代码只需要关心这些字节在使用它们的确切时刻意味着什么。现代 C 允许编译器施加额外的限制,但 C 包括 - 并且 C++ 保留 - 将指针分解为字节序列,将包含相同值的任何字节序列组装成指针,然后使用该指针访问较早的对象。

虽然这种能力在某些类型的应用程序中可能是有用的——甚至是必不可少的——但包含这种能力的语言在支持任何有用和可靠的垃圾收集的能力方面将非常有限。如果编译器不知道对构成指针的位所做的一切,它就无法知道足以重建指针的信息是否存在于宇宙的某个地方。因为这些信息有可能以计算机无法访问的方式存储,即使它知道它们(例如,构成指针的字节可能已经在屏幕上显示足够长的时间供某人写入它们写在一张纸上),计算机实际上不可能知道将来是否可以使用指针。

许多垃圾收集框架的一个有趣的怪癖是对象引用不是由其中包含的位模式定义的,而是由对象引用中保存的位与其他地方保存的其他信息之间的关系定义的。在 C 和 C++ 中,如果存储在指针中的位模式标识了一个对象,则该位模式将标识该对象,直到该对象被显式销毁。在典型的 GC 系统中,一个对象可能在某个时刻由位模式 0x1234ABCD 表示,但下一个 GC 周期可能会将对 0x1234ABCD 的所有引用替换为对 0x4321BABE 的引用,因此该对象将由后一种模式表示。即使要显示与对象引用关联的位模式,然后稍后从键盘读回,也不期望相同的位模式可用于识别相同的对象(或任何对象)。


这是一个非常好的观点,我最近刚刚从我的指针中偷了一些位,因为否则会有大量的缓存未命中。
@PasserBy:我想知道有多少使用 64 位指针的应用程序会从使用缩放的 32 位指针作为对象引用中受益更多,或者将几乎所有内容都保存在 4GiB 的地址空间中并使用特殊对象从高位存储/检索数据- 超速存储?机器有足够的 RAM,64 位指针的 RAM 消耗可能无关紧要,除了它们吞噬的缓存是 32 位指针的两倍。
S
Sohail Si

简短的回答:我们不知道如何有效地进行垃圾收集(时间和空间开销很小)并且始终正确(在所有可能的情况下)。

长答案:就像 C 一样,C++ 是一种系统语言。这意味着它在您编写系统代码(例如操作系统)时使用。换句话说,C++ 的设计就像 C 一样,以尽可能最佳的性能作为主要目标。该语言的标准不会添加任何可能阻碍性能目标的功能。

这暂停了一个问题:为什么垃圾收集会阻碍性能?主要原因是,在实现方面,我们 [计算机科学家] 不知道如何在所有情况下以最小的开销进行垃圾收集。因此,C++ 编译器和运行时系统不可能一直有效地执行垃圾收集。另一方面,C++ 程序员应该了解他的设计/实现,并且他是决定如何最好地进行垃圾收集的最佳人选。

最后,如果控制(硬件、细节等)和性能(时间、空间、功率等)不是主要限制因素,那么 C++ 就不是正确的工具。其他语言可能会提供更好的服务并提供更多 [隐藏] 运行时管理,但会产生必要的开销。


最佳答案。这是唯一指向主要原因和根本原因的答案。
B
Bob Holmes

所有的技术讨论都使这个概念过于复杂。

如果您将所有内存的 GC 自动放入 C++ 中,那么请考虑使用 Web 浏览器之类的东西。 Web 浏览器必须加载完整的 Web 文档并运行 Web 脚本。您可以将 Web 脚本变量存储在文档树中。在浏览器中打开许多选项卡的 BIG 文档中,这意味着每次 GC 必须执行完整收集时,它还必须扫描所有文档元素。

在大多数计算机上,这意味着会发生 PAGE FAULTS。因此,回答问题的主要原因是会发生PAGE FAULTS。当您的 PC 开始进行大量磁盘访问时,您就会知道这一点。这是因为 GC 必须接触大量内存才能证明无效指针。当您有一个真正的应用程序使用大量内存时,由于 PAGE FAULTS,必须扫描每个集合的所有对象是严重的。页面错误是指虚拟内存需要从磁盘读回 RAM。

所以正确的解决方案是将一个应用程序分成需要GC的部分和不需要GC的部分。在上面的 Web 浏览器示例中,如果文档树是使用 malloc 分配的,但 javascript 使用 GC 运行,那么每次 GC 启动时,它只会扫描一小部分内存和内存的所有 PAGED OUT 元素文档树不需要重新分页。

要进一步了解这个问题,请查看虚拟内存以及它是如何在计算机中实现的。这完全是因为当没有那么多 RAM 时,程序可以使用 2GB。在具有 2GB RAM 的 32 位系统的现代计算机上,只要一个程序正在运行,就不是这样的问题。

作为另一个示例,考虑一个必须跟踪所有对象的完整集合。首先,您必须扫描所有可通过根访问的对象。第二次扫描步骤 1 中可见的所有对象。然后扫描等待的析构函数。然后再次转到所有页面并关闭所有不可见的对象。这意味着许多页面可能会被多次换出和换回。

所以我简短的回答是,由于接触所有内存而发生的 PAGE FAULTS 的数量会导致程序中所有对象的完全 GC 不可行,因此程序员必须将 GC 视为脚本之类的辅助工具和数据库工作,但通过手动内存管理做正常的事情。

另一个非常重要的原因当然是全局变量。为了让收集器知道全局变量指针在 GC 中,它需要特定的关键字,因此现有的 C++ 代码将无法工作。


M
Mike76

当我们将 C++ 与 Java 进行比较时,我们发现 C++ 在设计时并没有考虑到隐式垃圾收集,而 Java 是。

在 C 风格中拥有诸如任意指针之类的东西不仅对 GC 实现不利,而且还会破坏大量 C++ 遗留代码的向后兼容性。

除此之外,C++ 是一种旨在作为独立可执行文件运行的语言,而不是具有复杂的运行时环境。

总而言之:是的,可以将垃圾收集添加到 C++,但为了连续性,最好不要这样做。


释放内存和运行析构函数是完全独立的问题。 (Java 没有析构函数,它是一个 PITA。)GC 释放内存,它不运行 dtors。
M
Marc Coll

主要有两个原因:

因为它不需要一个(恕我直言)因为它与 RAII 几乎不兼容,RAII 是 C++ 的基石

C++ 已经提供了手动内存管理、堆栈分配、RAII、容器、自动指针、智能指针......这应该足够了。垃圾收集器适用于不想花 5 分钟思考谁应该拥有哪些对象或何时应该释放资源的懒惰程序员。这不是我们在 C++ 中做事的方式。


有许多(较新的)算法在没有垃圾收集的情况下很难实现。时间继续前进。创新还来自与(垃圾收集)高级语言非常匹配的新见解。尝试将这些中的任何一个反向移植到无 GC 的 C++ 中,您会注意到路上的颠簸。 (我知道我应该举个例子,但我现在有点着急。抱歉。我现在能想到的一个围绕持久数据结构,其中引用计数不起作用。)。
w
www-0av-Com

实施垃圾收集实际上是从低级到高级的范式转变。

如果您查看带有垃圾收集的语言中处理字符串的方式,您会发现它们只允许高级字符串操作函数并且不允许对字符串进行二进制访问。简而言之,所有字符串函数首先检查指针以查看字符串的位置,即使您只是绘制一个字节。因此,如果您正在执行一个循环来处理带有垃圾收集的语言中的字符串中的每个字节,它必须计算每次迭代的基本位置和偏移量,因为它不知道字符串何时移动。然后你必须考虑堆、堆栈、线程等。