|
Программирование >> Обобщенные обратные вызовы
Задача 23. Новый взгляд на new. Часть 2: прагматизм в управлении памятью Сложность: 5 Избегайте использования new(nothrow) и убедитесь, что при проверке отказа new вы действительно проверяете именно то, что хотите проверить. Вопрос для новичка 1. Опишите два основных стандартных способа сообщения об ошибке оператором new в случае нехватки памяти. Вопрос для профессионала 2. Помогает ли использование не генерирующей исключения версии оператора new сделать код более безопасным с точки зрения исключений? Обоснуйте ваш ответ. 3. Опишите реальные ситуации - в пределах стандарта С++ или вне его - когда проверка исчерпания памяти невозможна или бесполезна. Решение в предыдущей задаче я проиллюстрировал и обосновал следующую рекомендацию. Любой класс, предоставляющий собственный оператор new или new[], должен также предоставлять соответствующие специфичные для данного класса версии обычного оператора new, размещающего оператора new и оператора new, не генерирующего исключений. В противном случае у пользователей вашего класса могут возникнуть лишние проблемы. Сейчас мы уделим наше внимание вопросу о том, что же означает отказ оператора new и как лучше всего обнаружить его и обработать. Избегайте использования new(nothrow), и убедитесь, что при проверке отказа new вы действительно проверяете именно то, что хотите проверить. Первая часть рекомендации может показаться немного неожиданной, но последняя - просто удивительной, потому что на некоторых распространенных платформах отказы выделения памяти обычно даже не проявляются так, как того требует стандарт. Здесь я опять для простоты не буду отдельно рассматривать оператор new для массивов. Все, что будет сказано об операторе new для одного объекта, относится и к оператору для массивов. Исключения, ошибки и new(nothrow) 1. Опишите два основных стандартных способа сообщения об ошибке оператором new в случае нехватки па.мяти. В то время как остальные разновидности оператора new сообщают об ошибках, генерируя исключение badalloc, не генерирующий исключений оператор new сообщает о них проверенным временем способом - как и функция malloc, он возвращает нулевой указатель. Этим гарантируется, что такой оператор, как следует из его названия, никогда не генерирует исключений. Главный вопрос - дает ли это нам что-нибудь или нет? Ряд программистов ошибочно полагали, что использование не генерирующего исключений оператора new увеличивает безопасность программы, поскольку уменьшает количество возможных исключений. Но так ли это на самом деле? 2. Помогает ли использование не генерирующей исключения версии оператора new сделать код более безопасным с точки зрения исключений? Обоснуйте ваш ответ. Ответ (возможно, неожиданный): нет, на самом деле не помогает. Сообщения об ошибках и их обработка - две большие разницы . Выбор между генерацией исключения bad al 1 ос и возвратом нулевого указателя - это выбор между двумя эквивалентными способами сообщения об ошибке. Таким образом, обнаружение и обработка ошибки представляет собой выбор между проверкой наличия исключения и проверкой значения указателя, не нулевой ли он. Для вызывающей оператор new программы отличие этих способов не более чем синтаксическое Это означает, что можно написать две совершенно одинаковые с точки зрения безопасности вообще и безопасности исключений в частности программы - для каждого из способов обнаружения ошибки, поскольку это всего лишь синтаксис, который приводит к минимальным изменениям в структуре вызывающей функции - например, вместо чего-то наподобие if (null) { HandleErrorO; throw MyownException(); } будет использоваться что-то вроде catch(bad alloc) { HandleErrorO; throw MyownException(); } Ни один из способов, которыми оператор new сообщает о происшедшей ошибке, не дает никакой дополнительной информации и не обеспечивает дополнительной безопасности, так что ни один из них не делает программу безопаснее или менее подверженной ошибкам - конечно, при аккуратном написании обработки ошибок. Но в чем тогда проявляется отличие для вызывающей программы, которая не проверяет наличие ошибок? В этом случае единственное различие будет в том, как именно проявится ошибка, но конечный результат будет одинаково печален. Либо непере-хваченное исключение bad alloc без всяких церемоний завершит выполнение программы (со сверткой стека или без нее), либо непроверенный нулевой указатель приведет при разыменовании к нарушению защиты памяти и немедленному аварийному завершению программы. Оба варианта достаточно катастрофичны для программы, но все же не перехваченное исключение имеет некоторые плюсы: по крайней мере, в этом случае будут сделаны попытки уничтожения некоторых из объектов и тем самым - освобождения ресурсов; кроме того, ряд аккуратно написанных объектов типа TextEditor при этом постараются сохранить свое состояние, чтобы впоследствии восстановить его. (Предупреждение: если память действительно исчерпана, то написать код, который бы корректно свернул стек и сохранил состояние объектов без использования дополнительной памяти, оказывается сложнее, чем кажется. Но, с другой стороны, вряд ли аварийный останов из-за обращения к памяти по некорректному указателю будет в чем-то лучше.) Отсюда мы выводим первую мораль: > Рекомендация Мораль №!: избегайте использования оператора new, не генерирующего исключения. Не генерирующий исключений оператор new не добавляет программе ни корректности, ни безопасности исключений. Для некоторых ошибок, а именно ошибок, которые игнорируются программой, - этот вариант оказывается хуже, чем вариант new с генерацией исключения, поскольку последний дает хоть какой-то шанс для сохранения состояния во время свертки стека. Как указывалось в предыдущей задаче, если классы предоставляют собственные операторы new, но при этом забывают об операторах new, не генерирующих исключений, то последние будут скрыты и не бу- дут работать. В большинстве случаев не генерирующие исключений операторы new не дают никаких преимуществ, так что в силу всего сказанного выше их следует избегать. Мне представляется, что есть только две ситуации, когда применение оператора new, не генерирующего исключения, может дать определенный выигрыш. Первый случай постепенно становится все менее значимым: это перенос большого количества старых приложений С+ + , в которых предполагалось, что ошибка в операторе new приведет к возврату нулевого указателя, и выполнялись соответствующие проверки. В таком случае может оказаться проще глобально заменить все new на new(nothrow) в старых файлах; однако таких файлов осталось не так уж и много, так как генерацию оператором new исключения bad al 1 ос трудно назвать новшеством. Второй случай, когда оправданно применение new, не генерирующего исключения - его применение в функции, критичной ко времени выполнения, или во внутреннем цикле, а сама функция компилируется слабым компилятором, генерирующим неэффективный код обработки исключений и приводит к заметной разнице во времени работы функции с использованием разных версий оператора new - генерирующей и не генерирующей исключений. Заметим, что, когда я говорю заметной , я говорю о времени работы всей функции в целом, а не только об ифушечном тесте самого оператора new. Только после того, как будет доказано наличие заметной разницы во времени работы такой функции, в ней можно рассмотреть возможность использования оператора new, не генерирующего исключений, но при этом обязательно рассмотреть также другие возможные методы повышения производительности выделения памяти, включая разработку собственного распределителя, работающего с блоками фиксированного размера и т.п. (только перед тем как приступить к этой работе, прочтите задачу 21). Итак, мы пришли к морали №2: > Рекомендация Мораль №2: очень часто проверка отказа в операторе new бесполезна. Эта рекомендация может ужаснуть многих программистов. Как вы можете предлагать такое - не проверять результат работы оператора new или, по крайней мере, утверждать, что такая проверка не важна? - могут спросить такие справедливо возмущенные люди. - Проверка ошибок - это краеугольный камень надежного программирования! Да, это так - в общем случае, но -- увы! - по причинам, свойственным исключительно распределению памяти, для него это не настолько важно, в отличие от других сбоев, которые должны быть проверены в обязательном порядке. Отсюда вытекает следующий вопрос. Теория и практика 3. Опишите реальные ситуации - в пределах стандарта С++ или вне его - когда проверка исчерпания памяти невозможна или бесполезна. Вот несколько причин, по которым проверка отказа при выполнении оператора new оказывается не .столь важной, как можно было бы предположить. Проверка результата работы оператора new может оказаться бесполезной в операционной системе, которая реально не выделяет память (commit) до тех пор, пока к ней не осуществляется обращение. В некоторых операционных системах* системные функции выделения памяти завершаются всегда успешно. Точка. Минутку, минутку, - можете удивиться вы. - Как же так - выделение памяти успешно даже в том случае, когда этой памяти в действительности нет? Причина в В некоторых версиях Linux, AIX и других операционных системах (например, в OS/2) такое отложенное вьщеленне памяти является поведением по умолчанию конфигурируемого свойства операционной системы.
|
© 2006 - 2024 pmbk.ru. Генерация страницы: 0
При копировании материалов приветствуются ссылки. |