Альтернативные решения, заменяющие dynamic_cast. Виртуальные функции для понижающего преобразования. Типовое решение Visitor

Альтернативные решения, заменяющие dynamic_cast. Виртуальные функции для понижающего преобразования. Типовое решение Visitor.

Простейшее решение, показанное ниже, работает в среднем от 2 до 7 раз быстрее типичной реализации оператора dynamic_cast, однако требует заметного объема дополнительных ручных действий со стороны программиста. В базовом классе следует определить по одному виртуальному методу преобразования к каждому конкретному классу иерархии. В базовом классе эти функции всегда должны возвращать значение nullptr:

 

class FilesystemEntry

{

// ...

public:

// ...

virtual File * asFile () { return nullptr; }

virtual Directory * asDirectory () { return nullptr; }

};

 

В каждом конкретном производном классе иерархии следует переопределить один из этих методов преобразования, соответствующий классу. Файл будет переопределять метод преобразования в файл, а каталог - в каталог:

 

class File : public FilesystemEntry

{

// ...

public:

// ...

File * asFile () override { return this; }

};

 

class Directory : public FilesystemEntry

{

// ...

public:

// ...

Directory * asDirectory () override { return this; }

};

 

Теперь можно воспользоваться этими методами вместо оператора dynamic_cast:

 

FilesystemEntry * pEntry = new File( “1.txt”, 50 );

File * pFile = pEntry->asFile();

assert( pFile );

 

При добавлении нового класса в иерархию (например, SymbolicLink - символическая ссылка), будет нужно добавить к базовому классу FilesystemEntry еще один метод преобразования для нового конкретного класса.

 

Лучшую гибкость обеспечивает типовое решение Visitor (посетитель), при этом не засоряя иерархию прикладными функциями. Оригинальную иерархию дополняют таким необычным интерфейсом, в котором имеется по одной чисто виртуальной функции visit, принимающей в качестве единственного аргумента ссылку на один из конкретных классов иерархии.В базовом классе следует ввести одну дополнительную операцию - прием посетителя - в виде чисто виртуальной функции.

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

Такой подход имеет ряд преимуществ:

· получена функционально идентичная реализация, в которой нет ни одного оператора преобразования типа вниз по иерархии;

· классы предметной области (FilesystemEntry, File, Directory) не засорены конкретными элементами реализации прикладного характера;

· если потребуется другая прикладная функциональность, будет достаточно определить еще одну реализацию интерфейса-посетителя в подобном стиле;

· при расширении иерархии модели новым классом, например, SymbolicLink, не удастся забыть добавить реализацию обработки таких объектов, поскольку благодаря комбинации чисто виртуальных функций accept/visit не удастся скомпилировать проект:

o без реализации метода accept нельзя будет создать объект SymbolicLink;

o решение этой проблемы потребует добавления нового метода visit в интерфейс посетителя;

o без добавления реализации этого нового метода не будет возможности создать ни один из объектов конкретного класса-посетителя;

 

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

 

Обработка исключений. Цели, синтаксис выброса и обработчиков. Выбор обработчика по типу. Передача данных исключения по значению, указателю и ссылке. Исключения языка и стандартной библиотеки.

 

Обработка исключений – поиск и устранение ошибок.

 

Основные принципы механизма исключений:

 

- разделение кода нормального функционирования и кода обработки ошибок;

- функция, обнаруживающая ошибку, транспортирует информацию о возникшей проблеме в место обработки;

- возможность выбора между тонкой обработкой конкретного типа ошибки и более широкой группы ситуаций одним или несколькими обработчиками общего назначения;

- малый расход при нормальном выполнении программы .

 

Синтаксис выброса и обработчиков. Блок try-catch :

 

try {“выброс” -- что-либо, генерирующее неподходящую инфу, которая призведёт к ошибке выполнения осн. функции,т.е. создание исключения}

catch (тип исключения) { “обработчик” }

 

После выхода из блока catch работа проги возобновляеться, если на выброшеное исключение не нашелся подходящий блок обработки catch, среда обработки вызовет ф-ю terminate, которая вызовет ф-ю abort, это неловко остановит прогу.

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

 

Разные типы ошибок = много блоков catch. Это выглядит так:

 

try {
// код
}
catch (int param) { cout << "int exception"; }
catch (char param) { cout << "char exception"; }
catch (...) { cout << "default exception"; }

 

Обработчики исключений выбираются в порядке, в котором они записаны в коде по типу, указанному в () после catch, как при вызове ф-ии. Управление передается в тот блок, тип исключения которого признается подходящим первым. Типы сопоставляются по следующему принципу:

● фактический тип исключения точно совпадает с типом в обработчике;

● тип в обработчике является однозначным базовым классом для фактического типа;

● если фактический тип исключения является указателем, а тип в обработчике является точно таким же указателем либо указателем на однозначный базовый класс;

● фактический тип исключения является простым копируемым объектом либо ссылкой, а тип в обработчике является ссылкой на это же тип либо ссылкой на однозначный базовый класс;

● на сопоставление не влияет дополнительный модификатор const в типе, указываемом в обработчике исключения

 

Так же возможен перехват всех типов исключений:

catch( ... ) троеточие вместо типа исключения = все типы

{

std::cerr << “Fatal error. Please contact support@company.com” << std::endl; }

 

Oбработчики всегда нужно писать от наиболее конкретных классов к базовым.

Если catch (...) и catch(тип исключения) сосуществуют, то обработчик всех типов нужно записать последним, т.к. он примет любой тип.

 

Оператор throw используеться для генерации исключительных ситуаций

try { throw 20;}, к примеру.

 

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

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

 

 

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

 

voidrunProgram ()

{

Try

{

ManagedIntegerArray a( 10 );

a[ rand() ] = 25;

}

catch( ManagedIntegerArray::IndexOutOfRange & e )

{

std::cout << “Program has a problem with array index” << std::endl;

}
}

 

intmain ()

{

Try

{

runProgram();

}

catch( ... )

{

std::cout << “Program has some unknown problem” << std::endl;

}
}

 

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

 

Иногда стратегия многоуровневой обработки ошибок состоит в поэтапном восстановлении работоспособности. Ближайший к месту выброса исключения обработчик catch делает максимум возможного восстановления на своем уровне, но после должен передать управление на следующий уровень, поскольку полностью устранить последствия возникновения ошибки не удается. В таком случае применяется повторная генерация исключения (rethrow), инструкция throw пишется без аргументов. При этом те же самые данные исключения передаются далее по цепочке на обработку:

 

voidrunProgram ()

{

Try

{

ManagedIntegerArray a( 10 );

a[ rand() ] = 25;

}

catch( ManagedIntegerArray::IndexOutOfRange & e )

{

// Локальная обработка

std::cout << “Program has a problem with array index” << std::endl;

 

// Повторная генерация того же исключения

throw;

}
}

 

Если повторную генерацию инициировать в момент, когда обработки исключения на самом деле не происходит (синтаксис языка позволяет написать инструкцию throw без аргументов в теле любой функции), то это приведет к немедленному вызову функции termin

 

Передача и перехват указателя:

 

Try

{

throw newDerivedException( 0, 0 );

}

catch( DerivedException * _pE )

{

delete_pE;

}

 

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

 

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

 

Исключения стандартной библиотеки. . Все исключения, выбрасываемые операторами языка и средствами стандартной библиотеки, образуют иерархию наследования классов, вершиной которой является класс std::exception.

Определение класса std::exception и большинства производных от него классов становится доступным после включения заголовочного файла <exception>. std::exception является конкретным классом, объекты которого могут быть непосредственно созданы. Такой объект содержит минимальную функциональность, а именно:

● конструктор по умолчанию;

● конструктор копий и оператор присвоения;

● виртуальный деструктор;

● виртуальный метод what(), возвращающий строку в стиле C, описывающую исключение.

Производные классы могут переопределять метод what() для выдачи более информативных сообщений.

 

Часто используемые исключения:

● исключение std::bad_alloc выбрасывается оператором new, когда он не может обеспечить выделение запрошенного блока динамической памяти;

 

● исключение std::bad_cast выбрасывается оператором dynamic_cast, когда запрошенного преобразования не существует, а работа ведется со ссылками, а не с укаталеями;

 

● исключение std::bad_typeid выбрасывается оператором typeid, если его вызвать на нулевом указателе полиморфного типа;

 

● исключение std::out_of_range выбрасывается контейнерами std::vector и std::deque при попытке обращения к ячейке по некорректному индексу;

 

● исключение std::ios_base::failure выбрасывается библиотекой ввода/вывода в ряде ошибочных ситуаций - это поведение отключено по умолчанию, чтобы его включить, необходимо вызвать метод exceptions, и тогда потоки в ошибочных ситуациях будут генерировать исключения:

 

std::ifstream file;
file.exceptions ( std::ifstream::failbit | std::ifstream::badbit );

 

Стандартные классы исключений std::logic_error и std::runtime_error принимают в качестве аргумента при создании строку std::string, описывающую возникшую проблему. По умолчанию, метод what() возвращает именно эту строку, что делает такие классы довольно удобными для обработки исключений в простых программах.

 

Класс std::logic_error предполагается использовать для ошибок внутренней логики программы, таких как нарушение логических предусловий и инвариантов. Такие ошибки являются, как правило, исправимыми в коде программы до ее запуска. Класс std::runtime_error, напротив, предназначен для ошибок, которые можно обнаружить только во время выполнения, например ошибки переполнения АТД “стек” фиксированного размера. Предполагается, что эти классы будут базовой отправной точкой для пользовательских классов-исключений. В прочем, часто пользовательские классы-исключения наследуют непосредственно от вершины иерархии - класса std::exception.

 

Процесс stack unwinding при выбросе исключений. Уничтожение полностью сконструированных локальных объектов. Утечки памяти при выбросе исключений. Исключения в конструкторах и деструкторах. Повторная генерация исключений.

 

Процесс поиска обработчика сгенерированного исключения по стеку вызовов функций называется stack unwinding (дословно, “разматывание” или “раскрутка” стека).

Собственно, в рамках этого процесса решаются 2 основные задачи:

1. Поиск подходящего обработчика исключения по стеку вызовов.

2. Вызов деструкторов локальных объектов.

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

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

Рассмотрим следующий простейший пример:

 

voidf ()

{

Date d1;

if( d1.GetDay() == 1 )

throwstd::runtime_error( “Cannot call f() on the 1st day of the month” );

 

Date d2( 2013, 12, 23 );

// ...

}

 

В зависимости от текущего числа, может быть сгенерировано исключение. К этому моменту локальный объект d1 уже сконструирован, в то время как объект d2 еще не сконструирован. Механизм stack unwinding должен гарантировать, что в таком случае будет вызван деструктор объекта d1, но не будет вызван деструктор объекта d2.

 

Для реализации такого поведения весьма полезным свойством языка является вызов деструкторов локальных объектов в обратном конструированию порядке. В частности, в обычной ситуации, когда исключения не происходит, при выходе потока управления из функции f, компилятор будет вызывать деструктор сначала на объекте d2, а затем на объекте d1:

 

voidf ()

{

Date d1;

if( d1.GetDay() == 1 )

throwstd::runtime_error( “Cannot call f() on the 1st day of the month” );

 

Date d2( 2013, 12, 23 );

// ...

 

// Date::~Date( & d2 );

// Date::~Date( & d1 );

}

 

Эта особенность чрезвычайна удобна для организации в компиляторе очистки при выбросе исключения. В случае выброса исключения в части функции, когда объект d1 уже сконструирован, а объект d2 еще нет, необходимо каким-либо образом “перескочить” вызовы деструкторов объектов, которые еще не сконструированы, и продолжить начиная с деструкторов, которые нужно вызвать.

 

Любую функцию условно можно условно разбить на зоны. В данном конкретном примере 3 зоны:

● зона 0 - не сконструировано ни одного объекта;

● зона 1 - сконструирован объект d1, но не d2;

● зона 2 - сконструированы объекты d1 и d2.

Чтобы упростить переход к нужному вызову деструктора, типичный компилятор резервирует на стеке каждом блоке инструкций специальную переменную (обычно, размером 1 байт), играющую роль текущего номера зоны. Как только один из объектов завершает конструирование, номер зоны увеличивается. По этому номеру зоны библиотека времени выполнения и определяет с какого деструктора следует начать очистку стека.

 

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

 

voidf ()

{

Date * pDate = newDate();

 

Try

{

if( pDate->GetDay() == 1 )

throwstd::runtime_error( “...” );

 

// ...

 

// Нормальное освобождение ресурса

deletepDate;

}

catch (... )

{

// Аварийное освобождение ресурса

deletepDate;

 

// Повторная генерация исключения

throw;

}

}

 

voidg ()

{

FILE * file = fopen( “test.txt”, “rt” );
try

{

if( some_condition )

throwstd::runtime_error( “Condition failed” );

 

// …

 

// Нормальное освобождение ресурса

fclose(file);

}

catch (... )

{

// Аварийное освобождение ресурса

fclose(file);

 

// Повторная генерация исключения

throw;

}

}

 

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

 

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

 

Если исключение генерируется при конструировании объекта, он считается неполностью сконструированным, и его деструктор не вызывается.Например, класс Date генерирует исключения, если передать неправильные компоненты даты:

Try

{

Date d1( 2013, 14, 1 ); // 14 месяц не существует

 

// До вызова деструктора выполнение не дойдет:

// Date::~Date( & d1 );

}

catch( ... )

{

}

 

В случае динамического выделения памяти для нового объекта и появления исключения в конструкторе, во избежание явной утечки памяти, будет автоматически вызван оператор delete:

 

Try

{

Date * pDate = newDate( 2013, 14, 1 ); // 14 месяц не существует

 

// До вызова деструктора выполнение не дойдет, но будет вызван оператор delete

}

catch( ... )

{

}

 

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

 

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

 

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

 

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

 

voidrunProgram ()

{

Try

{

ManagedIntegerArray a( 10 );

a[ rand() ] = 25;

}

catch( ManagedIntegerArray::IndexOutOfRange & e )

{

// Локальная обработка

std::cout << “Program has a problem with array index” << std::endl;

 

// Повторная генерация того же исключения

throw;

}
}

 

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