我问这个问题,首先不是因为垃圾收集的优点。我问这个问题的主要原因是我知道Bjarne Stroustrup说过c++在某个时间点上会有一个垃圾收集器。
话虽如此,为什么还没有加入呢?c++已经有了一些垃圾收集器。这是那种“说起来容易做起来难”的事情吗?或者还有其他原因没有添加它(并且不会在c++ 11中添加)?
交叉链接:
c++的垃圾收集器
澄清一下,我理解c++最初创建时没有垃圾收集器的原因。我想知道为什么不能添加收集器。
我问这个问题,首先不是因为垃圾收集的优点。我问这个问题的主要原因是我知道Bjarne Stroustrup说过c++在某个时间点上会有一个垃圾收集器。
话虽如此,为什么还没有加入呢?c++已经有了一些垃圾收集器。这是那种“说起来容易做起来难”的事情吗?或者还有其他原因没有添加它(并且不会在c++ 11中添加)?
交叉链接:
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比许多其他语言更短,更容易阅读,也更容易教授。
其他回答
什么类型?它应该针对嵌入式洗衣机控制器、手机、工作站或超级计算机进行优化吗? 它应该优先考虑gui响应还是服务器加载? 它应该使用大量内存还是大量CPU?
C/c++被用于太多不同的环境。 我怀疑像增强智能指针这样的东西对大多数用户来说就足够了
编辑:自动垃圾收集器并不是一个性能问题(你总是可以购买更多的服务器),而是一个可预测的性能问题。 不知道GC什么时候会起作用就像雇佣一个嗜睡症的飞行员,大多数时候他们是很棒的-但当你真的需要响应的时候!
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。
本来可以添加隐式垃圾收集,但它没有达到要求。可能不仅仅是因为实现的复杂性,还因为人们不能足够快地达成普遍共识。
引用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没有他想要的那么多。
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++的大多数“为什么”问题,请阅读c++的设计与进化