Анимация
JavaScript
|
Главная Библионтека 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) такое отложенное вьщеление памяти является поведением по умолчанию конфигурируемого свойства операционной системы. том, что операция выделения памяти просто записывает запрос на выделение определенного количества памяти, но при этом реального выделения памяти для запрашивающего процесса не происходит до тех пор, пока процесс не обратится к ней. Даже когда выделенная память используется процессом, часто реальная (физическая или виртуальная) память выделяется постранично, при обращении к конкретной странице, так что может оказаться, что в действительности вместо большого блока памяти процессу выделяется только его часть - с которой процесс реально работает. Заметим, что если оператор new использует возможности операционной системы непосредственно, то он всегда завершается успешно, но последующий за ним невинный код наподобие buf[100] = с; может привести к генерации исключения или аварийному останову. С точки зрения стандарта С++ оба действия некорректны, поскольку, с одной стороны, стандарт С++ требует, чтобы в случае, когда оператор new не может регьтьно вьщелить запрошенную память, он генерировал исключение (этого не происходит), и чтобы код наподобие buf [100] = с не приводил к генерации исключений или другим сбоям (что может произойти). Почему некоторые операционные системы выполняют такое отложенное выделение памяти? В основе такой схемы лежит прагматичный довод: процессу, который запросил данную память, сразу в полном объеме она может не понадобиться, более того - процесс может никогда не использовать ее полностью или не использовать ее всю одновременно - а тем временем сю мог бы воспользоваться другой процесс, которому эта память нужна ненадолго. Зачем выделять процессу всю память немедленно при запросе, если реально она ему может и не понадобиться? Поэтому описанная схема имеет ряд преимуществ. Основная проблема при таком подходе связана со сложностью обеспечения соответствия требованиям стандарта, что в свою очередь делает разработку корректной программы сложной задачей: ведь теперь любое обращение к успешно вьщеленной динамической памяти может привести к аварийному останову программы. Это явно нехорошо. Если выделение памяти завершилось неудачей, профамма знает, что памяти для завершения операции недостаточно, и может выполнить какие-то соответствующие ситуации действия - уменьшить запрашиваемый блок памяти, использовать менее критичный к памяти, но более медленный алгоритм, да, наконец, просто корректно завершиться со сверткой стека. Но если у программы нет способа узнать, доступна ли в действительности выделенная память, то любая попытка ее чтения или записи может привести к аварийному останову профаммы, причем предсказать сго невозможно, поскольку такая нсприятностъ может произойти как при псрюм обращении к некоторой части буфера, так и после миллионов успешных обращений к другим частям буфера. На первый взгляд кажется, что единственный способ избежать этого заключается в немедленной записи (или чтении) всего блока памяти, чтобы убедиться в его существовании, например: Пример 23-1: инициализация вручную с обращением к каждому байту выделенной памяти. char* р = new char[1000000000]; memsetC р, О, 1000000000 ); Если память выделяется для типа, который является типом класса, а не обычным старым типом (РОО), то обращение к памяти выполняется автоматически. POD означает "plain old data" - "простые старые данные". Неформально POD означает любой тип, представляющий собой набор простых данных, возможно, с пользовательскими функциями-членами для удобства. Говоря более строго, POD представляет собой класс или объединение, у которого нет пользовательского конструктора, копирующего присваивания, и деструктора, а также нет {нестатических) членов-данных, являющихся ссылками, указателями на члены или не являющимися POD, 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 [ 47 ] 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 |