我一直听到人们抱怨C ++没有垃圾收集。我还听说C ++标准委员会正在考虑将其添加到该语言中。我担心我只是没有看到它的意义......使用智能指针的RAII消除了对它的需要,对吗?
我对垃圾收集的唯一经验是在几台便宜的八十年代家用电脑上,这意味着系统每隔一段时间就会冻结几秒钟。我确信它从那时起已有所改善,但正如你猜测的那样,这并没有让我对它持高度评价。
垃圾收集为经验丰富的C ++开发人员提供了哪些优势?
答案 0 :(得分:67)
我很抱歉。严重。
C ++有RAII,我总是抱怨在Garbage Collected语言中找不到RAII(或阉割的RAII)。
另一种工具。
Matt J在他的帖子(Garbage Collection in C++ -- why?)中写得非常正确:我们不需要C ++功能,因为大多数功能都可以用C编码,而且我们不需要C功能,因为大多数功能都可以在汇编等编码。 C ++必须发展。
作为开发人员:我不关心GC。我尝试了RAII和GC,我发现RAII非常优越。正如Greg Rogers在他的帖子(Garbage Collection in C++ -- why?)中所说的那样,内存泄漏并不是那么可怕(至少在C ++中,如果真的使用C ++,它们很少见)以证明GC而不是RAII。 GC具有非确定性的释放/终止,只是编写一个不关心特定内存选择的代码的方法。
这最后一句很重要:编写“juste不关心”的代码非常重要。以同样的方式在C ++ RAII中我们不关心资源释放,因为RAII为我们做这件事,或者为了对象初始化,因为构造函数为我们做了,有时候编写代码而不关心谁是内存的所有者是很重要的,以及我们需要这个或这段代码的指针(共享,弱等)。 C ++中似乎需要GC。(即使我个人没有看到它)
有时,在应用中,您有“浮动数据”。想象一下树状结构的数据,但没有人真的是数据的“拥有者”(没有人真正关心它何时会被破坏)。多个对象可以使用它,然后丢弃它。当没有人再使用它时,你希望它被释放。
C ++方法使用智能指针。我想到了boost :: shared_ptr。因此,每个数据都由其自己的共享指针拥有。凉。问题是当每条数据都可以引用另一条数据时。您不能使用共享指针,因为它们使用的是引用计数器,它不支持循环引用(A指向B,B指向A)。因此,您必须知道如何使用弱指针(boost :: weak_ptr)以及何时使用共享指针。
使用GC,您只需使用树结构化数据。
缺点是你必须不关心何时“浮动数据”真的会被破坏。只有 >>才会被销毁。
所以最后,如果做得恰当,并且与当前C ++的习语兼容,那么GC将是又一个很好的C ++工具。
C ++是一种多范式语言:添加GC可能会让一些C ++粉丝因为叛国而哭泣,但最后,这可能是一个好主意,我想C ++标准委员会不会让这种专业功能打破语言,所以我们可以信任他们做必要的工作来启用一个不会干扰C ++的正确C ++ GC:和C ++一样,如果你不需要一个功能,不要使用它会花费你一切。
答案 1 :(得分:11)
简短的回答是垃圾收集原则上与使用智能指针的RAII非常相似。如果您分配的每一块内存都位于一个对象中,并且该对象仅由智能指针引用,那么您有一些接近垃圾收集的东西(可能更好)。优势来自于不必如此明智地确定每个对象的范围和智能指针,并让运行时为您完成工作。
这个问题似乎类似于“C ++必须为经验丰富的程序集开发人员提供什么?指令和子程序消除了对它的需要,对吧?”
答案 2 :(得分:9)
随着像valgrind这样的优秀记忆检查器的出现,我认为垃圾收集并没有太多用作安全网“万一”我们忘了解除某些东西 - 特别是因为它对管理更通用的东西没什么帮助除了记忆之外的资源的情况(虽然这些不太常见)。此外,在我看到的代码中,显式分配和释放内存(即使使用智能指针)也是相当罕见的,因为容器通常更简单,更好。
但垃圾收集可能会提供潜在的性能优势,特别是如果很多短期对象正在堆分配的话。 GC还可能为新创建的对象提供更好的引用局部性(与堆栈中的对象相当)。
答案 3 :(得分:8)
C ++中GC支持的激励因素似乎是lambda编程,匿名函数等。事实证明,lambda库可以在不关心清理的情况下分配内存。普通开发人员的好处是更简单,更可靠,更快速地编译lambda库。
GC还有助于模拟无限内存;您需要删除POD的唯一原因是您需要回收内存。如果您有GC或无限内存,则无需再删除POD。
答案 4 :(得分:7)
委员会没有添加垃圾收集,他们正在添加一些功能,以便更安全地实现垃圾收集。只有时间才能证明它们对未来的编译器是否真的有任何影响。具体实现可能有很大差异,但很可能涉及基于可达性的收集,这可能涉及轻微挂起,具体取决于它是如何完成的。
但有一件事是,没有符合标准的垃圾收集器能够调用析构函数 - 只能静默地重用丢失的内存。
答案 5 :(得分:7)
我不明白人们如何争辩RAII取代GC,或者是非常优越的。有很多案例由gc处理,RAII根本无法处理。他们是不同的野兽。
首先,RAII不是防弹的:它可以解决C ++中普遍存在的一些常见故障,但在很多情况下RAII根本没有帮助;异步事件(如UNIX下的信号)很脆弱。从根本上说,RAII依赖于范围界定:当变量超出范围时,它会自动释放(假设析构函数当然正确实现)。
这是一个简单的例子,auto_ptr或RAII都不能帮助你:
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <memory>
using namespace std;
volatile sig_atomic_t got_sigint = 0;
class A {
public:
A() { printf("ctor\n"); };
~A() { printf("dtor\n"); };
};
void catch_sigint (int sig)
{
got_sigint = 1;
}
/* Emulate expensive computation */
void do_something()
{
sleep(3);
}
void handle_sigint()
{
printf("Caught SIGINT\n");
exit(EXIT_FAILURE);
}
int main (void)
{
A a;
auto_ptr<A> aa(new A);
signal(SIGINT, catch_sigint);
while (1) {
if (got_sigint == 0) {
do_something();
} else {
handle_sigint();
return -1;
}
}
}
永远不会调用A的析构函数。当然,这是一个人为的,有点人为的例子,但实际上也会发生类似的情况;例如,当你的代码被处理SIGINT的另一个代码调用时,你完全没有控制权(具体的例子:matlab中的mex扩展)。这也是为什么最终在python中不能保证执行某些东西的原因。在这种情况下,Gc可以帮助您。
其他成语不能很好地解决这个问题:在任何非平凡的程序中,你都需要有状态的对象(我在这里使用广义的词对象,它可以是语言允许的任何构造);如果你需要在一个函数之外控制状态,那么用RAII就不能轻易做到这一点(这就是RAII对异步编程没有帮助的原因)。 OTOH,gc可以看到进程的整个内存,也就是它知道它分配的所有对象,并且可以异步清理。
使用gc也可以快得多,原因相同:如果你需要分配/解除分配许多对象(特别是小对象),gc将远远超过RAII,除非你编写自定义分配器,因为gc可以一次分配/清理许多对象。一些众所周知的C ++项目使用gc,即使性能很重要(例如参见Tim Sweenie关于在虚幻竞技场中使用gc:http://lambda-the-ultimate.org/node/1277)。 GC基本上以延迟为代价提高了吞吐量。
当然,有些情况下RAII优于gc;特别是,gc概念主要关注的是内存,而这并不是唯一的资源。像文件等等......可以用RAII很好地处理。没有像python或ruby这样的内存处理的语言确实有像RAII那样的情况,BTW(在python中使用语句)。当您需要控制何时释放资源时,RAII非常有用,例如,文件或锁通常就是这种情况。
答案 6 :(得分:7)
不必在缺乏经验的同事代码中追查资源泄漏。
答案 7 :(得分:6)
垃圾收集允许推迟关于谁拥有对象的决定。
C ++使用值语义,因此对于RAII,实际上,当超出范围时会重新收集对象。这有时被称为“立即GC”。
当你的程序开始使用引用语义(通过智能指针等......)时,语言不再支持你,你只需要智能指针库。
关于GC的一个棘手的问题是当不再需要一个对象时决定。
答案 8 :(得分:6)
假设因为C ++没有将烘焙到语言中的垃圾收集,这是一个常见错误,你不能在C ++期间使用垃圾收集。这是无稽之谈。我知道精英C ++程序员在工作中使用Boehm收集器。
答案 9 :(得分:5)
GC的一个属性在某些情况下可能非常重要。指针的赋值在大多数平台上自然是原子的,而创建线程安全的引用计数(“智能”)指针非常困难并且引入了显着的同步开销。因此,智能指针经常在多核架构上被告知“不能很好地扩展”。
答案 10 :(得分:5)
垃圾收集使RCU无锁同步更容易正确有效地实现。
答案 11 :(得分:3)
垃圾收集确实是自动资源管理的基础。让GC改变你以难以量化的方式解决问题的方式。例如,当您进行手动资源管理时,您需要:
在琐碎的情况下,没有复杂性。例如。您在方法的开头打开一个文件,并在最后关闭它。或者调用者必须释放这个返回的内存块。
当您有多个与资源交互的模块时,事情开始变得复杂,并且不清楚谁需要清理。最终结果是解决问题的整个方法包括一些妥协的编程和设计模式。
在具有垃圾收集的语言中,您可以使用disposable模式,在那里您可以释放您已知道已完成的资源,但如果您无法释放它们,那么GC就可以节省一天。
智能指针实际上是我提到的妥协的完美例子。除非您有备份机制,否则智能指针无法避免泄漏循环数据结构。为了避免这个问题,你经常会妥协并避免使用循环结构,即使它可能是最合适的。
答案 12 :(得分:2)
我也怀疑C ++委员会正在为标准添加一个完整的垃圾收集。
但我想说在现代语言中添加/进行垃圾收集的主要原因是反对垃圾收集的原因太少了。自八十年代以来,在内存管理和垃圾收集领域取得了巨大进步,我相信甚至有垃圾收集策略可以为您提供类似软实时的保证(例如,“GC不会超过...... ..在最坏的情况下“)。
答案 13 :(得分:2)
使用带有智能指针的RAII消除了对它的需要,对吗?
智能指针可用于在C ++中实现引用计数,这是垃圾收集(自动内存管理)的一种形式,但生产GC不再使用引用计数,因为它有一些重要的缺陷:
参考计数泄漏周期。考虑A↔B,对象A和B都相互引用,因此它们的引用计数均为1,并且两者都没有被收集,但它们都应该被回收。像trial deletion这样的高级算法解决了这个问题,但增加了很多复杂性。使用weak_ptr
作为解决方法可以追溯到手动内存管理。
由于多种原因,朴素的引用计数很慢。首先,它需要经常缓存超出缓存的引用计数(请参阅Boost's shared_ptr up to 10× slower than OCaml's garbage collection)。其次,在范围结束时注入的析构函数会导致不必要且昂贵的虚函数调用,并禁止优化,例如尾调用消除。
基于范围的引用计数会保持浮动垃圾,因为对象在范围结束前不会被回收,而跟踪GC可以在它们无法访问时立即回收它们,例如:可以在循环期间回收循环之前分配的本地吗?
垃圾收集为经验丰富的C ++开发人员提供了哪些优势?
生产力和可靠性是主要的好处。对于许多应用程序,手动内存管理需要大量的程序员工作。通过模拟无限内存机器,垃圾收集使程序员摆脱了这种负担,使他们能够专注于解决问题并避开一些重要的错误类别(悬空指针,缺少free
,双free
)。此外,垃圾收集有助于其他形式的编程,例如,通过解决upwards funarg problem (1970)。
答案 14 :(得分:2)
在支持GC的框架中,对诸如字符串之类的不可变对象的引用可以以与基元相同的方式传递。考虑类(C#或Java):
public class MaximumItemFinder
{
String maxItemName = "";
int maxItemValue = -2147483647 - 1;
public void AddAnother(int itemValue, String itemName)
{
if (itemValue >= maxItemValue)
{
maxItemValue = itemValue;
maxItemName = itemName;
}
}
public String getMaxItemName() { return maxItemName; }
public int getMaxItemValue() { return maxItemValue; }
}
请注意,此代码永远不必对任何字符串的内容执行任何操作,并且可以简单地将它们视为基元。类似maxItemName = itemName;
的语句可能会生成两条指令:寄存器加载后跟寄存器存储。 MaximumItemFinder
将无法知道AddAnother
的来电者是否会保留对传入字符串的任何引用,并且来电者无法知道MaximumItemFinder
将保留多长时间引用它们。 getMaxItemName
的呼叫者将无法知道MaximumItemFinder
以及返回的字符串的原始供应商是否以及何时放弃了对它的所有引用。因为代码可以像原始值一样简单地传递字符串引用,但这些事情都不重要。
另请注意,虽然上述类在同时调用AddAnother
时不会是线程安全的,但对GetMaxItemName
的任何调用都将保证返回对其中任何一个的有效引用一个空字符串或其中一个已传递给AddAnother
的字符串。如果想要确保最大项目名称与其值之间的任何关系,则需要进行线程同步,但即使在没有的情况下也确保内存安全。
我认为没有办法在C ++中编写如上所述的方法,这样可以在任意多线程使用的情况下保持内存安全,而无需使用线程同步或者要求每个string变量有自己的内容副本,保存在自己的存储空间中,在有问题的变量的生命周期内可能无法释放或重定位。当然不可能定义一个字符串引用类型,它可以像int
一样廉价地定义,分配和传递。
答案 15 :(得分:1)
处理诸如循环引用之类的事情的完全成熟的GC将比重新计数的shared_ptr
稍微升级一些。我会在C ++中欢迎它,但不是在语言层面。
关于C ++的一个优点是它不会强制垃圾收集。
我想纠正一个常见的误解:垃圾收集神话它以某种方式消除了泄漏。根据我的经验,调试其他人编写的代码并尝试发现最昂贵的逻辑泄漏的最糟糕的噩梦涉及通过资源密集型主机应用程序使用嵌入式Python等语言进行垃圾收集。
在谈论像GC这样的科目时,有理论,然后是实践。从理论上讲,这很棒,可以防止泄漏。然而在理论层面上,每种语言都是精彩且无泄漏的,因为从理论上讲,每个人都会编写完全正确的代码并测试每一个可能出现错误的单个代码的情况。
在我们的案例中,垃圾收集与不太理想的团队协作相结合导致了最严重,最难调试的漏洞。
问题仍然与资源所有权有关。当涉及持久性对象时,您必须在此处做出明确的设计决策,垃圾收集使您很容易认为自己没有。
鉴于某些资源R
,在团队环境中,开发人员不会在所有时间内不断地沟通和审核彼此的代码(在我的经验中有点太常见) ,开发人员A
很容易存储该资源的句柄。开发人员B
也可以用一种晦涩的方式将R
间接添加到某些数据结构中。 C
也是如此。在垃圾收集系统中,这创建了3个R
的所有者。
因为开发人员A
是最初创建资源的人并且认为他是该资源的所有者,所以他记得当用户表明他不再是R
时发布对B
的引用想要用它。毕竟,如果他没有这样做,什么都不会发生,从测试中可以明显看出用户端删除逻辑什么也没做。所以他记得发布它,因为任何合理的开发人员都会这样做。这会触发R
处理它的事件,并会记住释放对C
的引用。
然而,C
会忘记。他不是团队中更强大的开发人员之一:一个刚刚在系统中工作了一年的新人。或者他甚至不在团队中,只是一个受欢迎的第三方开发人员为我们的产品编写插件,许多用户都会添加到该软件中。使用垃圾收集,这是我们得到那些无声的逻辑资源泄漏。它们是最糟糕的类型:除了在运行程序的持续时间,内存使用量持续上升和某些神秘的上升这一事实之外,它们并不一定在软件的用户可见方面表现为明显的错误。目的。尝试使用调试器缩小这些问题可能与调试时间敏感的竞争条件一样有趣。
如果没有垃圾回收,开发人员C
就会创建一个悬空指针。他可能会尝试在某些时候访问它并导致软件崩溃。现在,这是一个测试/用户可见的错误。 valgrind
让我感到尴尬并纠正他的错误。在GC场景中,只是试图找出系统泄漏的位置可能非常困难,以至于某些泄漏从未得到纠正。这些不是C
类型的物理泄漏,可以轻松检测并精确定位到特定的代码行。
通过垃圾收集,开发人员R
创建了一个非常神秘的漏洞。他的代码可能会继续访问C
,现在只是软件中的一些不可见实体,此时与用户无关,但仍处于有效状态。由于{{1}}代码会产生更多泄漏,他会在无关资源上创建更多隐藏处理,而且软件不仅会泄漏内存,而且每次都会变得越来越慢。
因此垃圾收集不一定能减轻逻辑资源泄漏。在不太理想的情况下,它可以更容易地使泄漏更容易被忽视并保留在软件中。开发人员可能会非常沮丧地试图追踪他们的GC逻辑泄漏,他们只是告诉用户定期重新启动软件作为解决方法。它确实消除了悬空指针,并且在一个安全痴迷的软件中,在任何情况下崩溃都是完全不可接受的,那么我更喜欢GC。但我经常在安全性较低但资源密集型,性能关键的产品上工作,其中可以立即修复的崩溃比真正模糊和神秘的无声错误更可取,而资源泄漏并非那些微不足道的错误。
在这两种情况下,我们都在谈论没有驻留在堆栈上的持久对象,例如3D软件中的场景图或合成器中可用的视频剪辑或游戏世界中的敌人。当资源将其生命周期与堆栈联系起来时,C ++和任何其他GC语言都会使得正确管理资源变得微不足道。真正的困难在于持久的资源引用其他资源。
在C或C ++中,如果您未能明确指定谁拥有资源以及何时应释放它们的句柄(例如:设置为null以响应事件),则可能会出现由段错误引起的悬空指针和崩溃。然而在GC中,这种响亮且令人讨厌但通常易于发生的崩溃被交换为可能永远无法检测到的无声资源泄漏。