我听说C ++ 14在C ++标准库中引入垃圾收集器。 这个功能背后的理由是什么?这不是RAII存在于C ++中的原因吗?
答案 0 :(得分:30)
垃圾收集和RAII在不同的环境中很有用。 GC的存在不应影响您对RAII的使用。由于RAII是众所周知的,我举两个GC很方便的例子。
垃圾收集对实现无锁数据结构非常有帮助。
事实证明,确定性内存释放是无锁数据结构中的一个基本问题。 (来自Lock-Free Data Structures作者:Andrei Alexandrescu)
基本上问题是你必须确保在线程读取时不释放内存。这就是GC变得方便的地方:它可以查看线程,只在安全时才进行解除分配。请阅读文章了解详情。
这里要明确一点:这并不意味着 WHOLE WORLD 应该像Java一样被垃圾收集;只有相关数据才能准确地进行垃圾收集。
在他的一个演讲中, Bjarne Stroustrup也提供了一个很好的有效示例,其中GC变得很方便。想象一下用C / C ++编写的应用程序,大小为10M SLOC。该应用程序工作得相当好(相当无bug),但它泄漏。你既没有资源(工时)也没有功能知识来解决这个问题。源代码是一个有点混乱的遗留代码。你是做什么?我同意这可能是用GC解决问题的最简单,最便宜的方式。
正如sasha.sochka指出的那样,垃圾收集器将是可选的。
我个人担心的是,人们会开始使用GC,就像在Java中使用它一样,会编写草率的代码,垃圾收集所有内容。 (我的印象是shared_ptr
已成为默认的“转到”,即使在unique_ptr
或者地狱堆栈分配的情况下也是如此。)
答案 1 :(得分:11)
我同意@DeadMG当前C ++标准中没有GC,但我想在B. Stroustrup中添加以下引文:
当(不是)自动垃圾收集成为C ++的一部分时,它 将是可选的
所以Bjarne确信它将来会被添加。至少EWG(进化工作组)的主席和最重要的委员会成员之一(更重要的是语言创建者)想要添加它。
除非他改变了他的观点,否则我们可以预期它将来会被添加和实施。
答案 2 :(得分:11)
有些算法在没有GC的情况下编写复杂/低效/无法编写。我怀疑这是GC在C ++中的主要卖点,并且看不到它被用作通用分配器。
为什么不是通用分配器?
首先,我们有RAII,大多数人(包括我)似乎相信这是一种优越的资源管理方法。我们喜欢确定性,因为它使编写健壮,无泄漏的代码变得更加简单,并使性能可预测。
其次,你需要放置一些非C ++的限制 - 如何使用内存。例如,您至少需要一个可到达的,未混淆的指针。在常见的树容器库中使用的混淆指针(使用对齐保证的低位用于颜色标记)等,GC无法识别。
与此相关的是,如果你支持任何数量的混淆指针,那么使现代GC如此可用的东西将很难应用于C ++。分代碎片整理GC非常酷,因为分配非常便宜(基本上只是增加一个指针),最终你的分配会因为局部性的改进而被压缩成更小的东西。要做到这一点,物体需要移动。
为了使对象安全地移动,GC需要能够更新指向它的所有指针。它将无法找到混淆的。这可以容纳,但不会很漂亮(可能是gc_pin
类型或类似的,像当前std::lock_guard
一样使用,只要你需要一个原始指针就可以使用它。可用性将会出现。
不会让事情变得可动,GC会比你在其他地方习惯的速度慢得多,缩放程度也会低。
可用性原因(资源管理)和效率原因(快速,可移动的分配),GC还有什么好处?当然不是通用的。输入无锁算法。
为什么无锁?
无锁算法的工作原理是让争用操作与数据结构暂时“不同步”,并在以后的步骤中检测/纠正。这样做的一个结果是,在争用内存可能会在删除后被访问。例如,如果您有多个线程竞争从LIFO弹出一个节点,则一个线程可能会弹出并删除该节点,然后另一个线程已意识到该节点已被占用:
主题A:
主题B:
主题A:
主题B:
使用GC可以避免从未提交的内存中读取数据,因为在线程B引用它时,永远不会删除该节点。有一些解决方法,例如hazard pointers或在Windows上捕获SEH异常,但这些可能会严重影响性能。 GC往往是最优化的解决方案。
答案 3 :(得分:6)
没有,因为没有。在C ++ 11中引入了C ++曾用于GC的唯一功能,它们只是标记内存,不需要收集器。在C ++ 14中也不存在。
我的意见是收藏家无法通过委员会。
答案 4 :(得分:4)
我不是说GC是最好/最好的选择。我只是说它有不同的特点。在某些可能有利的情况下。
答案 5 :(得分:4)
到目前为止,没有一个答案涉及将垃圾收集添加到语言中的最重要的好处:在没有语言支持的垃圾收集的情况下,几乎不可能保证在引用它时不会销毁任何对象存在。更糟糕的是,如果发生这样的事情,几乎不可能保证以后尝试使用引用不会最终操纵其他随机对象。
虽然有很多种类的对象可以通过RAII比垃圾收集器更好地管理生命周期,但是让GC管理几乎所有对象,包括那些生命周期由RAII控制的对象,具有相当大的价值。一个对象的析构函数应该杀死该对象并使其无用,但将尸体留在GC后面。因此,对对象的任何引用都将成为对尸体的引用,并且在它(引用)完全不存在之前将保持为对象。只有当所有对尸体的引用都不复存在时,尸体才会这样做。
虽然有一些方法可以在没有固有语言支持的情况下实现垃圾收集器,但是这样的实现要么在任何时候创建或销毁引用时都要通知GC(增加相当大的麻烦和开销),或者冒着GC引用的风险不知道对象是否存在未被引用的对象。编译器对GC的支持消除了这两个问题。
答案 6 :(得分:1)
<强>解释强>
RCB GC:基于参考计数的GC。
MSB GC:基于Mark-Sweep的GC。
快速回答:
应将MSB GC添加到C ++标准中,因为在某些情况下它比RCB GC更方便。
两个说明性示例:
考虑一个初始大小很小的全局缓冲区,任何线程都可以动态扩大其大小,并保持其他线程可以访问旧内容。
实施1(MSB GC版):
int* g_buf = 0;
size_t g_current_buf_size = 1024;
void InitializeGlobalBuffer()
{
g_buf = gcnew int[g_current_buf_size];
}
int GetValueFromGlobalBuffer(size_t index)
{
return g_buf[index];
}
void EnlargeGlobalBufferSize(size_t new_size)
{
if (new_size > g_current_buf_size)
{
auto tmp_buf = gcnew int[new_size];
memcpy(tmp_buf, g_buf, g_current_buf_size * sizeof(int));
std::swap(tmp_buf, g_buf);
}
}
实施2(RCB GC版):
std::shared_ptr<int> g_buf;
size_t g_current_buf_size = 1024;
std::shared_ptr<int> NewBuffer(size_t size)
{
return std::shared_ptr<int>(new int[size], []( int *p ) { delete[] p; });
}
void InitializeGlobalBuffer()
{
g_buf = NewBuffer(g_current_buf_size);
}
int GetValueFromGlobalBuffer(size_t index)
{
return g_buf[index];
}
void EnlargeGlobalBufferSize(size_t new_size)
{
if (new_size > g_current_buf_size)
{
auto tmp_buf = NewBuffer(new_size);
memcpy(tmp_buf, g_buf, g_current_buf_size * sizeof(int));
std::swap(tmp_buf, g_buf);
//
// Now tmp_buf owns the old g_buf, when tmp_buf is destructed,
// the old g_buf will also be deleted.
//
}
}
请注意:
致电std::swap(tmp_buf, g_buf);
后,tmp_buf
拥有旧g_buf
。当tmp_buf
被破坏时,旧的g_buf
也将被删除。
如果另一个帖子正在调用GetValueFromGlobalBuffer(index);
来获取旧g_buf
中的值,那么将会发生种族危险!
因此,尽管实现2看起来像实现1一样优雅,但它不起作用!
如果我们想让实现2正常工作,我们必须添加某种锁机制;那么它不仅比实施1更慢,而且更不优雅。
<强>结论:强>
将MSB GC作为可选功能引入C ++标准是很好的。