Обработка исключений с в конструкторе

Обновлено: 03.05.2024

При работе программ возникают так называемые исключительные ситуации, когда дальнейшее нормальное выполнение приложения становится невозможным. Причиной исключительных ситуаций могут быть как ошибки в программе, так и неправильные действия пользователя, неверные данные и т.д.
Обычные конструкции, необходимые для проверки данных, делают более-менее серьезную программу сложночитабельной. Более того, программисту очень сложно отледить все исключительные ситуации.

Сдесь на помощь программисту приходят такие средства языка, как:
-защищенные блоки ( try -блоки) и перехваты исключений ( catch -блоки);
-инициализация исключений (инструкция throw ).

блок try-catch

Простейший формат защищенного блока имеет вид:

Важно! Многоточие является частью синтаксиса языка!
Работает эта конструкция так.
Выполняются инструкции, входящие в состав блока try (защищенный блок). Если при их выполнении исключение не возбуждается (в C++ чаще используется термин «выброс исключения»), то блок catch пропускается. При выбросе исключения выполнение защищенного блока прекращается, и начинают работать инструкции, записанные в блоке catch .
Основной смысл этих инструкций – корректная обработка исключительной ситуации. Кроме того, в блок catch имеет смысл поместить код, который освобождает ресурсы, захваченные выполнившимися инструкциями из блока try . После окончания работы блока catch исключение считается обработанным, и управление передается на первую инструкцию, следующую за конструкцией try … catch .

Пример: перехват системного исключения "деление на ноль"
int x = 0;
try std::cout // Последующие операторы выполняться не будут
>
catch (. ) std::cout

Вот и все, и совсем не больно)

throw

Гораздо более интересным является механизм создания собственных исключений. Для их возбуждения используется оператор throw .
Тип выражения, указанного в операторе throw , определяет тип исключительной ситуации, а значение может быть передано обработчику прерываний. Этот механизм, заявленный как стандартный, представляется весьма экзотическим без использования механизма классов. И только
использование стандартных классов-исключений или разработка собственных классов
позволяют в полной мере оценить все возможности такого подхода.

Соответственно, полный формат защищенного блока имеет вид:
try

Первый формат используется, если нам надо указать тип перехватываемого исключения, но не нужно обрабатывать связанное с этим исключением значение (это достигается при использовании второго формата оператора catch ). Наконец, третий формат оператора catch позволяет обработать все исключения.

Обработка исключений, возбужденных оператором throw , идет по следующей схеме:
1. Создается статическая переменная со значением, заданным в операторе throw .
Она будет существовать до тех пор, пока исключение не будет обработано.
Если переменная-исключение является объектом класса, при ее создании работает конструктор копирования.
2. Завершается выполнение защищенного try -блока: раскручивается
стек подпрограмм,
вызываются деструкторы для тех объектов, время жизни которых истекает и т.д.
3. Выполняется поиск первого из catch -блоков, который пригоден для обработки созданного
исключения.

Поиск ведется по следующим критериям:
— если тип, указанный в catch -блоке, совпадает с типом созданного исключения,
или является ссылкой на этот тип;
— класс, заданный в catch -блоке, является предком класса, заданного в throw ,
и наследование выполнялось с ключом доступа public ;
— указатель, заданный в операторе throw , может быть преобразован по стандартным правилам к указателю, заданному в catch -блоке.
— в операторе throw задано многоточие.

Если нужный обработчик найден, то ему передается управление и, при необходимости,
значение оператора throw . Оставшиеся catch -блоки,
относящиеся к защищенному блоку, в котором было создано исключение, игнорируются.
Из указанных правил поиска следует, что очень важен порядок расположения catch -блоков.
Так, блок catch(…) должен стоять последним в списке, а блок catch (void *) – после всех блоков с указательными типами.
Если ни один из catch -блоков, указанных после защищенного блока, не сработал, то исключение считается необработанным. Его обработка может быть продолжена во внешних блоках try (если они, конечно, есть).
В конце оператора catch может стоять оператор throw без параметров. В этом случае работа catch -блока считается незавершенной а исключение – не обработанным до конца, и происходит поиск соответствующего обработчика на более высоких уровнях.
Если оператор throw был вызван вне защищенного блока (что чаще всего случается, когда исключение возбуждается в вызванной функции), или если не был найден ни один подходящий обработчик этого исключения, то вызывается стандартная функция terminate() . Она, в свою очередь, вызывает функцию abort() для завершения работы с приложением. Единственное, что доступно программисту в этом случае – зарегистрировать с помощью функции set_terminate свою функцию, которая будет выполняться перед аварийным завершением работы.

Преимущества и недостатки работы с исключениями

Возбуждение исключения не является единственным способом сообщить о возникновении
внештатной ситуации в ходе выполнения программы. Просигнализировать о возникшей ошибке
могут и другие механизмы, например:
— занесение информации о состоянии программы в специальную переменную или поле класса и использование специальных механизмов доступа к этой информации;
— задание таких спецификаций для функций, которые сигнализировали бы о наступлении
нештатной ситуации (например, использование булевских функций, которые возвращали бы
значение false при возникновении таких ситуаций);
— использование стандартных функций, сообщающих об ошибках, например perror() .
Выбор того или иного механизма реакции на нештатные ситуации достаточно сложен, поскольку
четких критериев не существует, да и не может существовать. Мы должны учитывать следующие соображения:
— игнорирование исключения приводит к аварийному завершению приложения,
а игнорирование другой информации проходит незаметно (хотя результаты могут быть и непредсказуемыми);
— если в программе есть большое число вызовов функции, которая может создать
нештатную ситуацию, то логичнее было бы включить их в один защищенный блок, нежели
выполнять проверку результатов для каждого вызова;
— с другой стороны, следует иметь в виду, что после возникновения исключительной ситуации и ее обработки работа защищенного блока не возобновляется.

Говорят, что конструктор конструктор и деструктор класса не должны вырабатывать исключения?
В тоже время, я видел огромное количество кода и даже примеров из книг, где в конструкторе выделяется память при помощи оператора `new`, но ведь даже он может вырабатывать исключение `bad_alloc` — выходит, что весь этот код потенциально опасный? Как с этим бороться?

Исключения, ровно как и оператор return прерывают поток выполнения команд функции, из системного стека выбираются объекты (такие как локальные переменные) и для них вызываются деструкторы. Однако, если при выполнении оператора return раскрутка стека прекратиться в точке где была вызвана завершенная функция, то при при выполнении throw объекты из стека будут уничтожаться до тех пор, пока управление не будет передано в блок try<> , содержащий обработчик, соответствующий типу выброшенного исключения. Читать подробнее про обработку исключений [1].

Исключения в деструкторе класса

В связи с этим, в большинстве случаев разрушение объектов созданных на стеке (без использования new/malloc ) произойдет корректно — вызовом деструктора. Однако исключения в конструкторе или деструкторе могут приводить к нежелательным последствиям.

Во-первых, программа не должна вырабатывать исключения во время обработки другого исключения (когда происходит раскрутка стека) — это приведет к аварийному завершению работы программы (фактически вызову abort() ), которое уже не получится корректно обработать. Причина такой ошибки заключается в том, что один из деструкторов вырабатывает исключение или не обрабатывает исключение функции, которую вызывает:

destructor_exception_abort

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

Еще один из аспектов работы деструкторов и исключений иллюстрирует следующий фрагмент кода:

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

Стандартная библиотека предоставляет функцию std::uncaught_exception , которая в деструкторе позволяет узнать, почему уничтожается объект, из-за выброшенного исключения, или же по какой-либо другой причине.

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

Исключения в конструкторе класса

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

Стандарт языка С++ гарантирует, что если исключение возникнет в конструкторе, то памяти из под членов-данных класса будет освобождена корректно вызовом деструктора — т.е. если вы используете идиому RAII [2], то проблем не будет. Часто для этого достаточно использовать std::vector/std::string вместо старых массивов и строк, и умные указатели вместо обычных [3]. Если же вы продолжите использовать сырые указатели и динамически выделять память — нужно будет очень тщательно следить за ней, например в следующем фрагменте кода нет утечки, т.к. исключение будет выработано только если память не будет выделена [4]:

Одним из преимуществ механизма исключения является то, что выполнение вместе с данными об исключении переходит непосредственно из оператора, который создает исключение, в первую инструкцию Catch, которая его обрабатывает. Обработчик может иметь любое количество уровней вверх в стеке вызовов. Функции, которые вызываются между оператором try и оператором throw, не обязательно должны знать какие-либо сведения о возникшем исключении. Однако они должны быть спроектированы таким образом, чтобы они могли выйти из области "непредвиденно" в любой момент, где исключение может быть распространено ниже, и сделать это без выхода из частично созданных объектов, утечки памяти или структур данных, которые находятся в непригодных для использования состояниях.

Основные методы

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

Простота сохранения классов ресурсов

При инкапсулировании управления ресурсами вручную в классах используйте класс, который не выполняет никаких действий, кроме управления одним ресурсом. Благодаря простоте использования класса вы снижаете риск возникновения утечек ресурсов. Используйте смарт-указатели , если это возможно, как показано в следующем примере. Этот пример намеренно искусственно и упрощен, чтобы выделить различия при shared_ptr использовании.

Использование идиомы RAII для управления ресурсами

Чтобы обеспечить безопасность исключений, функция должна гарантировать, что объекты, выделенные с помощью malloc или new , уничтожаются, и все ресурсы, такие как дескрипторы файлов, закрываются или освобождаются, даже если возникает исключение. Функция получения ресурсов — инициализация (RAII) идиома связывает такие ресурсы с сроком службы автоматических переменных. Если функция выходит за пределы области действия, либо путем возвращения обычно или из-за исключения, вызываются деструкторы для всех полностью сформированных автоматических переменных. Объект-оболочка RAII, такой как интеллектуальный указатель, вызывает соответствующую функцию DELETE или Close в своем деструкторе. В коде, защищенном с исключением, очень важно немедленно передавать владение каждым ресурсом объекту RAII. Обратите внимание, что vector классы, string make_shared ,, fstream и схожие по себе обрабатывали ресурсы для получения. Однако традиционные shared_ptr конструкции являются специальными, unique_ptr так как получение ресурса выполняется пользователем, а не объектом. Следовательно, они считаются освобождением ресурсов , но их можно считать RAII.

Три гарантии исключений

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

Гарантия No — сбой

Гарантия «без ошибок» (или «без выдачи») является самой высокой гарантией, которую может предоставить функция. Он указывает, что функция не будет вызывать исключение или разрешить распространение. Однако нельзя гарантировать такую гарантию, если (a) известно, что все функции, которые вызывает эта функция, также не являются ошибками, или (б) известно, что все возникшие исключения перехватываются до того, как они достигли этой функции, или (c) вы узнали, как перехватить и правильно обрабатывайте все исключения, которые могут достичь этой функции.

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

Строгая гарантия

Строгая гарантия указывает, что если функция выходит за пределы области действия из-за исключения, она не будет приводить к утечке памяти, а состояние программы не будет изменено. Функция, обеспечивающая строгую гарантию, по сути, является транзакцией, которая имеет семантику фиксации или отката: либо полностью завершается успешно, либо не оказывает никакого воздействия.

Базовая гарантия

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

Классы, защищенные с исключением

Класс помогает обеспечить собственную безопасность исключений, даже если она используется небезопасными функциями, предотвращая ее частичное построение или частичное уничтожение. Если конструктор класса завершает работу до завершения, объект никогда не создается и его деструктор никогда не вызывается. Хотя автоматические переменные, инициализированные до исключения, будут вызывать свои деструкторы, динамически выделенная память или ресурсы, не управляемые смарт-указателем или аналогичной автоматической переменной, будут утеряны.

Встроенные типы являются неудачными, а стандартные типы библиотек поддерживают базовую гарантию как минимум. Следуйте этим рекомендациям для любого определяемого пользователем типа, который должен быть защищен от исключений.

Для управления всеми ресурсами используйте смарт-указатели или другие оболочки типа RAII. Избегайте функций управления ресурсами в деструкторе класса, так как деструктор не будет вызываться, если конструктор выдаст исключение. Однако если класс является выделенным диспетчером ресурсов, который управляет только одним ресурсом, то для управления ресурсами допустимо использовать деструктор.

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

Определите, следует ли сохранять все состояния класса в члене данных, заключенном в интеллектуальный указатель, особенно если класс имеет концепцию "инициализация, которая может быть неудачной". Хотя C++ допускает неинициализированные элементы данных, он не поддерживает неинициализированные или частично инициализированные экземпляры класса. Конструктор должен быть успешно выполнен или завершился ошибкой. Если конструктор не выполняется до завершения, объект не создается.

Не разрешать исключения в escape-последовательности из деструктора. Базовый аксиома C++ заключается в том, что деструкторы никогда не должны разрешать исключение для распространения стека вызовов. Если деструктор должен выполнить операцию, вызывающую исключение, он должен быть выполнен в блоке try catch и проглотить исключение. Стандартная библиотека обеспечивает эту гарантию для всех деструкторов, которые он определяет.

В современных C++ в большинстве случаев предпочтительным способом сообщить и обрабатывались как логические ошибки, так и ошибки времени выполнения — использовать исключения. Особенно это касается того, что стек может содержать несколько вызовов функций между функцией, которая обнаруживает ошибку, и функцией, которая имеет контекст для ее устранения. Исключения предоставляют формальный, четко определенный способ для кода, который обнаруживает ошибки для передачи информации вверх по стеку вызовов.

Использовать исключения для кода исключительного пользования

Ошибки программы часто делятся на две категории: логические ошибки, вызванные ошибками программирования, например, ошибкой «индекс вне диапазона». И ошибки времени выполнения, которые выходят за рамки управления программистом, например "ошибка" Сетевая служба недоступна ". В программировании в стиле C и в COM Управление отчетами об ошибках осуществляется либо путем возвращения значения, представляющего код ошибки, либо кода состояния для конкретной функции, либо путем установки глобальной переменной, которую вызывающий может дополнительно получить после каждого вызова функции, чтобы проверить, были ли обнаружены ошибки. Например, при программировании COM для передачи ошибок вызывающему объекту используется возвращаемое значение HRESULT. API-интерфейс Win32 содержит GetLastError функцию для получения последней ошибки, о которой сообщил стек вызовов. В обоих случаях для распознавания кода и реагирования на него требуется вызывающая сторона. Если вызывающий объект не обрабатывает код ошибки явным образом, программа может аварийно завершить работу без предупреждения. Или можно продолжить выполнение с использованием неверных данных и получить неверные результаты.

Исключения являются предпочтительными в современных C++ по следующим причинам:

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

Механизм обратной записи исключений уничтожает все объекты в области действия после возникновения исключения в соответствии с четко определенными правилами.

Исключение позволяет четко отделить код, который определяет ошибку, и код, обрабатывающий ошибку.

В следующем упрощенном примере показан синтаксис, необходимый для генерации и перехвата исключений в C++.

Основные рекомендации

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

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

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

Для каждой функции, которая может выдавать или распространять исключение, следует предоставить одно из трех гарантий исключений: строгая гарантия, Базовая гарантия или "Throw" (Except). Дополнительные сведения см. в разделе руководство. проектирование безопасности исключений.

Вызывайте исключения по значению, перехватите их по ссылке. Не перехватывайте объекты, которые не могут быть обработаны.

Не используйте спецификации исключений, которые являются устаревшими в C++ 11. Дополнительные сведения см. в разделе спецификации исключений и noexcept раздел.

Используйте типы исключений стандартной библиотеки при их применении. Наследовать пользовательские типы исключений от exception иерархии классов .

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

Исключения и производительность

Механизм исключения имеет минимальные затраты на производительность, если исключение не создается. При возникновении исключения стоимость прохода стека и его очистки приблизительно сравнима с затратами на вызов функции. Дополнительные структуры данных необходимы для контроля стека вызовов после того, как try был выполнен блок, и при возникновении исключения требуются дополнительные инструкции для очистки стека. Однако в большинстве случаев затраты на производительность и объем памяти не являются существенными. Негативное воздействие исключений на производительность может быть значительным только для систем с ограниченным объемом памяти. Кроме того, в циклах, критических с точки зрения производительности, часто возникает ошибка, и существует тесная связь между кодом и его обработкой. В любом случае невозможно понять фактическую стоимость исключений без профилирования и измерения. Даже в редких случаях, когда стоимость существенна, можно взвесить ее на более высокую правильность, упростить обслуживание и другие преимущества, предоставляемые хорошо спроектированной политикой исключений.

Исключения и утверждения

исключения C++ и Windows исключения SEH

программы C и C++ могут использовать механизм структурированной обработки исключений (SEH) в Windows операционной системе. Понятия SEH похожи на объекты в исключениях C++, за исключением того, что SEH использует __try конструкции, __except и __finally , а не try и catch . в компиляторе Microsoft C++ (MSVC) исключения C++ реализуются для SEH. Однако при написании кода C++ используйте синтаксис исключения C++.

Спецификации исключений и noexcept

Спецификации исключений были введены в C++ как способ указания исключений, которые может вызывать функция. Однако спецификации исключений выдают проблемы на практике и являются устаревшими в стандарте "черновик C++ 11". Мы рекомендуем не использовать throw спецификации исключений, кроме throw() , что указывает, что функция не допускает исключений для экранирования. если необходимо использовать спецификации исключений устаревшей формы throw( type-name ) , MSVC поддержка ограничена. Дополнительные сведения см. в разделе спецификации исключений (throw). noexcept Описатель вводится в c++ 11 в качестве предпочтительного варианта throw() .

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


Или хранить их по ссылке:


Существует множество «за» и «против» для каждого из подходов, но в этой заметке мне бы хотелось сосредоточиться на вопросах обработки исключений.

Начнём по-порядку. Пусть у нас есть некий класс, конструктор которого, в некоторых случаях, может вызывать исключение (нет файла, нет связи, не подошёл пароль, недостаточно прав для выполнения операции… что угодно). Наш класс будет предельно прост и предсказуем.


Казалось бы, ничего не забыли. (Хотя, строго говоря, конечно забыли как минимум конструктор копирования и операцию присвоения, которые бы корректно работали с нашими указателями; ну да ладно.)

Воспользуемся этим классом:


И разберёмся, что и когда будет конструиваться и уничтожаться.

  • Сперва запустится процесс создания объекта Cnt.
  • В нём будет создан объект *xa
  • Начнёт создание объекта *xb.
  • … и тут произойдёт исключение

Обратите внимание, что это одна из самых неприятных ситуаций, утечка возникает не всегда, а только при определённых аргументах (первый — не ноль, а второй — ноль). Отыскать такие утечки бывает очень сложно.

Очевидно, такое решение годится только для очень простеньких программок, которые в случае любого исключения просто беспомощно валятся и всё.

Какие же есть решения?

Самое простое, надёжное и естественное решение — хранить объект по значению


Это компактно, это элегантно, это естественно… но главное — это безопасно! В этом случае компилятор следит за всем происходящим, и (по-возможности) вычищает всё, что уже не понадобится.

Результат работы кода:


То есть объект Cnt::xa был автоматически корректно уничтожен.

Безумное решение с указателями

Настоящим кошмаром может стать вот такое решение:


Представляете, что будет, если появится Cnt::xc? А если придётся поменять порядок инициализации. Надо будет приложить не мало усилий, чтобы ничего не забыть, сопровождая такой код. И, что самое обидное, это вы сами для себя же разложили везде грабли.

Лирическое отступление про исключения.

Для чего были придуманы исключения? Для того, чтобы отделить описание нормального хода программы от описания реакции на какие-то сбои.

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

Это сводит на нет всю прелесть механизма исключений. Фактически, мы возвращаемся к концепции C, где после каждой операции надо проверять значения глобальных переменных или другие признаки возникновения ошибок.

Это делает код запутанным и трудным для понимания и поддержки.

Решение настоящих индейцев — умные указатели

Если вам всё же необходимо хранить указатели, то вы всё равно можете обезопасить свой код, если сделаете для указателей обёртки. Их можно писать самим, а можно использовать множество, уже существующих. Пример использования auto_ptr:


Мы практически вернулись к решению с хранением членов класса по значению. Здесь мы храним по значению объекты класса auto_ptr, о своевременном удалении этих объектов опять заботится компилятор (обратите внимание, теперь нам не надо самостоятельно вызывать delete в деструкторе); а они, в свою очередь, хранят наши указатели на объекты X и заботятся о том, чтобы память вовремя освобождалась.

Да! И не забудьте подключить


Там описан шаблон auto_ptr.

Лирическое отступление про new

Одно из преимуществ C++ перед C состоит в том, что C++ позволяет работать со сложными структурами данных (объектами), как с обычными переменными. То есть C++ сам создаёт эти структуры и сам удаляет их. Программист может не задумываться об освобождении ресурсов до тех пор, пока он (программист) не начнёт сам создавать объекты. Как только вы написали «new», вы обязали себя написать «delete» везде, где это нужно. И это не только деструкторы. Больше того, вам скорее всего придётся самостоятельно реализовывать операцию копирования и операцию присвоения… Одним словом, вы отказались от услуг С++ и попали на весьма зыбкую почву.

Конечно, в реальной жизни часто приходится использовать «new». Это может быть связано со спецификой алгоритма, продиктовано требованиями по производительности или просто навязано чужими интерфейсами. Но если у вас есть выбор, то наверно стоит трижды подумать, прежде, чем написать слово «new».

Читайте также: