Парадигмы управления памятью C++

Я перехожу из C в С++ 11 и пытаюсь выяснить парадигму управления памятью для программ С++ 11 (или любых современных языков со встроенными исключениями). В частности, у меня есть трещина в разработке игр, где исчерпание памяти - настоящая проблема.

В C я использую для проверки возвращаемого значения malloc; и обычно используют пользовательские распределители.

С С++ я совершенно смущен; хотя мне нравится, как были построены контейнеры STL, позволяющие настраивать распределители. Поскольку контейнеры STL управляют собственной памятью, просто добавление элемента к вектору может вызвать std::bad_alloc. Как я защищаю такие вещи? Я слышал, что все вызовы бросания в блоках try/catch могут быть чрезмерно дорогими.

Однако, позволяя исключению путешествовать по стоп-косту, можно было бы выполнить кучу функций, которые не выполнялись бы полностью, и приведут к некоему действительно сложному коду. т.е. если A->B->C->D - это столбец, D бросает и A улавливает, то B, C и D могут потенциально создать некоторые странные проблемы, поскольку они не могут нормально завершить выполнение.

Кроме того, аргумент nothrow, по-видимому, позволяет очень C-подобный код; хотя я теперь не вижу преимущества над простым malloc.

Каковы некоторые лучшие практики для написания безопасного кода на С++, защищающего от проблем с отсутствием памяти?

edit: Соответствующий ответ на progammers.stackexchange, аргументирующий создание дизайна на С++ без исключений. Не уверен, что эти аргументы все еще применяются к 8-го поколения консолей

Ответ 1

Мой ответ будет больше ориентирован на развитие игры, так как это мой фон, и это часть того, что вас интересует. Различные типы приложений будут иметь разные требования.

Игры обычно распределяют всю динамическую память спереди и вставляются в этот бюджет. Консоли, в частности, имеют жесткие ограничения в памяти, и большинство игр захотят использовать все это.

Есть несколько причин для выделения всего впереди.

Один, производительность. Распределение памяти происходит медленно. Вы хотите избежать этого любой ценой. Если вы выделите все на передний план, вы можете затем написать собственные высокопроизводительные распределители памяти, такие как Pool Allocators, Stack Allocators и т.д., Которые просто захватывают память из вашего предварительно выделенного буфера. Важно выбрать лучший распределитель для этой задачи.

Два, вы будете знать быстро, если недостаточно памяти для вашей игры. В разработке вы потерпите крах, если у вас закончится нехватка памяти, и вам придется корректировать использование, но окончательная версия не должна терпеть крах, потому что вы выделили фронт и застряли в своих бюджетах памяти.

Для исключений многие (но не все) игры отключают исключения, опять же по соображениям производительности. На самом деле некоторые компиляторы консоли даже не поддерживают исключения. Затем вам нужно будет либо использовать библиотеку STL без каких-либо исключений, либо реализовать свои собственные контейнеры. Многие игровые команды предпочитают реализовать свои собственные по соображениям производительности, а также лучше интегрировать их с пользовательскими распределителями памяти.

Тем не менее, динамическое распределение памяти, STL и исключения, вероятно, отлично подходят для небольших личных проектов/игр, но имейте в виду, что будет необходимо для больших игр с высокой производительностью в реальном времени.

Для безопасности исключений я бы определенно использовал RAII. Это его цель. Также я бы рекомендовал использовать интеллектуальные указатели, такие как std::unique_ptr и std::shared_ptr для управления памятью. В сочетании с RAII, если ваши конструкторы бросают, память будет освобождена.

Ответ 2

Используйте деструкторы для автоматической очистки при выходе из области действия. Это называется RAII, Resource Acquisition Is Initializion, хотя аббревиатура не совсем то, что можно было бы разработать. Все стандартные контейнеры и т.д. Автоматически очищаются.

В таких языках, как С# и Java, которые основаны на сборке мусора, вместо этого перепишите код с помощью блоков try и "используя". Java просто получил это (часть синтаксиса try IIRC); он был на С# с самого начала (ключевое слово using); в Python он называется with; и С++ не имеет его и не нуждается в нем. Я когда-то создавал макрос with для С++, умный маленький взломать, думая, что я буду использовать его все время, но я не использовал его один раз, кроме как попробовать его сразу после его создания: в С++ RAII делает это все.

Подведение итогов: используйте RAII, т.е. используйте деструкторы, и просто позвольте этим исключениям распространяться.


Что касается исчерпания памяти, обычно это просто считается "мы сделали для", нечего делать, кроме как закончить как можно более упорядоченное.

Но не помешает, возможно, выделить небольшой буфер, который может быть освобожден, когда память исчерпана, чтобы иметь рабочую память для очистки.


С++ не различает жесткие исключения (фатальные, например, из-за исчерпания памяти) и мягкие исключения (общие неудачи, которые не являются фатальными).