Я все время слышу, как люди жалуются, что в C++ нет сборки мусора. Я также слышал, что Комитет по стандартам C++ рассматривает возможность добавления его в язык. Боюсь, я просто не вижу в этом смысла ... использование RAII с интеллектуальными указателями устраняет необходимость в этом, верно?
Мой единственный опыт работы со сборкой мусора был на паре дешевых домашних компьютеров восьмидесятых годов, где это означало, что система время от времени зависала на несколько секунд. Я уверен, что с тех пор он улучшился, но, как вы можете догадаться, это не оставило меня высокого мнения о нем.
Какие преимущества может предложить сборка мусора опытному разработчику C++?
Это мощная идиома C++ и хорошо известный термин в мире C++. Если вы не знаете, я бы предложил задать вопрос (или поискать, возможно, он уже есть).
Он означает, что если вы строго объектно-ориентированы, вы можете полагаться на то, что delete [] будет вызываться для вашего объекта, когда он выпадает из области видимости или на него больше нет ссылок, что должно освободить () любую память и ресурсы для объект держался.





Комитет не добавляет сборку мусора, они добавляют несколько функций, которые позволяют более безопасно реализовать сборку мусора. Только время покажет, действительно ли они повлияют на будущие компиляторы. Конкретные реализации могут сильно различаться, но, скорее всего, будут включать сбор на основе достижимости, что может привести к небольшому зависанию, в зависимости от того, как это сделано.
Однако одна вещь заключается в том, что ни один совместимый со стандартами сборщик мусора не сможет вызывать деструкторы - только для того, чтобы незаметно повторно использовать потерянную память.
Вы правы, они не «добавляют сборку мусора». Я неправильно прочитал статью.
«Пара функций» верна, C++ не будет использовать полагаться и не произойдет, если не используется (абстракция с нулевой стоимостью). Страуструп сказал (ссылка со страницы в Википедии) сказал My view can be summarized as "C++ is such a good garbage-collected language because it creates so little garbage that needs to be collected".
Короткий ответ заключается в том, что сборка мусора в принципе очень похожа на RAII с интеллектуальными указателями. Если каждый фрагмент памяти, который вы когда-либо выделяли, находится внутри объекта, и на этот объект ссылаются только интеллектуальные указатели, у вас есть что-то близкое к сборке мусора (потенциально лучше). Преимущество заключается в том, что вам не нужно так рассудительно оценивать и умно указывать каждый объект, а также позволять среде выполнения делать всю работу за вас.
Этот вопрос кажется аналогом «что C++ может предложить опытному разработчику сборки? Инструкции и подпрограммы устраняют необходимость в этом, не так ли?»
Рад, что он был воспринят в том духе, в котором был задуман. Я сам склоняюсь к более ручным методам :-)
Если вы используете интеллектуальные указатели с подсчетом ссылок, остерегайтесь циклов ссылок. Одним из преимуществ сборки мусора является то, что ее не путают циклы ссылок.
Если вы правильно используете boost :: weak_ptr, циклы ссылок не будут проблемой.
за исключением того, что интеллектуальные указатели освобождают ресурсы в тот момент, когда они выходят за пределы области видимости, сборщик мусора может заставить их оставаться на связи целую вечность. Это может иметь большое значение.
@Head Geek - если вы правильно используете язык ассемблера и т.д. (см. Последний абзац ответа Мэтта Дж.).
@gbjbaanb: «в тот момент, когда они выйдут из поля зрения». И наоборот, сборщики мусора могут собирать и собирают значения до того, как они выйдут за пределы области действия, тогда как интеллектуальные указатели с подсчетом ссылок могут сохранять значения до конца своей области действия.
«Если каждый фрагмент памяти, который вы когда-либо выделяли, находится внутри объекта, и на этот объект ссылаются только интеллектуальные указатели, у вас есть что-то близкое к сборке мусора (потенциально лучше)». На самом деле то, что у вас есть буквально, представляет собой форму сборки мусора, хотя и примерно в 1960 году. Наивный подсчет ссылок на основе области видимости примерно в 10 раз медленнее, чем трассирующий сборщик мусора, но в C++ вы можете вручную отказаться от некоторых вычислительных усилий, чтобы сократить разрыв в производительности . flyingfrogblog.blogspot.co.uk/2011/01/…
С появлением хороших средств проверки памяти, таких как valgrind, я не вижу особого смысла в сборке мусора в качестве подстраховки на случай, если мы что-то забыли освободить - тем более, что это не очень помогает в управлении более общим случаем ресурсов. кроме памяти (хотя они встречаются гораздо реже). Кроме того, явное выделение и освобождение памяти (даже с помощью интеллектуальных указателей) довольно редко встречается в коде, который я видел, поскольку контейнеры обычно намного проще и лучше.
Но сборка мусора может потенциально повысить производительность, особенно если в куче выделяется много недолговечных объектов. GC также потенциально предлагает лучшую локальность ссылок для вновь созданных объектов (сравнимых с объектами в стеке).
Грег, не могли бы вы немного расширить свой последний абзац? Мне казалось, что это задача любого распределителя памяти, даже malloc, а не только сборщиков мусора (которые, по сути, выясняют, когда вызывать free () за вас). Но я не профессионал в этом вопросе, хотелось бы более подробного объяснения.
Одно большое потенциальное преимущество gc в производительности заключается в том, что вы можете выделить / освободить за один проход вместо множества. Это действительно зависит от ситуации: в некоторых средах ручное выделение памяти или RAII с настраиваемыми распределителями может быть проще, чем gc.
У меня тоже есть сомнения, что комитет по C++ добавляет к стандарту полноценную сборку мусора.
Но я бы сказал, что основная причина добавления / использования сборки мусора на современном языке заключается в том, что существует слишком мало веских причин для сборки мусора против. С восьмидесятых годов произошло несколько значительных достижений в области управления памятью и сборки мусора, и я считаю, что существуют даже стратегии сборки мусора, которые могут дать вам гарантии, подобные программному реальному времени (например, «GC не займет больше, чем ... .. в худшем случае »).
Аргумент реального времени в любом случае спорный, потому что malloc / free также не имеют гарантии наихудшего случая.
Я думаю, вы делаете взаимоисключающие предположения. Есть несколько аргументов против современных сборщиков мусора, но они требуют, чтобы скомпилированный код и сборщик мусора работали в идеальной гармонии, что практически невозможно с таким языком, как C++.
Мне их очень жаль. Серьезно.
В C++ есть RAII, и я всегда жалуюсь, что не нахожу RAII (или кастрированный RAII) в языках со сбором мусора.
Другой инструмент.
Мэтт Дж. Написал это совершенно правильно в своем сообщении (Сборка мусора в C++ - почему?): Нам не нужны функции C++, поскольку большинство из них может быть написано на C, и нам не нужны функции C, так как большинство из них могут быть закодированы на Assembly и т. д. C++ должен развиваться.
Как разработчик: я не забочусь о GC. Я пробовал и RAII, и GC, и считаю, что RAII намного лучше. Как сказал Грег Роджерс в своем посте (Сборка мусора в C++ - почему?), утечки памяти не настолько ужасны (по крайней мере, в C++, где они редки, если C++ действительно используется), чтобы оправдать GC вместо RAII. GC имеет недетерминированное освобождение / завершение и является просто способом напишите код, который просто не заботится о конкретном выборе памяти.
Это последнее предложение важно: важно писать код, который «не волнует». Точно так же в 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++ GC, который не будет мешать C++: Как всегда в C++, если вам не нужна функция, не используйте ее, и она вам ничего не будет стоить.
Единственное, что я надеюсь, что мы не получим, - это объекты Phoenix (похожие на Java). Если финализатор может снова оживить объект. Но во второй раз, когда выполняется сборка мусора, финализатор не запускается.
Как я понял, C++ 09 будет сборщиком мусора способствовать.
Теперь из Википедии: en.wikipedia.org/wiki/C%2B%2B0x#Transparent_garbage_collecti в: «Полная поддержка сборки мусора была возвращена в более позднюю версию стандарта или Технический отчет». Думаю, ты прав. :-)
RAII C++ ограничен и может быть лучше.
xtofl: Как я понял, новые стандарты C++ потребуют, чтобы C++ облегчал сборку мусора / for_memory_alone / - в C++ практически ничего не меняется, за исключением того, что память для объекта фактически не освобождается.
@Ctrl Alt D-1337: Не могли бы вы привести нам несколько примеров «RAII C++ ограничен и может быть лучше»? Есть ли язык с лучшим RAII?
@paercebal - try / finally иногда бывает полезно (но теперь его можно смоделировать с помощью лямбда-выражений), но кроме этого RAII C++ было бы трудно улучшить.
Более того, вам нужны как RAII, так и GC. Одно не исключает другого. Большинство языков с встроенным сборщиком мусора с самого начала также имеют идиомы, подобные RAII. И вы можете подумать, что у вас нет необходимость GC, но кто честно откажется от большего удобства и более высокой производительности, если они доступны? Pervasive GC заставляет вас проектировать и кодировать по-другому, и ваша продуктивность повышается. Еще одно преимущество, которое часто игнорируется, заключается в том, что он также работает лучше!
@Earwicker: Основные языки с GC, которые я знаю (т. Е. Не скриптовые, не нишевые языки), - это Java и C#. В Java вообще нет RAII, а RAII в C# далек от удовлетворительного, если исходить из C++. Тем не менее, у нас есть общая точка зрения: если мы можем себе это позволить, работа на языке, в котором выделение памяти выполняется в фоновом режиме, экономит много времени.
@Earwicker: Вы правы насчет try / finally: иногда мы хотим, чтобы код выполнялся независимо от того, как мы выходим из области видимости, и писать локальную структуру только для того, чтобы ее деструктор выполнял очистку, болезненно. Другое решение - использовать Boost.ScopedExit в boost.org/doc/libs/1_39_0/libs/scope_exit/doc/html/index.htm l ...
@paercebal: FWIW, деревья по определению не могут иметь циклов. Возможно, вы имели в виду «граф» над «деревом». В противном случае хороший ответ
@ Томас Эдинг: Нет, я имею в виду Дерево. (XML) DOM - это структура, подобная дереву, но каждый узел имеет (обычно), помимо списка указателей на его дочерние узлы, указатель на его родительский узел (и, возможно, даже указатель на его узел документа). Это означает, что два узла всегда имеют циклическое отношение. С этим можно справиться либо с помощью классов владельцев для каждого корня (документа?), Либо с помощью комбинации shared_ptr / weak_ptr, либо с помощью GC, что означает, что вы можете удерживать часть или всю DOM, не заботясь о том, какая часть должна быть уничтожена или нет, только установив некоторые указатели на ноль ...
@Thomas Eding: ... Проблема, которая возникает с GC, конечно, заключается в том, что вы держите один крошечный узел DOM, не осознавая, что к нему прикреплено все дерево. Это своего рода утечка. И затем, если по какой-то причине какая-то скрытая часть вашего кода содержит указатель на этот крошечный узел (например, какой-то прослушиватель событий, такой как делегат в C# или анонимный внутренний класс в Java), тогда у вас действительно есть утечка, независимо от GC ... В общем, у всех видов обработки памяти есть свои проблемы ... Хорошая вещь в наличии GC в C++ была бы иметь выбор обработки памяти ... :-)
D - это язык, на котором у вас есть RAII и GC.
Python имеет GC и RAII. Python реализует RAII с диспетчерами контекста, который представляет собой профессиональный протокол, обеспечивающий детерминированное выполнение кода настройки и teardon.
Более простой пример «плавающих» данных - строка. В .NET или Java передача строки не дороже, чем передача целого числа или числа с плавающей запятой, и не требует ни копирования данных, ни синхронизации памяти. Есть ли способ разработать строковый класс на C++, который был бы настолько же эффективным в многопоточных сценариях?
@supercat: используя "const std :: string &"? Если вы не изменяете std :: string, передача константной ссылки в порядке, не требует ни копирования, ни синхронизации ... И если вы хотите изменить ее значение, вы генерируете другую строку на всех языках (с разными затратами) . И если вы хотите его изменить (добавить данные, изменить символ), то версия C++ будет более эффективной. В конце концов, я не уверен, что неизменяемые данные - хороший повод для оправдания сборки мусора. Я что-то пропустил?
@paercebal: Рассмотрим класс class moof { String cache; public String q(int a, String st) { if (trickyFunction(a) cache = b; else b=cache; return b;}. На языке GC этому методу никогда не нужно копировать (или делать что-либо с) содержимое какой-либо строки, и его можно использовать для строк, которые также используются в других потоках, без необходимости синхронизации памяти. Можно ли написать версию на C++ с обоими этими чертами?
@paercebal: неизменяемые ссылочные типы «работают» медленнее, чем изменяемые типы, поскольку каждая операция требует создания нового объекта. С другой стороны, неизменяемые типы в системе сборки мусора могут передаваться так же дешево, как и примитивы. Во многих случаях, когда объект, однажды созданный, будет передаваться много без изменений, эффективным шаблоном является использование изменяемого объекта для создания состояния объекта, а затем создание неизменяемого объекта, инкапсулирующего это состояние. Этот шаблон заканчивается "выигрышем", если объект передается три или более раз, и может быть огромным выигрышем ...
... если часто передают большой объект. Я думаю, что этот шаблон редко встречается в C++, потому что C++ не может эффективно справиться с ним, но я думаю, что в языках GC он иногда может работать намного эффективнее, чем все, что можно было бы сделать без GC.
@paercebal: Я только что добавил ответ с более реалистичным примером использования.
Сборка мусора действительно является основой автоматического управления ресурсами. Кроме того, сборщик мусора меняет способ решения проблем таким образом, что его трудно измерить количественно. Например, когда вы выполняете управление ресурсами вручную, вам необходимо:
В тривиальном случае сложности нет. Например. вы открываете файл в начале метода и закрываете его в конце. Или вызывающий должен освободить этот возвращенный блок памяти.
Все начинает быстро усложняться, когда у вас есть несколько модулей, которые взаимодействуют с ресурсом, и не так ясно, кому нужно убирать. Конечным результатом является то, что весь подход к решению проблемы включает определенные шаблоны программирования и проектирования, которые являются компромиссом.
В языках, в которых есть сборка мусора, вы можете использовать шаблон одноразовый, где вы можете освобождать ресурсы, с которыми, как вы знаете, закончили, но если вы не можете освободить их, GC будет там, чтобы спасти положение.
Умные указатели, которые на самом деле являются прекрасным примером упомянутых мною компромиссов. Умные указатели не могут спасти вас от утечки циклических структур данных, если у вас нет механизма резервного копирования. Чтобы избежать этой проблемы, вы часто идете на компромисс и избегаете использования циклической структуры, даже если в противном случае она может быть лучше всего подходящей.
Проблема в том, что одноразовая выкройка спасет не во всех случаях. В C# шаблон «одноразового использования» сложно реализовать правильно (поскольку финализатор может вызываться несколько раз из разных потоков и т. д.), А в Java шаблон «одноразового использования» - это шутка.
И снова, правильное использование интеллектуальных указателей устраняет обе упомянутые вами проблемы.
Правильное использование Boost :: weak_ptr также может устранить проблемы с циклическими структурами данных. Это требует полного понимания того, как работает ваш код, но вы действительно должны иметь такое понимание в любом случае.
@Head Geek: Иногда вы просто не хотите заботиться о какой-то части вашего кода, точно так же, как вы просто не хотите заботиться о том, как std :: string выделяет / освобождает свою внутреннюю строку. Вы хотите, чтобы данные оставались там, пока вы их используете, как бы то ни было, и очищались, когда больше не используются.
Наконец, разработчик класса должен решить, как его освободить, а не попросить пользователя проверить реализацию / документацию класса.
@HeadGeek "правильное использование умных указателей". Не существует такой вещи, как «правильное использование» интеллектуальных указателей. Рассмотрим проблему представления изменяемого графа как структуры данных и автоматического восстановления недостижимых подграфов. Не существует «правильного» способа решить эту проблему с помощью интеллектуальных указателей.
Сборка мусора позволяет откладывать принимать решение о том, кто является владеет объектом.
C++ использует семантику значений, поэтому с RAII действительно объекты вспоминаются при выходе из области видимости. Иногда это называют «немедленной сборкой мусора».
Когда ваша программа начинает использовать ссылочную семантику (с помощью интеллектуальных указателей и т. д.), Язык больше не поддерживает вас, вы оставлены наедине с вашей библиотекой интеллектуальных указателей.
Сложность в сборке мусора заключается в том, что когда больше не нужен.
Умные указатели полностью избавляют от необходимости решать, кому принадлежит объект.
@Head Geek: Не совсем так. Если у вас есть 2 объекта, A и B, на которые указывают умные указатели, вы правы. Теперь, если A указывает также на B, и если B указывает также на A, тогда у вас есть проблема, и вы должны решить, кому принадлежит объект, с помощью weak_ptr и / или shared_ptr.
-1 Вы увековечиваете миф о том, что объекты становятся недоступными после того, как выходят из поля зрения.
@JonHarrop: Я 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 и который вы вообще не контролируете (конкретный пример: расширения mex в Matlab). По той же причине, по которой наконец, в python что-то не выполняется. Gc может вам помочь в этом случае.
Другие идиомы не подходят для этого: в любой нетривиальной программе вам понадобятся объекты с отслеживанием состояния (здесь я использую слово «объект» в очень широком смысле, это может быть любая конструкция, разрешенная языком); если вам нужно управлять состоянием вне одной функции, вы не сможете легко это сделать с RAII (вот почему RAII не так полезен для асинхронного программирования). OTOH, gc имеют представление обо всей памяти вашего процесса, то есть он знает обо всех выделенных им объектах и может выполнять очистку асинхронно.
Также может быть намного быстрее использовать gc по тем же причинам: если вам нужно выделить / освободить много объектов (в частности, небольшие объекты), gc значительно превзойдет RAII, если вы не напишете собственный распределитель, поскольку gc может выделять / очистить много объектов за один проход. Некоторые хорошо известные проекты C++ используют gc даже там, где важна производительность (см., Например, Тим Суини об использовании gc в Unreal Tournament: http://lambda-the-ultimate.org/node/1277). GC в основном увеличивает пропускную способность за счет задержки.
Конечно, есть случаи, когда RAII лучше, чем gc; в частности, концепция gc в основном связана с памятью, и это не единственный ресурс. Такие вещи, как файл и т. д., Можно хорошо обрабатывать с помощью RAII. Языки без обработки памяти, такие как python или ruby, имеют что-то вроде RAII для этих случаев, BTW (с выражением в python). RAII очень полезен, когда вам нужно точно контролировать, когда ресурс освобождается, и это довольно часто бывает, например, с файлами или блокировками.
«gc видит весь ваш процесс» - 99,99 из которых НЕ являются указателем на ресурс X. Вот почему RAII хорош; он статически ограничивает количество мест, где могут скрываться соответствующие указатели. RAII также позволяет вам напрямую управлять управлением памятью - просто назначьте NULL интеллектуальному указателю.
Вы неправильно понимаете, что я имею в виду: тот факт, что gc может просматривать всю память процесса и не ограничен по объему, означает, что он может освобождать память асинхронно и освобождать несколько объектов «одновременно» (за один проход). Тот факт, что RAII статически ограничивает объем ресурсов, является не только функцией, но и проблемой.
Ближе к правде сказать, что во многих случаях RAII может справиться с тем, что не может выполнить сборка мусора. GC концентрируется на памяти; RAII обрабатывает любые ресурсы. И, насколько я могу судить, умные указатели устраняют ваш аргумент о «хрупкости».
Если вы оберните свои ресурсы в объекты, которые можно собирать, тогда GC также сможет управлять ресурсами. Если вы подкрепите это одноразовым шаблоном, вы сможете освободить ресурсы, с которыми, как вы знаете, закончили.
@Head geek: например, как умный указатель обрабатывает сигналы? Что касается памяти, в большинстве случаев gc намного лучше, чем RAII. Почти каждый язык высокого уровня использует gc, я думаю, это о чем-то говорит.
@Quinane: в зависимости от ресурсов вам нужно детерминированное освобождение. Как правило, для файлов (или блокировок; хотя я думаю, что RAII плохо работает и для потоков), вы хотите точно контролировать, когда вы освобождаете ресурс.
Я добавил пример использования сигнала, в котором ни интеллектуальный указатель, ни RAII не освобождают ресурс.
@cournape: Мне очень жаль, но этот пример кажется довольно надуманным. Вызов exit () в обработчике сигнала также не позволит сборщику мусора что-либо очистить.
И RAII довольно хорошо работает с резьбовыми замками. Я использую его по этой причине регулярно; Фактически, именно эта ситуация познакомила меня с RAII как концепцией.
@head geek: пример показывает, что RAII может дать сбой, и это его единственная цель. Конечно, вы никогда не стали бы использовать это в реальном коде. Но не возвращаться к вызываемому после того, как sigint происходит в реальном коде: подумайте о том, что ваш код вызывается другим кодом, который вы вообще не можете контролировать и который сам обрабатывает sigint
RAII может помочь с блокировкой потоков, я согласен, но это не панацея. Думаю, меня действительно беспокоит идея, что RAII - это чудо-решение, которое волшебным образом предотвращает взаимоблокировки, утечку памяти и т. д. Это определенно полезно, но это не волшебная палочка.
У RAII, без сомнения, есть свои недостатки. Но я не считаю, что сборщик мусора решает их, он просто меняет один набор недостатков на другой.
Да, у них есть разные недостатки, это называется компромиссом :) Но gc решает проблемы, которые RAII не может решить (сохраняемость вне области видимости, как в моем примере, предполагая, что он сразу делает что-то еще, кроме захватывающего), это очень полезный инструмент. Я не уверен, что это было бы так полезно для C++.
хм, ваш пример так же плох с GC, за исключением GC, даже если объект был очищен при выходе, он все равно не получит вызов своего финализатора (поскольку поток финализации запускается во второй раз, когда его собирают).
Отсутствие необходимости выявлять утечки ресурсов в коде менее опытных коллег.
Утечки ресурсов просто не может произойти, если вы настаиваете на том, чтобы все использовали RAII и интеллектуальные указатели.
Но установление и соблюдение этих правил имеет свою цену, и наличие их в качестве руководящих принципов не означает, что они всегда будут соблюдаться.
И в любом случае утечка памяти при использовании RAII и интеллектуального указателя довольно легко. Например, обработчик сигнала, который изменяет путь кода и никогда не возвращается вызываемому: ни RAII, ни интеллектуальный указатель вам в этом случае не помогут.
Приобретение ресурсов - инициализация: en.wikipedia.org/wiki/Resource_acquisition_is_initialization
Как бы то ни было, даже такой умный сборщик мусора, как .NET, не может предотвратить все утечки ресурсов.
Я не считаю, что сборщик мусора помогает с ресурсами. Самая большая вещь, с которой он помогает, - это возможность использования ссылок на неизменяемые объекты в качестве прокси для их содержимого, без необходимости рассматривать содержимое как ресурс.
Фактором, побуждающим к поддержке сборки мусора в C++, является лямбда-программирование, анонимные функции и т. д. Оказывается, что лямбда-библиотеки выигрывают от способности выделять память, не заботясь об очистке. Преимущество для обычных разработчиков будет заключаться в более простой, надежной и быстрой компиляции лямбда-библиотек.
GC также помогает имитировать бесконечную память; Единственная причина, по которой вам нужно удалять POD, состоит в том, что вам нужно повторно использовать память. Если у вас есть сборщик мусора или бесконечная память, больше нет необходимости удалять POD.
Другими словами, это просто костыль для неопытных программистов? :-)
Только если рассматривать функциональное программирование как что-то для неопытных программистов, да. gc - чрезвычайно мощный инструмент, за который приходится платить: как и все мощные абстракции, он позволяет сосредоточиться на проблеме, но иногда он ломается, и вам приходится опускаться ниже абстракции.
+1 за первый правильный ответ, который я видел до сих пор. Технический термин, который вы ищете, - это «проблема восходящего потока», появившийся почти полвека назад. dl.acm.org/citation.cfm?id=1093411
Распространенной ошибкой является предположение, что, поскольку C++ не имеет сборки мусора запеченный на языке, вы не можете использовать сборку мусора в период C++. Это нонсенс. Я знаю элитных программистов на C++, которые, естественно, используют в своей работе сборщик Boehm.
Да, я видел несколько дополнительных библиотек для сборки мусора. Я просто не понимаю, почему они необходимы или желательны в большинстве случаев. Ответы на этот вопрос дали мне (очень) несколько случаев, когда это было бы желательно, поэтому я и спросил.
Есть одно свойство GC, которое может быть очень важным в некоторых сценариях. Назначение указателя, естественно, атомарно на большинстве платформ, в то время как создание поточно-безопасных указателей с подсчетом ссылок («умных») довольно сложно и приводит к значительным издержкам синхронизации. В результате интеллектуальным указателям часто говорят, что они «плохо масштабируются» в многоядерной архитектуре.
Это верный момент, хотя я бы обычно не беспокоился о нем. Когда я занимаюсь многопоточным программированием, потоки редко делятся своими структурами данных.
Технически подсчет ссылок масштабируется лучше, чем другие алгоритмы сборки мусора, в асимптоте бесконечного размера кучи.
Сборка мусора значительно упрощает правильную и эффективную реализацию синхронизации без блокировки RCU.
На самом деле, множество структур данных и алгоритмов без блокировок и без ожидания намного проще реализовать с помощью автоматического управления памятью. Но, возможно, это бессмысленно, если распределитель не заблокирован или не ожидает, что всегда не так ...
@JonHarrop: только если вы предполагаете, что один распределитель используется для всех потоков, что становится узким местом. Большинство высокопроизводительных схем распределения, хотя и могут иметь блокировку, используют детализированную блокировку. И до тех пор, пока на горячем пути вашего алгоритма без блокировки / ожидания нет распределения, это в любом случае не имеет большого значения.
using RAII with smart pointers eliminates the need for it, right?
Интеллектуальные указатели могут использоваться для реализации подсчета ссылок в C++, который является формой сборки мусора (автоматическое управление памятью), но производственные сборщики мусора больше не используют подсчет ссылок, поскольку у него есть некоторые важные недостатки:
Циклы подсчета ссылок на утечки. Рассмотрим A↔B, оба объекта A и B ссылаются друг на друга, поэтому они оба имеют счетчик ссылок 1, и ни один из них не собирается, но оба они должны быть возвращены. Продвинутые алгоритмы, такие как пробное удаление, решают эту проблему, но добавляют много сложности. Использование weak_ptr в качестве обходного пути означает возврат к ручному управлению памятью.
Наивный подсчет ссылок выполняется медленно по нескольким причинам. Во-первых, это требует частого увеличения количества ссылок вне кэша (см. Увеличить shared_ptr до 10 раз медленнее, чем сборка мусора OCaml). Во-вторых, деструкторы, внедренные в конце области видимости, могут вызывать ненужные и дорогостоящие вызовы виртуальных функций и препятствовать оптимизации, такой как устранение хвостовых вызовов.
Подсчет ссылок на основе области сохраняет плавающий мусор, поскольку объекты не перерабатываются до конца области действия, тогда как сборщики мусора при трассировке могут восстанавливать их, как только они становятся недоступными, например может ли локальный объект, выделенный до цикла, быть восстановлен во время цикла?
What advantages could garbage collection offer an experienced C++ developer?
Производительность и надежность - главные преимущества. Для многих приложений ручное управление памятью требует значительных усилий программиста. Моделируя машину с бесконечной памятью, сборка мусора освобождает программиста от этого бремени, что позволяет ему сосредоточиться на решении проблем и избегать некоторых важных классов ошибок (висячие указатели, отсутствие free, двойной free). Кроме того, сборка мусора облегчает другие формы программирования, например путем решения восходящая задача Funarg (1970).
Я не думаю, что (3) проблема. Что касается цикла: да, область видимости заканчивается в конце цикла, поэтому хранилище переменных, объявленных внутри цикла, не накапливается. На практике я считаю, что управление сроком службы на основе области видимости в высшей степени практично и прекрасно работает в большинстве контекстов. И да, в некоторых случаях (восходящие сообщения, сложный многопоточный обмен данными…).
Никакого рассмотрения unique_ptr -> сбой. Ваш пример vcalls в конце области видимости - это специфическая вещь boost :: shared_ptr, это совсем не обязательно. Кроме того, сборщики мусора хранят мусор намного больше, а не RAII. Не говоря уже о том, что GC имеет некоторые очень серьезные недостатки, такие как недетерминированное разрушение ресурсов, невозможность использования с памятью / ресурсами, отличными от GC, и т. д. Кроме того, ваш пост вводит в заблуждение, потому что он подразумевает, что пользователи RAII сталкиваются с висячими указателями , утечки памяти и двойные удаления, чего не происходит. Короче, ты плохой и неправ.
@DeadMG: "особенная вещь boost :: shared_ptr". Нет, это относится к виртуальным деструкторам. «Сборщики мусора хранят намного больше мусора, чем RAII». Я измерил требования к памяти для движка векторной графики, написанного на C++ и OCaml, и обнаружил, что C++ требует в 5 раз больше памяти, чем OCaml, собирающий мусор, из-за этой проблемы со сборкой на основе области видимости. «Пользователи RAII сталкиваются с висячими указателями, утечками памяти и двойными удалениями, чего у них нет». Использование unique_ptr, когда должно быть два владельца, может оставить висячие указатели. Ссылка подсчета утечек. Нить небезопасного shared_ptr можно дважды удалить.
@JonHarrop: shared_ptr не подразумевает и не требует виртуального деструктора. Использование стирания типа - выбор пользователя. Анекдот из вашей памяти - это логическая ошибка (как ни странно, анекдот). Тривиально очевидно, что у сборщика мусора всегда будет больше мусора, чем у системы, которая освобождает его в тот момент, когда на него нельзя ссылаться. Нет никаких значимых реализаций shared_ptr, не ориентированных на многопоточность, интерфейс unique_ptr не допускает двойного владения, если вы не очень стараетесь, и, честно говоря, циклы ref никогда не возникают.
«Тривиально очевидно, что у сборщика мусора всегда будет больше мусора, чем у системы, которая освобождает его в тот момент, когда на него нельзя ссылаться». Вы увековечиваете распространенный миф об управлении памятью. Подсчет ссылок на основе области действия не освобождает на самом раннем этапе. «Нет никаких значимых реализаций shared_ptr, не поддерживающих потоки». См. BOOST_SP_DISABLE_THREADS. "циклы реф. никогда не появляются". Потому что вы приносите жертвы, чтобы приспособиться к плохим стратегиям управления памятью. Циклы повсеместны на JVM и .NET.
В среде, поддерживающей сборщик мусора, ссылка на неизменяемый объект, например на строку, может передаваться так же, как и на примитив. Рассмотрим класс (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++, который поддерживал бы безопасность памяти при произвольном многопоточном использовании, не используя ни синхронизацию потоков, ни требуя, чтобы каждая строковая переменная имела свою собственную копию своего содержимого. , хранится в собственном пространстве хранения, которое не может быть освобождено или перемещено в течение времени существования рассматриваемой переменной. Конечно, было бы невозможно определить тип строковой ссылки, который можно было бы определять, назначать и передавать так же дешево, как int.
Мне нравится пример. Кстати, вы уверены, что присвоение ссылок атомарно? (Примечание: после небольшого исследования это кажется таким же в C#, и я предполагаю, что это также и в Java: чтобы ваш пример работал на C++, нужно было бы также сделать присвоение указателей атомарным, либо на языке - Я так не думаю - или с помощью std :: atomic)
@paercebal: Назначение ссылок в Java и .NET так же атомарно, как и назначение int [т.е. a=b выполняет атомарное чтение a и атомарную запись b, хотя операция в целом может не быть атомарной]. Ключевое требование, которому указатели C++ не могут удовлетворить, заключается в том, что сборщик мусора должен знать о регистре, в который считывается b во время присваивания (если поток, выполняющий присваивание, отключается на некоторое время, в течение которого b перезаписывается и становится необходим цикл сборщика мусора. , регистр этого потока может быть единственной ссылкой на b где-нибудь во вселенной).
@paercebal: Единственный способ увидеть, что код, подобный приведенному выше, практичен и эффективен в C++ с объектами, слишком большими для эффективного копирования, - это если у класса GCreference было бы некоторое хранилище с быстрым доступом для каждого потока; в этом сценарии a=b может транслироваться в currentThreadTempRef=b.data; a.data=currentThreadTempRef; currentThreadTempRef=null;, и если сборщик мусора сработает в любой точке этого процесса, он сможет просмотреть currentThreadTempRef каждого потока и узнать, что идентифицированные таким образом объекты должны быть закреплены.
Это предполагает, что само присваивание является атомарным. Например, предположим, что один поток выполняет b = c ;, а другой одновременно выполняет a = b ;, причем b является необработанным указателем, совместно используемым двумя потоками. В этот момент b мог быть наполовину записан потоком 1, полностью прочитан потоком 2, а последний наполовину записан потоком 1 (я не предполагаю, что у нас автоматически есть атомарное присвоение указателям в стандартном переносимом C++). Это означает, что поток 2 имеет неверное значение указателя. Я прав?
@paercebal: Вы правы, что помимо предоставления формы локального хранилища потока, платформа должна также гарантировать, что запись указателя, за которой следует чтение, всегда будет видеть либо старое, либо новое значение; кроме того, если кто-то хочет использовать «обычные» хранилища, он должен иметь средство, с помощью которого GC мог бы заставить другие потоки очищать свои кеши и добавить проверку if (gcBusy) gcWait(); в процесс присвоения ссылок. Если предположить, что GC имеет эти полномочия, требование назначения указателя без разделения на сегменты является незначительным по сравнению.
@paercebal: В любом случае, моя основная мысль заключается в том, что приведенный выше метод представляет собой шаблон, который является обычным, эффективным и на 100% безопасным для памяти в Java и .NET, но который было бы очень сложно поддерживать безопасным для памяти способом в C++, даже если реализация имеет доступное эффективное локальное хранилище потока [концепция, которую я хотел бы, широко поддерживалась достаточно эффективно, чтобы критичный по времени код не мог ее избежать].
Вы можете описать, что такое «RAII с умными указателями»?