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

话虽如此,为什么还没有加入呢?c++已经有了一些垃圾收集器。这是那种“说起来容易做起来难”的事情吗?或者还有其他原因没有添加它(并且不会在c++ 11中添加)?

交叉链接:

c++的垃圾收集器

澄清一下,我理解c++最初创建时没有垃圾收集器的原因。我想知道为什么不能添加收集器。


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

C/c++被用于太多不同的环境。 我怀疑像增强智能指针这样的东西对大多数用户来说就足够了

编辑:自动垃圾收集器并不是一个性能问题(你总是可以购买更多的服务器),而是一个可预测的性能问题。 不知道GC什么时候会起作用就像雇佣一个嗜睡症的飞行员,大多数时候他们是很棒的-但当你真的需要响应的时候!

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

引用Bjarne Stroustrup自己的话:

我本来希望是个收垃圾的 哪些可以选择启用 是c++ 0x的一部分,但是 我有足够多的技术问题 凑合着用一个详细的 如何规范这样的收集器 的其余部分集成 语言,如果提供的话。事实就是这样 基本上拥有c++ 0x的所有特性, 存在一个实验性的实现。

这里有关于这个话题的很好的讨论。

总体概述:

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

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

垃圾收集有两种类型…

显式垃圾收集:

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

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

对于c++ 0x之前的版本,boost:shared_ptr是存在的,用途相同。

隐式垃圾收集:

但是它没有透明的垃圾收集。不过,它将是未来c++规范的一个焦点。

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

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

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

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

If you want automatic garbage collection, there are good commercial and public-domain garbage collectors for C++. For applications where garbage collection is suitable, C++ is an excellent garbage collected language with a performance that compares favorably with other garbage collected languages. See The C++ Programming Language (4rd Edition) for a discussion of automatic garbage collection in C++. See also, Hans-J. Boehm's site for C and C++ garbage collection (archive). Also, C++ supports programming techniques that allow memory management to be safe and implicit without a garbage collector. I consider garbage collection a last choice and an imperfect way of handling for resource management. That does not mean that it is never useful, just that there are better approaches in many situations.

来源:http://www.stroustrup.com/bs_faq.html垃圾收集

至于为什么它没有内置它,如果我没记错的话,它是在GC出现之前发明的,我不相信这种语言有GC,有几个原因(我不相信它有GC)。C)向后兼容。

希望这能有所帮助。

要回答关于c++的大多数“为什么”问题,请阅读c++的设计与进化

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

deterministic lifetimes of objects (reference counting gives you this, but GC doesn't. Although it may not be that big of a deal). what happens if a destructor throws when the object is being garbage collected? Most languages ignore this exception, since theres really no catch block to be able to transport it to, but this is probably not an acceptable solution for C++. How to enable/disable it? Naturally it'd probably be a compile time decision but code that is written for GC vs code that is written for NOT GC is going to be very different and probably incompatible. How do you reconcile this?

这些只是面临的问题中的一小部分。

为了增加争论。

关于垃圾收集有一些已知的问题,了解它们有助于理解为什么c++中没有垃圾收集。

1. 性能?

第一个抱怨通常是关于性能,但大多数人并没有真正意识到他们在谈论什么。正如马丁·贝克特(Martin Beckett)所指出的,问题可能不是表现本身,而是表现的可预测性。

目前有两个GC家族被广泛部署:

标记和清扫类 引用计数类型

标记和清除更快(对整体性能的影响较小),但它患有“冻结世界”综合征:即当GC开始时,其他一切都停止,直到GC完成清理。如果您希望构建一个在几毫秒内响应的服务器……有些交易不会达到你的期望:)

The problem of Reference Counting is different: reference-counting adds overhead, especially in Multi-Threading environments because you need to have an atomic count. Furthermore there is the problem of reference cycles so you need a clever algorithm to detect those cycles and eliminate them (generally implement by a "freeze the world" too, though less frequent). In general, as of today, this kind (even though normally more responsive or rather, freezing less often) is slower than the Mark And Sweep.

I have seen a paper by Eiffel implementers that were trying to implement a Reference Counting Garbage Collector that would have a similar global performance to Mark And Sweep without the "Freeze The World" aspect. It required a separate thread for the GC (typical). The algorithm was a bit frightening (at the end) but the paper made a good job of introducing the concepts one at a time and showing the evolution of the algorithm from the "simple" version to the full-fledged one. Recommended reading if only I could put my hands back on the PDF file...

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

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

锁(多线程,文件句柄,…) 连接(到数据库、另一台服务器……)

其思想是正确地控制对象的生命周期:

只要你需要,它就应该是活的 当你用完它的时候,它应该被杀死

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

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

当堆栈分配足够时不要使用GC:这通常是为了解决性能问题,但在我们的例子中,它确实有帮助,因为作用域定义了生命周期 使用构造…但它是显式(弱)RAII,而在c++中RAII是隐式的,因此用户不能在不知不觉中犯错误(通过省略using关键字)

3.智能指针

在c++中,智能指针通常是处理内存的灵丹妙药。我经常听到:我们根本不需要GC,因为我们有智能指针。

这是大错特错了。

智能指针确实有帮助:auto_ptr和unique_ptr使用RAII概念,确实非常有用。它们很简单,你可以很容易地自己写出来。

然而,当一个人需要共享所有权时,它变得更加困难:你可能在多个线程之间共享,并且在计数的处理上有一些微妙的问题。因此,很自然地使用shared_ptr。

这很棒,毕竟这就是Boost的用途,但它并不是万能的。事实上,shared_ptr的主要问题是它模拟了一个由引用计数实现的GC,但你需要自己实现周期检测…开始

当然有这个weak_ptr的东西,但不幸的是,我已经看到内存泄漏尽管使用shared_ptr,因为这些周期…当你在多线程环境中,它是非常难以检测的!

4. 解决方案是什么?

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

如果可能的话,最好在一个特定的时间拥有一个所有者 如果没有,请确保您的类图没有与所有权相关的任何循环,并通过weak_ptr的微妙应用来打破它们

所以,如果有一个GC…然而,这不是一个微不足道的问题。与此同时,我们只需要卷起袖子。

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

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

The general idea of garbage collection is that it should make a reasonable attempt at assuring that a memory allocation will succeed. Unfortunately, it's essentially impossible to guarantee that any memory allocation will succeed, even if you do have a garbage collector in operation. This is true to some extent in any case, but particularly so in the case of C++, because it's (probably) not possible to use a copying collector (or anything similar) that moves objects in memory during a collection cycle.

如果不能移动对象,就不能创建一个单独的、连续的内存空间来进行分配——这意味着您的堆(或自由存储区,或任何您喜欢称呼它的地方)可能会随着时间的推移而变得碎片化。这反过来又会阻止分配成功,即使空闲内存比请求的内存多。

尽管有可能提出某种保证,即(本质上)如果您重复完全相同的分配模式,并且它在第一次成功,那么它将在后续迭代中继续成功,前提是分配的内存在迭代之间变得不可访问。这是一个非常微弱的保证,基本上毫无用处,但我看不到任何加强它的合理希望。

即便如此,它也比为c++所提议的更强大。之前的提议[警告:PDF](被放弃了)根本不能保证任何东西。在28页的提案中,你在外部可观察到的行为中看到的是一个单一的(不规范的)注释:

[注意:对于垃圾收集程序,高质量的托管实现应该尝试最大限度地回收不可访问的内存量。-结束注释]

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

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

我要用数学方法总结一下情况:这是一个复杂的情况。数学家都知道,复数有两部分:实数和虚数。在我看来,我们这里的成本是真实的,但收益(至少大部分)是虚构的。

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

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

On most computers this means that PAGE FAULTS will occur. So the main reason, to answer the question is that PAGE FAULTS will occur. You will know this as when your PC starts making lots of disk access. This is because the GC must touch lots of memory in order to prove invalid pointers. When you have a bona fide application using lots of memory, having to scan all objects every collection is havoc because of the PAGE FAULTS. A page fault is when virtual memory needs to get read back into RAM from disk.

因此,正确的解决方案是将应用程序分为需要GC的部分和不需要GC的部分。在上面的web浏览器示例中,如果文档树是用malloc分配的,但javascript是用GC运行的,那么每次GC启动时,它只扫描一小部分内存,并且文档树的内存中所有page OUT元素不需要被换回。

为了进一步理解这个问题,请查阅虚拟内存以及它是如何在计算机中实现的。这都是关于当没有那么多RAM时,程序可用2GB的事实。在32BIt系统的2GB RAM的现代计算机上,只要只有一个程序在运行,就不存在这样的问题。

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

因此,我的回答是,由于触及所有内存而发生的PAGE FAULTS的数量导致对程序中所有对象进行完整的GC是不可行的,因此程序员必须将GC视为脚本和数据库工作的辅助,但使用手动内存管理进行正常工作。

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

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

长一点的回答: 就像C一样,c++是一种系统语言;这意味着当您编写系统代码时,例如操作系统时,将使用它。换句话说,c++就像C一样,以尽可能好的性能作为主要目标。该语言标准不会增加任何可能阻碍性能目标的特性。

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

最后,如果控制(硬件、细节等)和性能(时间、空间、电源等)不是主要的限制,那么c++就不是合适的工具。其他语言可能会更好,并提供更多[隐藏的]运行时管理,以及必要的开销。

Stroustrup在2013年的Going Native大会上对此发表了一些很好的评论。

在这个视频中跳过大约25m50s。(其实我建议你看完整个视频,但这段视频跳过了垃圾收集的内容。)

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

在现代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;
}

This is unsafe in C++. But it's also unsafe in Java! In C++, if the function returns early, the delete will never be called. But if you had full garbage collection, such as in Java, you merely get a suggestion that the object will be destructed "at some point in the future" (Update: it's even worse that this. Java does not promise to call the finalizer ever - it maybe never be called). This isn't good enough if Gadget holds an open file handle, or a connection to a database, or data which you have buffered for write to a database at a later point. We want the Gadget to be destroyed as soon as it's finished, in order to free these resources as soon as possible. You don't want your database server struggling with thousands of database connections that are no longer needed - it doesn't know that your program is finished working.

那么解决方案是什么呢?有几种方法。最明显的方法,你将用于绝大多数的对象是:

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;
}

这需要更少的字符来输入。它没有新的障碍。它不需要您键入Gadget两次。对象在函数结束时被销毁。如果这是你想要的,这是非常直观的。gadget的行为与int或double相同。可预测,易读,易教。一切都是“价值”。有时是一个很大的值,但是值更容易教,因为你不需要指针(或引用)那样的“远距离操作”。

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

范围和生命周期很重要。大多数情况下,如果生命期与作用域相同,就会更容易。这样更容易理解,也更容易教。当您想要不同的生存期时,阅读代码就会很明显地知道您正在这样做,例如通过使用shared_ptr。(或利用move-semantics或unique_ptr按值返回(大)对象。

这似乎是一个效率问题。如果我想从foo()返回一个Gadget怎么办?c++ 11的move语义使得返回大对象更容易。只需编写Gadget foo(){…它会起作用的,而且很快就会起作用。你不需要打扰&&自己,只需要按值返回,语言通常能够做必要的优化。(即使在c++ 03之前,编译器在避免不必要的复制方面做得非常好。)

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

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

如果这对您不起作用,您可以使用unique_ptr,如果失败,可以使用shared_ptr。在内存管理方面,编写良好的c++ 11比许多其他语言更短,更容易阅读,也更容易教授。

当我们比较c++和Java时,我们看到c++在设计时并没有考虑到隐式垃圾收集,而Java则是。

在C风格中使用任意指针这样的东西不仅不利于gc实现,而且还会破坏大量c++遗留代码的向后兼容性。

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

总之: 是的,在c++中添加垃圾收集是可能的,但是为了连续性,最好不要这样做。

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

While that ability can be useful--or even indispensable--in some kinds of applications, a language that includes that ability will be very limited in its ability to support any kind of useful and reliable garbage collection. If a compiler doesn't know everything that has been done with the bits that made up a pointer, it will have no way of knowing whether information sufficient to reconstruct the pointer might exist somewhere in the universe. Since it would be possible for that information to be stored in ways that the computer wouldn't be able to access even if it knew about them (e.g. the bytes making up the pointer might have been shown on the screen long enough for someone to write them down on a piece of paper), it may be literally impossible for a computer to know whether a pointer could possibly be used in the future.

An interesting quirk of many garbage-collected frameworks is that an object reference not defined by the bit patterns contained therein, but by the relationship between the bits held in the object reference and other information held elsewhere. In C and C++, if the bit pattern stored in a pointer identifies an object, that bit pattern will identify that object until the object is explicitly destroyed. In a typical GC system, an object may be represented by a bit pattern 0x1234ABCD at one moment in time, but the next GC cycle might replace all references to 0x1234ABCD with references to 0x4321BABE, whereupon the object would be represented by the latter pattern. Even if one were to display the bit pattern associated with an object reference and then later read it back from the keyboard, there would be no expectation that the same bit pattern would be usable to identify the same object (or any object).

主要有两个原因:

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

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

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

Bjarne Stroustrup对此问题的常见问题解答如下:

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


现在编写的代码(c++ 17和遵循官方核心指南)的情况如下:

Most memory ownership-related code is in libraries (especially those providing containers). Most use of code involving memory ownership follows the CADRe or RAII pattern, so allocation is made on construction and deallocation on destruction, which happens when exiting the scope in which something was allocated. You do not explicitly allocate or deallocate memory directly. Raw pointers do not own memory (if you've followed the guidelines), so you can't leak by passing them around. If you're wondering how you're going to pass the starting addresses of sequences of values in memory - you can and should prefer span's, obviating the need for raw pointers. You can still use such pointers, they'll just be non-owning. If you really need an owning "pointer", you use C++' standard-library smart pointers - they can't leak, and are decently efficient (although the ABI can get in the way of that). Alternatively, you can pass ownership across scope boundaries with "owner pointers". These are uncommon and must be used explicitly; but when adopted - they allow for nice static checking against leaks.

“哦,是吗?但是……

... 如果我只是像以前写c++那样写代码?”

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

但这并不是一种“不要这么做”的情况,即开发者应该保持良好的自我控制;编写不符合规范的代码并不简单,也没有更快,也没有更好的性能。慢慢地,它也会变得更加难以编写,因为你将面临与符合规范的代码所提供和期望的“阻抗不匹配”的增加。

... 如果我retrepret_cast ?或者做复杂的指针运算?还是其他类似的黑客?”

事实上,如果你用心去做,你可以编写一些代码,尽管你很好地遵循了指导原则。但是:

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

... 图书馆发展?”

如果你是c++库开发人员,那么你确实会编写包含原始指针的不安全代码,并且你被要求谨慎而负责地编码——但这些是由专家编写的自包含代码片段(更重要的是,由专家评审)。

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

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

强制垃圾收集实际上是一个低级到高级的范式转换。

If you look at the way strings are handled in a language with garbage collection, you will find they ONLY allow high level string manipulation functions and do not allow binary access to the strings. Simply put, all string functions first check the pointers to see where the string is, even if you are only drawing out a byte. So if you are doing a loop that processes each byte in a string in a language with garbage collection, it must compute the base location plus offset for each iteration, because it cannot know when the string has moved. Then you have to think about heaps, stacks, threads, etc etc.