Главная страница
Навигация по странице:

  • 10.2 Объекты-исключения

  • 10.3 Раскрутка стека

  • 10.4 Повторное возбуждение исключений

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

  • ттттт. Объектноориентированное программирование


    Скачать 1.73 Mb.
    НазваниеОбъектноориентированное программирование
    Анкорттттт
    Дата30.10.2021
    Размер1.73 Mb.
    Формат файлаpdf
    Имя файлаOOP-PrePrint.pdf
    ТипКонспект
    #259341
    страница8 из 15
    1   ...   4   5   6   7   8   9   10   11   ...   15
    10.1 Перехват исключений
    В языке C++ исключения обрабатываются в предложениях catch
    . Когда какая-то инструкция внутри try- блока возбуждает исключение, то про- сматривается список последующих предложений catch в поисках такого, который может его обработать.
    Catch
    -обработчик состоит из трех частей: ключевого слова catch
    , объ- явления одного типа или одного объекта, заключенного в круглые скобки
    (оно называется объявлением исключения), и составной инструкции. Если для обработки исключения выбрано некоторое catch
    -предложение, то вы- полняется эта составная инструкция. Рассмотрим catch
    -обработчики ис- ключений pushOnFull и popOnEmpty в функции main()
    более подробно: catch ( pushOnFull ) { cerr << "trying to push value on a full stack\n"; return errorCode88;
    } catch ( popOnEmpty ) { cerr << "trying to pop a value on an empty stack\n"; return errorCode89;
    }
    В обоих catch
    -обработчиках есть объявление типа класса; в первом это pushOnFull
    , а во втором – popOnEmpty
    . Для обработки исключения выби- рается тот обработчик, для которого типы в объявлении исключения и в воз- бужденном исключении совпадают. Например, когда функция-член pop()

    82 класса iStack возбуждает исключение popOnEmpty
    , то управление попа- дает во второй обработчик. После вывода сообщения об ошибке в cerr
    , функция main()
    возвращает код errorCode89
    А если catch
    -обработчики не содержат инструкции return
    , с какого ме- ста будет продолжено выполнение программы? После завершения обработ- чика выполнение возобновляется с инструкции, идущей за последним catch
    -обработчиком в списке. В нашем примере оно продолжается с ин- струкции return в функции main()
    . После того как catch- обработчик popOnEmpty выведет сообщение об ошибке, main()
    вернет 0. int main() { iStack stack( 32 ); try { stack.display(); for ( int x = 1; ix < 51; ++ix )
    {
    // то же, что и раньше
    }
    } catch ( pushOnFull ) { cerr << "trying to push value on a full stack\n";
    } catch ( popOnEmpty ) { cerr << "trying to pop a value on an empty stack\n";
    }
    // исполнение продолжается отсюда return 0;
    }
    Говорят, что механизм обработки исключений в C++ невозвратный: по- сле того как исключение обработано, управление не возобновляется с того места, где оно было возбуждено. В нашем примере управление не возвра- щается в функцию-член pop()
    , возбудившую исключение.
    10.2 Объекты-исключения
    Объявлением исключения в catch
    -обработчике могут быть объявления типа или объекта. В каких случаях это следует делать? Тогда, когда необхо- димо получить значение или как-то манипулировать объектом, созданным в выражении throw
    . Если классы исключений спроектированы так, что в объектах-исключениях при возбуждении сохраняется некоторая информа- ция и если в объявлении исключения фигурирует такой объект, то инструк- ции внутри catch
    -обработчика могут обращаться к информации, сохранен- ной в объекте выражением throw

    83
    Изменим реализацию класса исключения pushOnFull
    , сохранив в объ- екте-исключении то значение, которое не удалось поместить в стек.
    Catch
    -обработчик, сообщая об ошибке, теперь будет выводить его в cerr
    Для этого мы сначала модифицируем определение типа класса pushOnFull следующим образом:
    // новый класс исключения:
    // он сохраняет значение, которое не удалось поместить в стек class pushOnFull { public: pushOnFull( int i ) : _value( i ) { } int value { return _value; } private: int _value;
    };
    Новый закрытый член _
    value содержит число, которое не удалось по- местить в стек. Конструктор принимает значение типа int и сохраняет его в члене _
    data
    . Вот как вызывается этот конструктор для сохранения значе- ния из выражения throw
    : void iStack::push( int value )
    { if ( full() )
    // значение, сохраняемое в объекте-исключении throw pushOnFull( value );
    // ...
    }
    У класса pushOnFull появилась также новая функция-член value()
    , ко- торую можно использовать в catch
    -обработчике для вывода хранящегося в объекте-исключении значения: catch ( pushOnFull eObj ) { cerr << "trying to push value << "eObj.value()
    << "on a full stack\n";
    }
    Обратите внимание, что в объявлении исключения в catch
    -обработчике фигурирует объект eObj
    , с помощью которого вызывается функция-член value()
    класса pushOnFull
    Объект-исключение всегда создается в точке возбуждения, даже если выражение throw
    – это не вызов конструктора и, на первый взгляд, не должно создавать объекта.
    Например: enum EHstate { noErr, zeroOp, negativeOp, severeError }; enum EHstate state = noErr;

    84 int mathFunc( int i ) { if ( i == 0 ) { state = zeroOp; throw state; // создан объект-исключение
    }
    // иначе продолжается обычная обработка
    }
    В этом примере объект state не используется в качестве объекта-ис- ключения. Вместо этого выражением throw создается объект-исключение типа
    EHstate
    , который инициализируется значением глобального объекта state
    . Как программа может различить их? Для ответа на этот вопрос мы должны присмотреться к объявлению исключения в catch
    -обработчике бо- лее внимательно.
    Это объявление ведет себя почти так же, как объявление формального параметра. Если при входе в catch
    -обработчик исключения выясняется, что в нем объявлен объект, то он инициализируется копией объекта-исключе- ния. Например, следующая функция calculate()
    вызывает определенную выше mathFunc()
    . При входе в catch- обработчик внутри calculate()
    объект eObj инициализируется копией объекта-исключения, созданного выражением throw void calculate( int op ) { try { mathFunc( op );
    } catch ( EHstate eObj ) {
    // eObj - копия сгенерированного объекта-исключения
    }
    }
    Объявление исключения в этом примере напоминает передачу пара- метра по значению. Объект eObj инициализируется значением объекта-ис- ключения точно так же, как переданный по значению формальный параметр функции – значением соответствующего фактического аргумента.
    Как и в случае параметров функции, в объявлении исключения может фигурировать ссылка. Тогда catch
    -обработчик будет напрямую ссылаться на объект-исключение, сгенерированный выражением throw
    , а не создавать его локальную копию: void calculate( int op ) { try { mathFunc( op );
    } catch ( EHstate &eObj ) {
    // eObj ссылается на сгенерированный объект-исключение
    }
    }

    85
    Для предотвращения ненужного копирования больших объектов приме- нять ссылки следует не только в объявлениях параметров типа класса, но и в объявлениях исключений того же типа.
    В последнем случае catch
    -обработчик сможет модифицировать объект- исключение. Однако переменные, определенные в выражении throw
    , оста- ются без изменения. Например, модификация eObj внутри catch
    -обработ- чика не затрагивает глобальную переменную state
    , установленную в выра- жении throw
    : void calculate( int op ) { try { mathFunc( op );
    } catch ( EHstate &eObj ) {
    // исправить ошибку, вызвавшую исключение eObj = noErr; // глобальная переменная state не изменилась
    }
    }
    Catch
    -обработчик переустанавливает eObj в noErr после исправления ошибки, вызвавшей исключение. Поскольку eObj
    – это ссылка, можно ожи- дать, что присваивание модифицирует глобальную переменную state
    . Од- нако изменяется лишь объект-исключение, созданный в выражении throw
    , поэтому модификация eObj не затрагивает state
    10.3 Раскрутка стека
    Поиск catch
    -обработчикадля возбужденного исключения происходит следующим образом. Когда выражение throw находится в try
    -блоке, все ассоциированные с ним предложения catch исследуются с точки зрения того, могут ли они обработать исключение. Если подходящее предложение catch найдено, то исключение обрабатывается. В противном случае поиск продолжается в вызывающей функции. Предположим, что вызов функции, выполнение которой прекратилось в результате исключения, погружен в try
    -блок; в такой ситуации исследуются все предложения catch
    , ассоции- рованные с этим блоком. Если один из них может обработать исключение, то процесс заканчивается. В противном случае переходим к следующей по порядку вызывающей функции. Этот поиск последовательно проводится во всей цепочке вложенных вызовов. Как только будет найдено подходящее предложение, управление передается в соответствующий обработчик.
    В нашем примере первая функция, для которой нужен catch
    -обработ- чик, – это функция-член pop()
    класса iStack
    . Поскольку выражение throw внутри pop()
    не находится в try
    -блоке, то программа покидает pop()
    , не

    86 обработав исключение. Следующей рассматривается функция, вызвавшая pop()
    , то есть main()
    . Вызов pop()
    внутри main()
    находится в try- блоке, и далее исследуется, может ли хотя бы одно ассоциированное с ним пред- ложение catch обработать исключение. Поскольку обработчик исключения popOnEmpty имеется, то управление попадает в него.
    Процесс, в результате которого программа последовательно покидает составные инструкции и определения функций в поисках предложения catch
    , способного обработать возникшее исключение, называется раскрут- кой стека. По мере раскрутки прекращают существование локальные объ- екты, объявленные в составных инструкциях и определениях функций, из которых произошел выход. C++ гарантирует, что во время описанного про- цесса вызываются деструкторы локальных объектов классов, хотя они исче- зают из-за возбужденного исключения.
    Если в программе нет предложения catch
    , способного обработать ис- ключение, оно остается необработанным. Но исключение – это настолько серьезная ошибка, что программа не может продолжать выполнение. По- этому, если обработчик не найден, вызывается функция terminate()
    из стандартной библиотеки C++. По умолчанию terminate()
    активизирует функцию abort()
    , которая аномально завершает программу. (В большин- стве ситуаций вызов abort()
    оказывается вполне приемлемым решением.
    Однако иногда необходимо переопределить действия, выполняемые функ- цией terminate()
    Вы уже, наверное, заметили, что обработка исключений и вызов функ- ции во многом похожи. Выражение throw ведет себя аналогично вызову, а предложение catch чем-то напоминает определение функции. Основная разница между этими двумя механизмами заключается в том, что информа- ция, необходимая для вызова функции, доступна во время компиляции, а для обработки исключений – нет. Обработка исключений в C++ требует языковой поддержки во время выполнения. Например, для обычного вызова функции компилятору в точке активизации уже известно, какая из перегру- женных функций будет вызвана. При обработке же исключения компилятор не знает, в какой функции находится catch
    -обработчик и откуда возобно- вится выполнение программы. Функция terminate()
    предоставляет меха- низм времени выполнения, который извещает пользователя о том, что под- ходящего обработчика не нашлось.

    87
    10.4 Повторное возбуждение исключений
    Может оказаться так, что в одном предложении catch не удалось пол- ностью обработать исключение. Выполнив некоторые корректирующие действия, catch
    -обработчик может решить, что дальнейшую обработку сле- дует поручить функции, расположенной «выше» в цепочке вызовов. Пере- дать исключение другому catch
    -обработчику можно с помощью повтор- ного возбуждения исключения. Для этой цели в языке предусмотрена кон- струкция throw
    , которая вновь генерирует объект-исключение. Повторное возбуждение возможно только внутри составной инструкции, являющейся частью catch
    -обработчика: catch ( exception eObj ) { if ( canHandle( eObj ) )
    // обработать исключение return; else
    // повторно возбудить исключение, чтобы его перехватил другой
    // catch-обработчик throw;
    }
    При повторном возбуждении новый объект-исключение не создается.
    Это имеет значение, если catch
    -обработчик модифицирует объект, прежде чем возбудить исключение повторно. В следующем фрагменте исходный объект-исключение не изменяется. Почему? enum EHstate { noErr, zeroOp, negativeOp, severeError }; void calculate( int op ) { try {
    // исключение, возбужденное mathFunc(), имеет значение zeroOp mathFunc( op );
    } catch ( EHstate eObj ) {
    // что-то исправить
    // пытаемся модифицировать объект-исключение eObj = severeErr;
    // предполагалось, что повторно возбужденное исключение будет
    // иметь значение severeErr throw;
    }
    }
    Так как eObj не является ссылкой, то catch
    -обработчик получает копию объекта-исключения, так что любые модификации eObj относятся к локаль- ной копии и не отражаются на исходном объекте-исключении, передавае- мом при повторном возбуждении. Таким образом, переданный далее объект по-прежнему имеет тип zeroOp

    88
    Чтобы модифицировать исходный объект-исключение, в объявлении ис- ключения внутри catch
    -обработчика должна фигурировать ссылка: catch ( EHstate &eObj ) {
    // модифицируем объект-исключение eObj = severeErr;
    // повторно возбужденное исключение имеет значение severeErr throw;
    }
    Теперь eObj ссылается на объект-исключение, созданный выражением throw
    , так что все изменения относятся непосредственно к исходному объ- екту. Поэтому при повторном возбуждении исключения далее передается модифицированный объект.
    Таким образом, другая причина для объявления ссылки в catch
    -обработ- чике заключается в том, что сделанные внутри обработчика модификации объекта-исключения в таком случае будут видны при повторном возбужде- нии исключения
    10.5 Обработка исключений в С
    В эпоху расцвета процедурного программирования синтаксис работы с ошибками был тривиален и основывался на том, что вернула функция.
    Если функция возвращала
    TRUE
    – все хорошо, если же
    FALSE
    – то произо- шла ошибка. При этом сразу выделились два подхода к работе с ошиб- ками:

    Подход два в одном – функция возвращает
    FALSE
    или нулевой указа- тель как для ожидаемой, так и для неожиданной ошибки. Такой подход как правило применялся в API общего назначения и коде пользовательских про- грамм, когда большую часть ошибок можно было смело считать фаталь- ными и падать. Для тех редких случаев когда делить было все же нужно использовалась некая дополнительная машинерия вида
    GetLastError()
    Фрагмент кода того времени, копирующего данные из одного файла в дру- гой и возвращающего ошибку в случае возникновения любых проблем:
    BOOL Copy( CHAR* sname, CHAR* dname ) {
    FILE *sfile = 0, *dfile = 0; void* mem = 0;
    UINT32 size = 0, written = 0;
    BOOL ret = FALSE; sfile = fopen( sname, "rb" ); if( ! sfile ) goto cleanup; dfile = fopen( dname, "wb" );

    89 if( ! dfile ) goto cleanup; mem = malloc( F_CHUNK_SIZE ); if( ! mem ) goto cleanup; do
    { size = fread( sfile, mem, F_CHUNK_SIZE ); written = fwrite( dfile, mem, size ); if( size != written ) goto cleanup;
    } while( size ) ret = TRUE; cleanup: // Аналог деструктора. if( sfile) fclose( sfile ); if( dfile) fclose( dfile ); if( mem ) free( mem ); return ret; // Ожидаемая ошибка.
    }

    Подход разделения ошибок, при котором функция возвращает
    FALSE
    в случае неожиданной ошибки, а ожидаемую ошибку возвращает отдель- ным возвращаемым значением (в примере это error), если нужно. Такой под- ход применялся в более надежном коде, например apache, и подразумевал разделение на ожидаемые ошибки (файл не получилось открыть потому что его нет) и неожиданные (файл не получилось открыть потому, что закончи- лась память и не получилось выделить 20 байт чтобы скопировать строку с именем). Фрагмент того же код, но уже разделяющего неожиданную ошибку (возврат
    FALSE
    ) и ожидаемую (возврат
    HANDLE
    ).
    BOOL Copy( CHAR* sname, CHAR* dname, OUT HANDLE* error ) {
    HANDLE sfile = 0, dfile = 0, data = 0;
    UINT32 size = 0;
    ENSURE( PoolAlloc() ); // Обработка неожиданной ошибки.
    ENSURE( FileOpen( sname, OUT& sfile, OUT error ) );
    REQUIRE( SUCCESS( error ) ); // Обработка ожидаемой ошибки.
    ENSURE( FileOpen( dname, OUT& dfile, OUT error ) );
    REQUIRE( SUCCESS( error ) );
    ENSURE( MemAlloc( OUT& data ) );
    REQUIRE( SUCCESS( error ) ); do
    {
    ENSURE( FileRead( sfile, F_CHUNK_SIZE, OUT& data, OUT error ) );
    REQUIRE( SUCCESS( error ) );
    ENSURE( FileWrite( dfile, & data ) );
    REQUIRE( SUCCESS( error ) );
    ENSURE( MemGetSize( OUT& size ) )
    } while( size );

    90
    ENSURE( PoolFree() ); // Пул обеспечивает аналог деструкторов и RAII. return TRUE;
    }
    Через некоторое время разработчики заметили, что большинство успеш- ных решений использует ООП и решили, что неплохо бы его вынести в син- таксис языка, дабы писать больше кода по делу и меньше – повторяющегося кода для поддержки архитектуры.
    Давайте возьмем код выше и посмотрим, как он трансформировался по- сле добавления ООП в синтаксис языков программирования. Конструиро- вание и уничтожение объектов (
    fopen
    , fclose
    ) стало конструкторами и де- структорами. Переброс неожиданной ошибки (
    BOOL ret в первом примере, макрос
    ENSURE
    во втором) однозначно стал исключением.
    А вот с ожидаемой ошибкой случилось самое интересное – случился вы- бор. Можно было использовать возвращаемое значение – теперь, когда за- боту о неожиданных ошибках взяли на себя исключения, возвращаемое зна- чение снова стало в полном распоряжении программиста. А можно было ис- пользовать исключения другого типа – если функции копирования файлов самой не нужно обрабатывать ожидаемые ошибки то логично вместо if и
    REQUIRE
    просто ничего не делать – и оба типа ошибок уйдут вверх по стеку.
    Соответственно, у программистов снова получилось два варианта:

    Подход только исключения – ожидаемые и неожиданные ошибки – это разные типы исключений. void Copy( string sname, string dname ) { file source( sname ); file destination( sname ); source.open( "rb" ); destination.open( "wb" ); data bytes; do
    { bytes = source.read( F_CHUNK_SIZE ); destination.write( bytes )
    } while( bytes.size() )
    }

    Комбинированный подход – использование исключений для неожи- данных ошибок и кодов возврата / nullable типов для ожидаемых: bool Copy( string sname, string dname ) { file source( sname ); file destination( sname ); if( ! source.open( "rb" ) || ! destination.open( "wb" ) ) return false;

    91 data bytes; do
    { bytes = source.read( F_CHUNK_SIZE ); if( bytes.isValid() )
    { if( ! destination.write( bytes ) ) return false;
    }
    } while( bytes.isValid() && bytes.size() )
    }
    Итак, если внимательно посмотреть на два приведенных выше фраг- мента кода то становится не совсем понятно почему выжил второй. Кода в нем объективно больше. Выглядит менее красиво. Если функция возвра- щает объект – то использовать коды возврата совсем неудобно. Вопрос – почему коды возврата вообще выжили в языках с поддержкой объектно- ориентированного программирования и исключений на уровне синтаксиса?
    Что я могу по этому поводу сказать:
    Первые реализации исключений, особенно в C++, были не очень удобны для ежедневного использования. Например, бросание исключения во время обработки другого исключения приводил к завершению программы. Или же бросание исключения в конструкторе приводило к тому, что деструктор не вызывался.
    Разработчикам API забыли объяснить для чего нужны исключения. В ре- зультате первое время не было даже деления на ожидаемые (
    checked
    ) и неожиданные (
    unchecked
    ), а API комбинировали как исключения, так и коды возврата.
    В большинстве языков для исключений забыли добавить семантику «иг- норировать ожидаемую ошибку». В результате на практике код, использу- ющий исключения как для ожидаемых так и для неожиданных ошибок, с невероятной скоростью обрастал try и catch везде, где только можно.
    1   ...   4   5   6   7   8   9   10   11   ...   15


    написать администратору сайта