我不断听到人们抱怨 C++ 没有垃圾回收功能。我还听说 C++ 标准委员会正在考虑将其添加到该语言中。恐怕我不明白这有什么意义......将 RAII 与智能指针结合使用就不再需要它了,对吧?

我唯一的垃圾收集经验是在几台廉价的八十年代家用计算机上,这意味着系统会时不时地冻结几秒钟。我确信从那时起它已经有所改善,但正如你可以猜到的那样,这并没有让我对它有很高的评价。

垃圾收集可以为经验丰富的 C++ 开发人员提供哪些优势?

有帮助吗?

解决方案

我一直听到人们抱怨C ++没有垃圾收集。

我很抱歉。严重。

C ++有RAII,我总是抱怨在Garbage Collected语言中找不到RAII(或阉割的RAII)。

垃圾收集为经验丰富的C ++开发人员提供了哪些优势?

另一种工具。

Matt J在他的帖子中写得非常正确( C ++中的垃圾收集 - 为什么?):我们不需要C ++功能,因为大多数功能都可以用C编码,我们不需要C功能,因为大多数功能都可以在汇编等编码。 C ++必须发展

作为开发人员:我不关心GC。我尝试了RAII和GC,我发现RAII非常优越。正如Greg Rogers在帖子中所说( C ++中的垃圾收集 - 为什么?),内存泄漏并不是那么糟糕(至少在C ++中,如果真的使用C ++,它们很少见),以证明GC而不是RAII。 GC具有非确定性的释放/终止,只是编写一个不关心特定内存选择的代码的方法

最后一句很重要:编写“juste不关心”的代码非常重要。以同样的方式在C ++ RAII中我们不关心资源释放,因为RAII为我们做这件事,或者为了对象初始化,因为构造函数为我们做了,有时候编写代码而不关心谁是什么内存的所有者是很重要的,以及我们需要这个或这段代码的指针(共享,弱等)。 C ++中似乎需要GC。(即使我个人没有看到它)

在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 ++一样,如果你不需要一个功能,不要使用它会花费你一切。

其他提示

简短的回答是垃圾收集原则上与使用智能指针的RAII非常相似。如果您分配的每一块内存都位于一个对象中,并且该对象仅由智能指针引用,那么您有一些接近垃圾收集的东西(可能更好)。优势来自于不必如此明智地确定每个对象的范围和智能指针,并让运行时为您完成工作。

这个问题似乎类似于“C ++必须为经验丰富的装配开发人员提供什么?指令和子程序消除了对它的需要,对吧?“

随着像valgrind这样的好记忆检查器的出现,我认为垃圾收集作为安全网并没有太多用处“以防万一”。我们忘了解除一些东西 - 特别是因为它在管理除了内存之外的更通用的资源案例方面没有多大帮助(尽管这些不太常见)。此外,在我看到的代码中,显式分配和释放内存(即使使用智能指针)也是相当罕见的,因为容器通常更简单,更好。

但垃圾收集可能会提供潜在的性能优势,特别是如果很多短期对象正在堆分配的话。 GC还可能为新创建的对象提供更好的引用局部性(与堆栈中的对象相当)。

C ++中GC支持的激励因素似乎是lambda编程,匿名函数等。事实证明,lambda库可以在不关心清理的情况下分配内存。普通开发人员的好处是更简单,更可靠,更快速地编译lambda库。

GC还有助于模拟无限内存;您需要删除POD的唯一原因是您需要回收内存。如果您有GC或无限内存,则无需再删除POD。

委员会没有添加垃圾收集,他们正在添加一些功能,以便更安全地实现垃圾收集。只有时间才能证明它们对未来的编译器是否真的有任何影响。具体实现可能有很大差异,但很可能涉及基于可达性的收集,这可能涉及轻微挂起,具体取决于它是如何完成的。

但有一件事是,没有符合标准的垃圾收集器能够调用析构函数 - 只能静默地重用丢失的内存。

垃圾收集为经验丰富的C ++开发人员提供了哪些优势?

不必在缺乏经验的同事代码中追查资源泄漏。

我不明白人们如何争辩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非常有用,例如,文件或锁通常就是这种情况。

假设由于C ++没有将烘焙到语言中的垃圾收集,这是一个常见的错误,你不能在C ++期间使用垃圾收集。这是无稽之谈。我知道精英C ++程序员在工作中使用Boehm收集器。

垃圾收集允许推迟关于谁拥有对象的决定。

C ++使用值语义,因此对于RAII,实际上,当超出范围时会重新收集对象。这有时被称为“立即GC”。

当你的程序开始使用引用语义(通过智能指针等......)时,语言不再支持你,你只需要智能指针库。

关于GC的一个棘手的问题是当不再需要一个对象时决定

垃圾收集使 RCU 无锁同步更容易正确有效地实施。

更简单的线程安全性和可伸缩性

GC的一个属性在某些情况下可能非常重要。指针的分配在大多数平台上自然是原子的,而创建线程安全引用计数(“智能”)指针非常困难并且引入了显着的同步开销。结果,智能指针经常被告知“不能很好地扩展”。关于多核架构。

垃圾收集确实是自动资源管理的基础。GC 以一种难以量化的方式改变了你解决问题的方式。例如,当您进行手动资源管理时,您需要:

  • 考虑何时可以释放某个项目(所有模块/类都已完成它吗?)
  • 考虑谁有责任在准备释放资源时释放该资源(哪个类/模块应该释放该项目?)

在琐碎的情况下,并不存在复杂性。例如。您在方法开始时打开一个文件并在方法结束时关闭它。或者调用者必须释放这个返回的内存块。

当您有多个模块与资源交互并且不清楚谁需要清理时,事情就会很快变得复杂。最终结果是解决问题的整个方法包括某些妥协的编程和设计模式。

在具有垃圾收集功能的语言中,您可以使用 一次性的 在这种模式下,您可以释放您已经使用完毕的资源,但如果您无法释放它们,GC 就会来拯救世界。


智能指针实际上是我提到的妥协的完美例子。除非您有备份机制,否则智能指针无法避免循环数据结构的泄漏。为了避免这个问题,您经常会妥协并避免使用循环结构,即使它可能是最合适的。

我也怀疑C ++委员会正在为标准添加一个完整的垃圾收集。

但我想说在现代语言中添加/进行垃圾收集的主要原因是反对垃圾收集的原因太少了。自八十年代以来,在内存管理和垃圾收集领域取得了一些巨大进步,我相信甚至有垃圾收集策略可以为您提供类似软实时的保证(例如,“GC不会超过。 ......在最坏的情况下“)。

  

使用带有智能指针的RAII消除了对它的需要,对吗?

智能指针可用于在C ++中实现引用计数,这是垃圾收集(自动内存管理)的一种形式,但生产GC不再使用引用计数,因为它有一些重要的缺陷:

  1. 参考计数泄漏周期。考虑A&#8596; B,对象A和B都相互引用,因此它们的引用计数均为1,两者都没有被收集,但它们都应该被回收。 试用删除等高级算法解决了这个问题但添加了很多复杂性。使用 weak_ptr 作为解决方法可以追溯到手动内存管理。

  2. 由于多种原因,朴素的引用计数很慢。首先,它需要经常缓存超出缓存的引用计数(参见 Boost的shared_ptr最多10&#215;慢于OCaml的垃圾收集)。其次,在范围结束时注入的析构函数会导致不必要且昂贵的虚函数调用,并禁止优化,例如尾调用消除。

  3. 基于范围的引用计数会保持浮动垃圾,因为对象在范围结束前不会被回收,而跟踪GC可以在它们无法访问时立即回收它们,例如:可以在循环期间回收循环之前分配的本地吗?

  4.   

    垃圾收集为经验丰富的C ++开发人员提供了哪些优势?

    生产力和可靠性是主要的好处。对于许多应用程序,手动内存管理需要大量的程序员工作。通过模拟无限内存机器,垃圾收集使程序员摆脱了这种负担,使他们能够专注于解决问题并避开一些重要的错误类别(悬空指针,缺少 free ,双 free )。此外,垃圾收集有助于其他形式的编程,例如,通过解决向上funarg问题(1970)。 / p>

在支持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 的调用者是否会保留对传入字符串的任何引用,并且调用者无法知道多长时间< code> MaximumItemFinder 将保留对它们的引用。 getMaxItemName 的调用者将无法知道 MaximumItemFinder 以及返回的字符串的原始供应商是否以及何时放弃了对它的所有引用。因为代码可以像原始值一样简单地传递字符串引用,但这些事情都不重要

另请注意,虽然上述类在同时调用 AddAnother 的情况下不是线程安全的,但 GetMaxItemName 的任何调用都将保证返回对空字符串或已传递给 AddAnother 的字符串之一的有效引用。如果想要确保最大项目名称与其值之间的任何关系,则需要进行线程同步,但即使在没有的情况下也确保内存安全

我认为没有办法在C ++中编写如上所述的方法,它可以在任意多线程使用的情况下保持内存安全,而无需使用线程同步或者要求每个字符串变量都有自己的副本其内容,保存在自己的存储空间中,在有关变量的生命周期内可能无法释放或重新定位。当然不可能定义一个字符串引用类型,它可以像 int 那样廉价地定义,分配和传递。

垃圾收集会使您最糟糕的噩梦泄漏

处理诸如循环引用之类的事情的完全成熟的GC在某种程度上是对重新计算的 shared_ptr 的升级。我会在C ++中欢迎它,但不是在语言层面。

关于C ++的一个优点是它不会强制垃圾收集。

我想纠正一个常见的误解:垃圾收集神话它以某种方式消除了泄漏。根据我的经验,调试其他人编写的代码并尝试发现最昂贵的逻辑泄漏的最糟糕的噩梦涉及通过资源密集型主机应用程序使用嵌入式Python等语言进行垃圾收集。

在谈论像GC这样的主题时,有理论,然后就是实践。从理论上讲,这很棒,可以防止泄漏。然而在理论层面上,每种语言都是精彩且无泄漏的,因为从理论上讲,每个人都会编写完全正确的代码并测试每一个可能出现错误的单个代码的情况。

在我们的案例中,垃圾收集与不太理想的团队协作相结合导致了最严重,最难调试的漏洞。

问题仍然与资源所有权有关。当涉及持久对象时,您必须在此处做出明确的设计决策,而垃圾收集使您很容易认为不这样做。

给定一些资源, R ,在团队环境中,开发人员不会经常仔细地沟通和仔细查看彼此的代码(在我的经验中有点太常见),它变成了开发人员 A 很容易存储该资源的句柄。开发人员 B 也可能以一种不明确的方式将 R 间接添加到某些数据结构中。 C 也是如此。在垃圾收集系统中,这创建了3个 R 的所有者。

因为开发人员 A 是最初创建资源的人并且认为他是它的所有者,所以他记得在用户表明他时发布对 R 的引用不再想使用它。毕竟,如果他没有这样做,什么都不会发生,从测试中可以明显看出用户端删除逻辑什么也没做。所以他记得发布它,因为任何合理的开发人员都会这样做。这会触发 B 处理它的事件,并且还会记住释放对 R 的引用。

然而, C 会忘记。他不是团队中更强大的开发人员之一:一个刚刚在系统中工作了一年的新人。或许他甚至不在团队中,只是一个受欢迎的第三方开发人员为我们的产品编写插件,许多用户都将其添加到软件中。使用垃圾收集,这是我们得到那些无声的逻辑资源泄漏。它们是最糟糕的类型:除了在运行程序的持续时间内,内存使用量继续上升并且出于某种神秘目的而上升这一事实之外,它们并不一定表现在软件的用户可见方面作为明显的错误。尝试使用调试器缩小这些问题可能与调试时间敏感的竞争条件一样有趣。

没有垃圾收集,开发人员 C 就会创建一个悬空指针。他可能会尝试在某些时候访问它并导致软件崩溃。现在这是一个测试/用户可见的错误。 C 有点尴尬并纠正他的错误。在GC场景中,只是试图找出系统泄漏的位置可能非常困难,以至于某些泄漏从未得到纠正。这些不是 valgrind 类型的物理泄漏,可以轻松检测并精确定位到特定的代码行。

使用垃圾收集,开发人员

许可以下: CC-BY-SA归因
不隶属于 StackOverflow
scroll top