10.6 Исключительные ситуации в конструкторах и деструкторах
Конструкторы не могут возвращать коды ошибок, соответственно ис- ключения – это единственный метод, чтобы понять, что в конструкторе что- то пошло не так. Однако, необходимо правильно обрабатывать исключи- тельные ситуации в таких случаях. Особенно если в конструкторе формиру- ются объекты в динамической памяти.
Пример исключительной ситуации в конструкторе: class Test { public:
92
Test() { std::cout << "Test::Test()" << std::endl;
// Здесь, в соответствии с RAII, захватили ресурсы if ( 1 ) { throw std::runtime_error( "AAAAAAAA" );
} else {}
}
Test() { std::cout << "Test::Test()" << std::endl;
// А здесь мы освобождаем те самые важные ресурсы...
}
}; int main() {
Test* t = 0; try { t = new Test(); // Вроде бы создали...
} catch ( const std::exception& exc ) { std::cout << exc.what() << std::endl;
} delete t; // Удалили? Ну-ну... return 0;
}
Не стоит пытаться передавать исключения за тело конструктора, т.к. иначе, даже если мы не забываем вызвать delete
, он не будет вызывать де- структор неправильно созданного объекта. В этом случае, никакие ресурсы, которые были инициализированы в конструкторе до вызова исключения
(связи с БД, открытые файлы, и т.п.) уже никогда не будут освобождены
(если они создавались в динамической памяти).
Вместо указателей на объекты в динамической памяти можно использо- вать один из множества типов «умных указателей», которые удаляются ав- томатически при выходе из области видимости (как локальные объекты):
// С указателями на объекты в
// динамической памяти: class Cnt { private:
X *xa;
X *xb; public:
Cnt(int a, int b) { cout << "Cnt::Cnt" << endl; xa = new X(a); xb = new X(b);
}
Cnt() { cout << "Cnt::Cnt" << endl; delete xa; delete xb;
}
};
// С умными указателями: class Cnt { private: auto_ptr ia; auto_ptr ib; public:
Cnt(int a, int b) : ia(new
X(a)), ib(new X(b)) { cout << "Cnt::Cnt" << endl;
}
Cnt() { cout << "Cnt::Cnt" << endl;
}
};
93 std::auto_ptr<>
– реализует семантику владения. Так называемое раз- рушающее копирование – при присваивании, объект передается от одного указателя другому, удаляясь у первого, чтобы не удалять объект дважды при выходе из области видимости.
Пример исключительной ситуации в деструкторе: class test
{ public: test() { }
test(){ throw std::runtime_error("Game over!");
}
}; int main() { try { test t; throw std::runtime_error("Error!");
} catch(std::exception const&)
{ } return 0;
}
Когда исключение покидает блок, все локальные объекты, созданные в этом блоке, уничтожаются. Если деструктор объекта, уничтожаемого во время развертки стека, генерирует исключение, то программа будет завер- шена досрочно, и ее уже ничего не спасет – вызывается функция terminate
Исключение при раскрутке стека всегда будет инициализировать функ- цию terminate()
– обрабатывайте все ошибки деструктора внутри деструк- тора! Ни в коем случае не инициализируйте исключение!
94
11. ИДЕНТИФИКАЦИЯ ТИПОВ ВО ВРЕМЯ ВЫПОЛНЕНИЯ RTTI
идентификация типов во время выполнения (Run-time Type
Identification), позволяет программам, которые манипулируют объектами через указатели или ссылки на базовые классы, получить истинный произ- водный тип адресуемого объекта. Для поддержки RTTI в языке C++ есть два оператора:
оператор dynamic_cast поддерживает преобразования типов во время выполнения, обеспечивая безопасную навигацию по иерархии клас- сов. Он позволяет трансформировать указатель на базовый класс в указатель на производный от него, а также преобразовать l-значение, ссылающееся на базовый класс, в ссылку на производный, но только в том случае, если это завершится успешно;
оператор typeid позволяет получить фактический производный тип объекта, адресованного указателем или ссылкой.
Однако для получения информации о типе производного класса операнд любого из операторов dynamic_cast или typeid должен иметь тип класса, в котором есть хотя бы одна виртуальная функция. Таким образом, опера- торы RTTI
это события времени выполнения для классов с виртуальными функциями и события времени компиляции для всех остальных типов. В данном разделе мы более подробно познакомимся с их возможностями. Ис- пользование RTTI оказывается необходимым при реализации таких прило- жений, как отладчики или объектные базы данных,
когда тип объектов, ко- торыми манипулирует программа, становится известен только во время вы- полнения путем исследования RTTI-информации, хранящейся вместе с ти- пами объектов. Однако лучше пользоваться статической системой типов
C++, поскольку она безопаснее и эффективнее.
11.1 Оператор dynamic_cast Оператор dynamic_cast можно применять для преобразования указа- теля, ссылающегося на объект типа класса в указатель на тип класса из той же иерархии. Его также используют для трансформации l-значения объекта типа класса в ссылку на тип класса из той же иерархии. Приведение типов с помощью оператора dynamic_cast
, в отличие от других имеющихся в C++ способов, осуществляется во время выполнения программы. Если указатель или l-значение не могут быть преобразованы в целевой тип, то dynamic_cast завершается неудачно. В случае приведения типа указателя признаком неудачи служит возврат нулевого значения. Если же l-значение
95 нельзя трансформировать в ссылочный тип, возбуждается исключение.
Ниже мы приведем примеры неудачного выполнения этого оператора.
Прежде чем перейти к более детальному рассмотрению dynamic_cast
, посмотрим, зачем его нужно применять. Предположим, что в программе ис- пользуется библиотека классов для представления различных категорий служащих компании. Входящие в иерархию классы поддерживают функ- ции-члены для вычисления зарплаты: class employee { public: virtual int salary();
}; class manager : public employee { public: int salary();
}; class programmer : public employee { public: int salary();
}; void company::payroll( employee *pe ) {
// используется pe->salary()
}
В компании есть разные категории служащих. Параметром функции- члена payroll()
класса company является указатель на объект employee
, который может адресовать один из типов manager или programmer
. По- скольку payroll()
обращается к виртуальной функции-члену salary()
, то вызывается подходящая замещающая функция, определенная в классе manager или programmer
, в зависимости от того, какой объект адресован указателем.
Допустим, класс employee перестал удовлетворять нашим потребно- стям, и мы хотим его модифицировать, добавив еще одну функцию-член bonus()
, используемую совместно с salary()
при расчете платежной ведо- мости. Для этого нужно включить новую функцию-член в классы, составля- ющие иерархию employee
: class employee { public: virtual int salary(); virtual int bonus();
}; class manager : public employee { public: int salary();
};
96 class programmer : public employee { public: int salary(); int bonus();
}; void company::payroll( employee *pe ) {
// eniieucoaony pe->salary() e pe->bonus()
}
Если параметр pe функции payroll()
указывает на объект типа manager
, то вызывается виртуальная функция-член bonus()
из базового класса employee
, поскольку в классе manager она не замещена. Если же pe указывает на объект типа programmer
, то вызывается виртуальная функция- член bonus()
из класса programmer
После добавления новых виртуальных функций в иерархию классов при- дется перекомпилировать все функции-члены. Добавить bonus()
можно, если у нас есть доступ к исходным текстам функций-членов в классах employee
, manager и programmer
. Однако если иерархия была получена от независимого поставщика, то не исключено, что в нашем распоряжении имеются только заголовочные файлы, описывающие интерфейс библиотеч- ных классов и объектные файлы с их реализацией, а исходные тексты функ- ций-членов недоступны. В таком случае перекомпиляция всей иерархии не- возможна.
Если мы хотим
расширить функциональность библиотеки классов, не добавляя новые виртуальные функции-члены, можно воспользоваться опе- ратором dynamic_cast
Этот оператор применяется для получения указателя на производный класс, чтобы иметь возможность работать с теми его элементами, которые по-другому не доступны. Предположим, что мы расширяем библиотеку за счет добавления новой функции-члена bonus()
в класс programmer
. Ее объ- явление можно включить в определение programmer
, находящееся в заголо- вочном файле, а саму функцию определить в одном из своих исходных фай- лов: class employee { public: virtual int salary();
}; class manager : public employee { public: int salary();
}; class programmer : public employee {
97 public: int salary(); int bonus();
};
Напомним, что payroll()
принимает в качестве параметра указатель на базовый класс employee
. Мы можем применить оператор dynamic_cast для получения указателя на производный programmer и воспользоваться им для вызова функции-члена bonus()
: void company::payroll( employee *pe )
{ programmer *pm = dynamic_cast< programmer* >( pe );
// anee pe oeacuaaao ia iauaeo oeia programmer,
// oi dynamic_cast auiieieony oniaoii e pm aoaao
// oeacuaaou ia ia?aei iauaeoa programmer if ( pm ) {
// eniieuciaaou pm aey auciaa programmer::bonus()
}
// anee pe ia oeacuaaao ia iauaeo oeia programmer,
// oi dynamic_cast auiieieony iaoaa?ii
// e pm aoaao niaa??aou 0 else {
// eniieuciaaou ooieoee-?eaiu eeanna employee
}
}
Оператор dynamic_cast< programmer* >( pe )
приводит свой операнд pe к типу programmer*
. Преобразование будет успешным, если pe ссыла- ется на объект типа programmer
, и неудачным в противном случае: тогда результатом dynamic_cast будет 0.
Таким образом, оператор dynamic_cast осуществляет сразу две опера- ции. Он проверяет, выполнимо ли запрошенное приведение, и если это так, выполняет его. Проверка производится во время работы программы. dynamic_cast безопаснее, чем другие операции приведения типов в C++, поскольку проверяет возможность корректного преобразования.
Если в предыдущем примере pe действительно указывает на объект типа programmer
, то операция dynamic_cast завершится успешно и pm будет инициализирован указателем на объект типа programmer
. В противном слу- чае pm получит значение 0. Проверив значение pm
, функция company::payroll()
может узнать, указывает ли pm на объект programmer
Если это так, то она вызывает функцию-член programmer::bonus()
для вы- числения премии программисту. Если же dynamic_cast завершается не- удачно, то pe указывает на объект типа manager
, а значит, необходимо при- менить более общий алгоритм расчета, не использующий новую функцию- член programmer::bonus()
98
Оператор dynamic_cast употребляется для безопасного приведения ука- зателя на базовый класс к указателю на производный. Такую операцию ча- сто называют понижающим приведением (
downcasting
). Она применяется, когда необходимо воспользоваться особенностями производного класса, от- сутствующими в базовом. Манипулирование объектами производного класса с помощью указателей на базовый обычно происходит автоматиче- ски, с помощью виртуальных функций. Однако иногда использовать вирту- альные функции невозможно. В таких ситуациях dynamic_cast предлагает альтернативное решение, хотя этот механизм в большей степени подвержен ошибкам, чем виртуализация, и должен применяться с осторожностью.
Одна из возможных ошибок – это работа с результатом dynamic_cast без предварительной проверки на 0: нулевой указатель нельзя использовать для адресации объекта класса. Например: void company::payroll( employee *pe )
{ programmer *pm = dynamic_cast< programmer* >( pe );
// iioaioeaeuiay ioeaea: pm eniieucoaony aac i?iaa?ee cia?aiey static int variablePay = 0; variablePay += pm->bonus();
// ...
}
Результат, возвращенный dynamic_cast
,
всегда следует проверять, прежде чем использовать в качестве указателя. Более правильное определе- ние функции company::payroll()
могло бы выглядеть так: void company::payroll( employee *pe )
{
// auiieieou dynamic_cast e i?iaa?eou ?acoeuoao if ( programmer *pm = dynamic_cast< programmer* >( pe ) ) {
// eniieuciaaou pm aey auciaa programmer::bonus()
} else {
// eniieuciaaou ooieoee-?eaiu eeanna employee
}
}
Результат операции dynamic_cast используется для инициализации пе- ременной pm внутри условного выражения в инструкции if
. Это возможно, так как объявления в условиях возвращают значения. Ветвь, соответствую- щая истинности условия, выполняется, если pm не равно нулю: мы знаем, что операция dynamic_cast завершилась успешно и pe указывает на объект programmer
. В противном случае результатом объявления будет 0 и выпол- няется ветвь else
. Поскольку теперь оператор и проверка его результата
99 находятся в одной инструкции программы, то невозможно случайно вста- вить какой-либо код между выполнением dynamic_cast и проверкой, так что pm будет использоваться только тогда, когда содержит правильный ука- затель.
В предыдущем примере операция dynamic_cast преобразует указатель на базовый класс в указатель на производный. Ее также можно применять для трансформации l-значения типа базового класса в ссылку на тип произ- водного. Синтаксис такого использования dynamic_cast следующий: dynamic_cast< Type& >( lval )
Здесь
Type&
– это целевой тип преобразования, а lval
– l-значение типа базового класса. Операнд lval успешно приводится к типу
Type&
только в том случае, когда lval действительно относится к объекту класса, для ко- торого один из производных имеет тип
Type
Поскольку нулевых ссылок не бывает, то проверить успешность выпол- нения операции путем сравнения результата (т.е. возвращенной оператором dynamic_cast ссылки) с нулем невозможно. Если вместо указателей ис- пользуются ссылки, тогда следующее условие: if ( programmer *pm = dynamic_cast< programmer* >( pe ) )
Нельзя переписать в виде: if ( programmer &pm = dynamic_cast< programmer& >( pe ) )
Для извещения об ошибке в случае приведения к ссылочному типу опе- ратор dynamic_cast возбуждает исключение. Следовательно, предыдущий пример можно записать так:
#include < typeinfo> void company::payroll( employee &re )
{ try { programmer &rm = dynamic_cast< programmer & >( re );
// eniieuciaaou rm aey auciaa programmer::bonus()
} catch ( std::bad_cast ) {
// eniieuciaaou ooieoee-?eaiu eeanna employee
}
}
В случае неудачного завершения ссылочного варианта dynamic_cast возбуждается исключение типа bad_cast
. Класс bad_cast определен в стандартной библиотеке; для ссылки на него необходимо включить в про- грамму заголовочный файл. (Исключения из стандартной библиотеки мы будем рассматривать в следующем разделе.)
100
Когда следует употреблять ссылочный вариант dynamic_cast вместо указательного? Это зависит только от желания программиста. При его ис- пользовании игнорировать ошибку приведения типа и работать с результа- том без проверки (как в указательном варианте) невозможно; с другой сто- роны, применение исключений увеличивает накладные расходы во время выполнения программы.
11.2 Оператор typeid Второй оператор, входящий в состав RTTI, – это typeid
, который позво- ляет выяснить фактический тип выражения. Если оно принадлежит типу класса и этот класс содержит
хотя бы одну виртуальную функцию-член, то ответ может и не совпадать с типом самого выражения. Так, если выражение является ссылкой на базовый класс, то typeid сообщает тип производного класса объекта:
#include < typeinfo> programmer pobj; employee &re = pobj;
// n ooieoeae name() iu iiciaeiieiny a iia?acaaea, iinayuaiiii type_info
// iia aica?auaao C-no?ieo "programmer" coiut <
Операнд re оператора typeid имеет тип employee
. Но так как re
– это ссылка на тип класса с виртуальными функциями, то typeid говорит, что тип адресуемого объекта – programmer
(а не employee
, на который ссыла- ется re
). Программа, использующая такой оператор, должна включать заго- ловочный файл, что мы и сделали в этом примере.
Где применяется typeid
? В сложных системах разработки, например при построении отладчиков, а также при использовании устойчивых объек- тов, извлеченных из базы данных. В таких системах необходимо знать фак- тический тип объекта, которым программа манипулирует с помощью указа- теля или ссылки на базовый класс, например для получения списка его свойств во время сеанса работы с отладчиком или для правильного сохране- ния или извлечения объекта из базы данных. Оператор typeid допустимо использовать с выражениями и именами любых типов. Например, его опе- рандами могут быть выражения встроенных типов и константы. Если опе- ранд не принадлежит к типу класса, то typeid просто возвращает его тип: int iobj; cout << typeid( iobj ).name() << endl; // ia?aoaaony: int
101 cout << typeid( 8.16 ).name() <
Если операнд имеет тип класса, в котором нет виртуальных функций, то typeid возвращает тип операнда, а не связанного с ним объекта: class Base { /* нет виртуальных функций */ }; class Derived : public Base { };
Derived dobj;
Base *pb = &dobj; cout <Операнд typeid имеет тип
Base
, т.е. тип выражения
*pb
. Поскольку в классе
Base нет виртуальных функций, результатом typeid будет
Base
, хотя объект, на который указывает pb
, имеет тип
Derived
Результаты, возвращенные оператором typeid
, можно сравнивать.
Например:
#include employee *pe = new manager; employee& re = *pe; if ( typeid( pe ) == typeid( employee* ) )
// ?oi-oi naaeaou
/* if ( typeid( pe ) == typeid( manager* ) ) if ( typeid( pe ) == typeid( employee ) ) if ( typeid( pe ) == typeid( manager ) )
*/
Условие в инструкции if сравнивает результаты применения typeid к операнду, являющемуся выражением, и к операнду, являющемуся именем типа. Обратите внимание, что сравнение возвращает истину: typeid( pe ) == typeid( employee* )
Это удивит пользователей, привыкших писать:
// вызов виртуальной функции pe->salary();
Это приводит к вызову виртуальной функции salary()
из производного класса manager
. Поведение typeid(pe) не подчиняется данному меха- низму. Это связано с тем, что pe
– указатель, а для получения типа произ- водного класса операндом typeid должен быть тип класса с виртуальными функциями. Выражение typeid(pe)
возвращает тип pe
, т.е. указатель на employee
. Это значение совпадает со значением typeid(employee*)
, тогда как все остальные сравнения дают ложь.
Только при употреблении выражения
*pe в качестве операнда typeid результат будет содержать тип объекта, на который указывает pe
:
102 typeid( *pe ) == typeid( manager ) // истинно typeid( *pe ) == typeid( employee ) // ложно
В этих сравнениях
*pe
– выражение типа класса, который имеет вирту- альные функции, поэтому результатом применения typeid будет тип адре- суемого операндом объекта manager
Такой оператор можно использовать и со ссылками: typeid( re ) == typeid( manager ) // истинно typeid( re ) == typeid( employee ) // ложно typeid( &re ) == typeid( employee* ) // истинно typeid( &re ) == typeid( manager* ) // ложно
В первых двух сравнениях операнд re имеет тип класса с виртуальными функциями, поэтому результат применения typeid содержит тип объекта, на который ссылается re
. В последних двух сравнениях операнд
&re имеет тип указателя, следовательно, результатом будет тип самого операнда, т.е. employee*
На самом деле оператор typeid возвращает объект класса типа type_info
, который определен в заголовочном файле
. Интер- фейс этого класса показывает, что можно делать с результатом, возвращен- ным typeid