straustrup2 (852740), страница 63
Текст из файла (страница 63)
Стоит сказать, что та же проблема возникает ив языках, не поддерживающих особые ситуации. Так, обращение к функции longjump()из стандартнойбиблиотеки С может иметь такие же неприятные последствия.Если вы создаете устойчивую к ошибкам системам, эту проблему придется решать. Можно датьпримитивное решение:void use_file(const char* fn){FILE* f = fopen(fn,"w");try {// работаем с f}catch (...) {fclose(f);throw;}fclose(f);}Вся часть функции, работающая с файлом f, помещена в проверяемый блок, в котором243Бьерн Страуструп.Язык программирования С++перехватываются все особые ситуации, закрывается файл и особая ситуация запускается повторно.Недостаток этого решения в его многословности, громоздкости и потенциальной расточительности. Ктому же всякое многословное и громоздкое решение чревато ошибками, хотя бы в силу усталостипрограммиста.
К счастью, есть более приемлемое решение. В общем виде проблему можносформулировать так:void{//////////////}acquire()запрос ресурса 1...запрос ресурса nиспользование ресурсовосвобождение ресурса n...освобождение ресурса 1Как правило бывает важно, чтобы ресурсы освобождались в обратном по сравнению с запросамипорядке.
Это очень сильно напоминает порядок работы с локальными объектами, создаваемымиконструкторами и уничтожаемыми деструкторами. Поэтому мы можем решить проблему запроса иосвобождения ресурсов, если будем использовать подходящие объекты классов с конструкторами идеструкторами. Например, можно определить класс FilePtr, который выступает как тип FILE* :class FilePtr {FILE* p;public:FilePtr(const char* n, const char* a){ p = fopen(n,a); }FilePtr(FILE* pp) { p = pp; }~FilePtr() { fclose(p); }operator FILE*() { return p; }};Построить объект FilePtr можно либо, имея объект типа FILE*, либо, получив нужные для fopen()параметры.
В любом случае этот объект будет уничтожен при выходе из его области видимости, и егодеструктор закроет файл. Теперь наш пример сжимается до такой функции:void use_file(const char* fn){FilePtr f(fn,"w");// работаем с f}Деструктор будет вызываться независимо от того, закончилась ли функция нормально, или произошелзапуск особой ситуации.9.4.1 Конструкторы и деструкторыОписанный способ управления ресурсами обычно называют "запрос ресурсов путем инициализации".Это универсальный прием, рассчитанный на свойства конструкторов и деструкторов и ихвзаимодействие с механизмом особых ситуаций.Объект не считается построенным, пока не завершил выполнение его конструктор.
Только после этоговозможна раскрутка стека, сопровождающая вызов деструктора объекта. Объект, состоящий извложенных объектов, построен в той степени, в какой построены вложенные объекты.Хорошо написанный конструктор должен гарантировать, что объект построен полностью и правильно.Если ему не удается сделать это, он должен, насколько это возможно, восстановить состояниесистемы, которое было до начала построения. Для простых конструкторов было бы идеально всегдаудовлетворять хотя бы одному условию - правильности или законченности объектов, и никогда неоставлять объект в "наполовину построенном" состоянии. Этого можно добиться, если применять припостроении членов способ "запроса ресурсов путем инициализации".244Бьерн Страуструп.Язык программирования С++Рассмотрим класс X, конструктору которого требуется два ресурса: файл x и замок y (т.е.
монопольныеправа доступа к чему-либо). Эти запросы могут быть отклонены и привести к запуску особой ситуации.Чтобы не усложнять работу программиста, можно потребовать, чтобы конструктор класса X никогда незавершался тем, что запрос на файл удовлетворен, а на замок нет. Для представления двух видовресурсов мы будем использовать объекты двух классов FilePtr и LockPtr (естественно, было быдостаточно одного класса, если x и y ресурсы одного вида).
Запрос ресурса выглядит какинициализация представляющего ресурс объекта:class X {FilePtr aa;LockPtr bb;// ...X(const char* x, const char* y): aa(x),bb(y){ }// ...};// запрос `x'// запрос `y'Теперь, как это было для случая локальных объектов, всю служебную работу, связанную с ресурсами,можно возложить на реализацию. Пользователь не обязан следить за ходом такой работой. Например,если после построения aa и до построения bb возникнет особая ситуация, то будет вызван толькодеструктор aa, но не bb.Это означает, что если строго придерживаться этой простой схемы запроса ресурсов, то все будет впорядке.
Еще более важно то, что создателю конструктора не нужно самому писать обработчики особыхситуаций.Для требований выделить блок в свободной памяти характерен самый произвольный порядок запросаресурсов. Примеры таких запросов уже неоднократно встречались в этой книге:class X {int* p;// ...public:X(int s) { p = new int[s]; init(); }~X() { delete[] p; }// ...};Это типичный пример использования свободной памяти, но в совокупности с особыми ситуациями онможет привести к ее исчерпанию памяти. Действительно, если в init() запущена особая ситуация, тоотведенная память не будет освобождена. Деструктор не будет вызываться, поскольку построениеобъекта не было завершено.
Есть более надежный вариант этого примера:template<class T> class MemPtr {public:T* p;MemPtr(size_t s) { p = new T[s]; }~MemPtr() { delete[] p; }operator T*() { return p; }}class X {MemPtr<int> cp;// ...public:X(int s):cp(s) { init(); }// ...};Теперь уничтожение массива, на который указывает p, происходит неявно в MemPtr. Если init() запуститособую ситуацию, отведенная память будет освобождена при неявном вызове деструктора для245Бьерн Страуструп.Язык программирования С++полностью построенного вложенного объекта cp.Отметим также, что стандартная стратегия выделения памяти в С++ гарантирует, что если функцииoperator new() не удалось выделить память для объекта, то конструктор для него никогда не будетвызываться. Это означает, что пользователю не надо опасаться, что конструктор или деструктор можетбыть вызван для несуществующего объекта.Теоретически дополнительные расходы, требующиеся для обработки особых ситуаций, когда на самомделе ни одна из них не возникла, могут быть сведены к нулю.
Однако, вряд ли это верно для раннихреализациях языка. Поэтому будет разумно в критичных внутренних циклах программы пока неиспользовать локальные переменные классов с деструкторами.9.4.2 ПредостереженияНе все программы должны быть устойчивы ко всем видам ошибок. Не все ресурсы являются настолькокритичными, чтобы оправдать попытки защитить их с помощью описанного способа "запроса ресурсовпутем инициализации". Есть множество программ, которые просто читают входные данные ивыполняются до конца. Для них самой подходящей реакцией на динамическую ошибку будет простопрекращение счета (после выдачи соответствующего сообщения).
Освобождение всех затребованныхресурсов возлагается на систему, а пользователь должен произвести повторный запуск программы сболее подходящими входными данными. Наша схема предназначена для задач, в которых такаяпримитивная реакция на динамическую ошибку неприемлема. Например, разработчик библиотекиобычно не в праве делать допущения о том, насколько устойчива к ошибкам, должна быть программа,работающая с библиотекой.
Поэтому он должен учитывать все динамические ошибки и освобождатьвсе ресурсы до возврата из библиотечной функции в пользовательскую программу. Метод "запросаресурсов путем инициализации" в совокупности с особыми ситуациями, сигнализирующими об ошибке,может пригодиться при создании многих библиотек.9.4.3 Исчерпание ресурсаЕсть одна из вечных проблем программирования: что делать, если не удалось удовлетворить запрос наресурс? Например, в предыдущем примере мы спокойно открывали с помощью fopen() файлы изапрашивали с помощью операции new блок свободной памяти, не задумываясь при этом, что такогофайла может не быть, а свободная память может исчерпаться.
Для решения такого рода проблем упрограммистов есть два способа:1.Повторный запрос: пользователь должен изменить свой запрос и повторить его.2.Завершение: запросить дополнительные ресурсы от системы, если их нет, запустить особуюситуацию.Первый способ предполагает для задания приемлемого запроса содействие пользователя, во второмпользователь должен быть готов правильно отреагировать на отказ в выделении ресурсов.
Вбольшинстве случаев последний способ намного проще и позволяет поддерживать в системеразделение различных уровней абстракции.В С++ первый способ поддержан механизмом вызова функций, а второй - механизмом особыхситуаций. Оба способа можно продемонстрировать на примере реализации и использования операцииnew:#include <stdlib.h>extern void* _last_allocation;extern void* operator new(size_t size){void* p;while ( (p=malloc(size))==0 ) {if (_new_handler)(*_new_handler)(); // обратимся за помощьюelsereturn 0;}246Бьерн Страуструп.Язык программирования С++return _last_allocation=p;}Если операция new() не может найти свободной памяти, она обращается к управляющей функции_new_handler(). Если в _new_handler() можно выделить достаточный объем памяти, все нормально.Если нет, из управляющей функции нельзя возвратиться в операцию new, т.к.