<< Пред.           стр. 94 (из 121)           След. >>

Список литературы по разделу

 void company::payroll( employee *pe )
 {
  programmer *pm = dynamic_cast< programmer* >( pe );
 
  // если pe указывает на объект типа programmer,
  // то dynamic_cast выполнится успешно и pm будет
  // указывать на начало объекта programmer
  if ( pm ) {
  // использовать pm для вызова programmer::bonus()
  }
  // если pe не указывает на объект типа programmer,
  // то dynamic_cast выполнится неудачно
  // и pm будет содержать 0
  else {
  // использовать функции-члены класса 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().
 Оператор dynamic_cast употребляется для безопасного приведения указателя на базовый класс к указателю на производный. Такую операцию часто называют понижающим приведением (downcasting). Она применяется, когда необходимо воспользоваться особенностями производного класса, отсутствующими в базовом. Манипулирование объектами производного класса с помощью указателей на базовый обычно происходит автоматически, с помощью виртуальных функций. Однако иногда использовать виртуальные функции невозможно. В таких ситуациях dynamic_cast предлагает альтернативное решение, хотя этот механизм в большей степени подвержен ошибкам, чем виртуализация, и должен применяться с осторожностью.
 Одна из возможных ошибок – это работа с результатом dynamic_cast без предварительной проверки на 0: нулевой указатель нельзя использовать для адресации объекта класса. Например:
 void company::payroll( employee *pe )
 {
  programmer *pm = dynamic_cast< programmer* >( pe );
 
  // потенциальная ошибка: pm используется без проверки значения
  static int variablePay = 0;
  variablePay += pm->bonus();
  // ...
 }
 Результат, возвращенный dynamic_cast, всегда следует проверять, прежде чем использовать в качестве указателя. Более правильное определение функции company::payroll() могло бы выглядеть так:
 void company::payroll( employee *pe )
 {
  // выполнить dynamic_cast и проверить результат
  if ( programmer *pm = dynamic_cast< programmer* >( pe ) ) {
  // использовать pm для вызова programmer::bonus()
  }
  else {
  // использовать функции-члены класса employee
  }
 }
 Результат операции dynamic_cast используется для инициализации переменной pm внутри условного выражения в инструкции if. Это возможно, так как объявления в условиях возвращают значения. Ветвь, соответствующая истинности условия, выполняется, если pm не равно нулю: мы знаем, что операция dynamic_cast завершилась успешно и pe указывает на объект programmer. В противном случае результатом объявления будет 0 и выполняется ветвь else. Поскольку теперь оператор и проверка его результата находятся в одной инструкции программы, то невозможно случайно вставить какой-либо код между выполнением dynamic_cast и проверкой, так что pm будет использоваться только тогда, когда содержит правильный указатель.
 В предыдущем примере операция dynamic_cast преобразует указатель на базовый класс в указатель на производный. Ее также можно применять для трансформации l-значения типа базового класса в ссылку на тип производного. Синтаксис такого использования dynamic_cast следующий:
 dynamic_cast< Type & >( lval )
 где Type& – это целевой тип преобразования, а lval – l-значение типа базового класса. Операнд lval успешно приводится к типу Type& только в том случае, когда lval действительно относится к объекту класса, для которого один из производных имеет тип Type.
 Поскольку нулевых ссылок не бывает (см. раздел 3.6), то проверить успешность выполнения операции путем сравнения результата (т.е. возвращенной оператором dynamic_cast ссылки) с нулем невозможно. Если вместо указателей используются ссылки, условие
 if ( programmer *pm = dynamic_cast< programmer* >( pe ) )
 нельзя переписать в виде
 if ( programmer &pm = dynamic_cast< programmer& >( pe ) )
 Для извещения об ошибке в случае приведения к ссылочному типу оператор dynamic_cast возбуждает исключение. Следовательно, предыдущий пример можно записать так:
 #include
 void company::payroll( employee &re )
 {
  try {
  programmer &rm = dynamic_cast< programmer & >( re );
  // использовать rm для вызова programmer::bonus()
  }
  catch ( std::bad_cast ) {
  // использовать функции-члены класса employee
  }
 }
 В случае неудачного завершения ссылочного варианта dynamic_cast возбуждается исключение типа bad_cast. Класс bad_cast определен в стандартной библиотеке; для ссылки на него необходимо включить в программу заголовочный файл . (Исключения из стандартной библиотеки мы будем рассматривать в следующем разделе.)
 Когда следует употреблять ссылочный вариант dynamic_cast вместо указательного? Это зависит только от желания программиста. При его использовании игнорировать ошибку приведения типа и работать с результатом без проверки (как в указательном варианте) невозможно; с другой стороны, применение исключений увеличивает накладные расходы во время выполнения программы (см. главу 11).
 19.1.2. Оператор typeid
 Второй оператор, входящий в состав RTTI, – это typeid, который позволяет выяснить фактический тип выражения. Если оно принадлежит типу класса и этот класс содержит хотя бы одну виртуальную функцию-член, то ответ может и не совпадать с типом самого выражения. Так, если выражение является ссылкой на базовый класс, то typeid сообщает тип производного класса объекта:
 #include
 
 programmer pobj;
 employee &re = pobj;
 
 // с функцией name() мы познакомимся в подразделе, посвященном type_info
 // она возвращает C-строку "programmer"
 coiut << typeid( re ).name() << endl;
 Операнд re оператора typeid имеет тип employee. Но так как re – это ссылка на тип класса с виртуальными функциями, то typeid говорит, что тип адресуемого объекта – programmer (а не employee, на который ссылается re). Программа, использующая такой оператор, должна включать заголовочный файл , что мы и сделали в этом примере.
 Где применяется typeid? В сложных системах разработки, например при построении отладчиков, а также при использовании устойчивых объектов, извлеченных из базы данных. В таких системах необходимо знать фактический тип объекта, которым программа манипулирует с помощью указателя или ссылки на базовый класс, например для получения списка его свойств во время сеанса работы с отладчиком или для правильного сохранения или извлечения объекта из базы данных. Оператор typeid допустимо использовать с выражениями и именами любых типов. Например, его операндами могут быть выражения встроенных типов и константы. Если операнд не принадлежит к типу класса, то typeid просто возвращает его тип:
 int iobj;
 
 cout << typeid( iobj ).name() << endl; // печатается: int
 cout << typeid( 8.16 ).name() << endl; // печатается: double
 Если операнд имеет тип класса, в котором нет виртуальных функций, то typeid возвращает тип операнда, а не связанного с ним объекта:
 class Base { /* нет виртуальных функций */ };
 class Derived : public Base { /* нет виртуальных функций */ };
 
 Derived dobj;
 Base *pb = &dobj;
 
 cout << typeid( *pb ).name() << endl; // печатается: Base
 Операнд typeid имеет тип Base, т.е. тип выражения *pb. Поскольку в классе Base нет виртуальных функций, результатом typeid будет Base, хотя объект, на который указывает pb, имеет тип Derived.
 Результаты, возвращенные оператором typeid, можно сравнивать. Например:
 #include
 
 employee *pe = new manager;
 employee& re = *pe;
 if ( typeid( pe ) == typeid( employee* ) ) // истинно
  // что-то сделать
 /*
 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:
 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. (В следующем подразделе мы подробно рассмотрим этот интерфейс.)
 19.1.3. Класс type_info
 Точное определение класса type_info зависит от реализации, но некоторые его характерные черты остаются неизменными в любой программе на C++:
 class type_info {
  // представление зависит от реализации
 private:
  type_info( const type_info& );
  type_info& operator= ( const type_info& );
 public:
  virtual ~type_info();
 
  int operator==( const type_info& );
  int operator!=( const type_info& );
 
  const char * name() const;
 };
 Поскольку копирующие конструктор и оператор присваивания – закрытые члены класса type_info, то пользователь не может создать его объекты в своей программе:
 #include
 
 type_info t1; // ошибка: нет конструктора по умолчанию
  // ошибка: копирующий конструктор закрыт
 type_info t2 (typeid( unsigned int ) );
 Единственный способ создать объект класса type_info – воспользоваться оператором typeid.
 В классе определены также операторы сравнения. Они позволяют сравнивать два объекта type_info, а следовательно, и результаты, возвращенные двумя операторами typeid. (Мы говорили об этом в предыдущем подразделе.)
 typeid( re ) == typeid( manager ) // истинно
 typeid( *pe ) != typeid( employee ) // ложно
 Функция name() возвращает C-строку с именем типа, представленного объектом type_info. Этой функцией можно пользоваться в программах следующим образом:
 #include
 int main() {
  employee *pe = new manager;
 
  // печатает: "manager"
  cout << typeid( *pe ).name() << endl;
 }
 Для работы с функцией-членом name() нужно включить заголовочный файл .
 Имя типа – это единственная информация, которая гарантированно возвращается всеми реализациями C++, при этом используется функция-член name() класса type_info. В начале этого раздела упоминалось, что поддержка RTTI зависит от реализации и иногда в классе type_info бывают дополнительные функции-члены. Чтобы узнать, каким образом обеспечивается поддержка RTTI в вашем компиляторе, обратитесь к справочному руководству по нему. Кроме того, можно получить любую информацию, которую компилятор знает о типе, например:
 список функций-членов класса;
 способ размещения объекта в памяти, т.е. взаимное расположение подобъектов базового и производных классов.
 Одним из способов расширения поддержки RTTI является включение дополнительной информации в класс, производный от type_info. Поскольку в классе type_info есть виртуальный деструктор, то оператор dynamic_cast позволяет выяснить, имеется ли некоторое конкретное расширение RTTI. Предположим, что некоторый компилятор предоставляет расширенную поддержку RTTI посредством класса extended_type_info, производного от type_info. С помощью оператора dynamic_cast программа может узнать, принадлежит ли объект типа type_info, возвращенный оператором typeid, к типу extended_type_info. Если да, то пользоваться расширенной поддержкой RTTI разрешено.
 #include
 
 // Файл typeinfo содержит определение типа extended_type_info
 
 void func( employee* p )
 {
  // понижающее приведение типа type_info* к extended_type_info*
  if ( eti *eti_p = dynamic_cast( &typeid( *p ) ) )
  {
  // если dynamic_cast завершается успешно,
  // можно пользоваться информацией из extended_type_info через eti_p
  }
  else
  {
  // если dynamic_cast завершается неудачно,
  // можно пользоваться только стандартным type_info
  }
 }
 Если dynamic_cast завершается успешно, то оператор typeid вернет объект класса extended_type_info, т.е. компилятор обеспечивает расширенную поддержку RTTI, чем программа может воспользоваться. В противном случае допустимы только базовые средства RTTI.
 Упражнение 19.1
 Дана иерархия классов, в которой у каждого класса есть конструктор по умолчанию и виртуальный деструктор:
 class X { ... };
 class A { ... };
 class B : public A { ... };
 class C : public B { ... };
 class D : public X, public C { ... };
 Какие из данных операторов dynamic_cast завершатся неудачно?
 (a) D *pd = new D;
  A *pa = dynamic_cast< A* > ( pd );
 (b) A *pa = new C;
  C *pc = dynamic_cast< C* > ( pa );
 (c) B *pb = new B;
  D *pd = dynamic_cast< D* > ( pb );
 (d) A *pa = new D;
  X *px = dynamic_cast< X* > ( pa );
 Упражнение 19.2
 Объясните, когда нужно пользоваться оператором dynamic_cast вместо виртуальной функции?
 Упражнение 19.3
 Пользуясь иерархией классов из упражнения 19.1, перепишите следующий фрагмент так, чтобы в нем использовался ссылочный вариант dynamic_cast для преобразования *pa в тип D&:
 if ( D *pd = dynamic_cast< D* >( pa ) ) {
  // использовать члены D
 }
 else {
  // использовать члены A
 }
 Упражнение 19.4
 Дана иерархия классов, в которой у каждого класса есть конструктор по умолчанию и виртуальный деструктор:
 class X { ... };
 class A { ... };
 class B : public A { ... };
 class C : public B { ... };
 class D : public X, public C { ... };
 Какое имя типа будет напечатано в каждом из следующих случаев:
 (a) A *pa = new D;
  cout << typeid( pa ).name() << endl;
 
 (b) X *px = new D;
  cout << typeid( *px ).name() << endl;
 
 (c) C obj;
  A& ra = cobj;
  cout << typeid( &ra ).name() << endl;
 
 (d) X *px = new D;
  A& ra = *px;
  cout << typeid( ra ).name() << endl;
 19.2. Исключения и наследование
 Обработка исключений – это стандартное языковое средство для реакции на аномальное поведение программы во время выполнения. C++ поддерживает единообразный синтаксис и стиль обработки исключений, а также способы тонкой настройки этого механизма в специальных ситуациях. Основы его поддержки в языке C++ описаны в главе 11, где показано, как программа может возбудить исключение, передать управление его обработчику (если таковой существует) и как обработчики исключений ассоциируются с try-блоками.
 Возможности механизма обработки исключений становятся больше, если в качестве исключений использовать иерархии классов. В этом разделе мы расскажем, как писать программы, которые умеют возбуждать и обрабатывать исключения, принадлежащие таким иерархиям.
 19.2.1. Исключения, определенные как иерархии классов
 В главе 11 мы использовали два типа класса для описания исключений, возбуждаемых функциями-членами нашего класса iStack:
 class popOnEmpty { ... };
 class pushOnFull { ... };
 В реальных программах на C++ типы классов, представляющих исключения, чаще всего организуются в группы, или иерархии. Как могла бы выглядеть вся иерархия для этих классов?
 Мы можем определить базовый класс Excp, которому наследуют оба наши класса исключений. Он инкапсулирует данные и функции-члены, общие для обоих производных:
 class Excp { ... };
 class popOnEmpty : public Excp { ... };
 class pushOnFull : public Excp { ... };
 Одной из операцией, которые предоставляет базовый класс, является вывод сообщения об ошибке. Эта возможность используется обоими классами, стоящими ниже в иерархии:
 class Excp {
 public:
  // напечатать сообщение об ошибке
  static void print( string msg ) {
  cerr << msg << endl;
  }
 };
 Иерархию классов исключений разрешается развивать и дальше. От Excp можно произвести другие классы для более точного описания исключений, обнаруживаемых программой:
 class Excp { ... };
 
 class stackExcp : public Excp { ... };
  class popOnEmpty : public stackExcp { ... };
  class pushOnFull : public stackExcp { ... };
 class mathExcp : public Excp ( ... };
  class zeroOp : public mathExcp { ... };
  class divideByZero : public mathExcp { ... };
 Последующие уточнения позволяют более детально идентифицировать аномальные ситуации в работе программы. Дополнительные классы исключений организуются как слои. По мере углубления иерархии каждый новый слой описывает все более специфичные исключения. Например, первый, самый общий слой в приведенной выше иерархии представлен классом Excp. Второй специализирует Excp, выделяя из него два подкласса: stackExcp (для исключений при работе с нашим iStack) и mathExcp (для исключений, возбуждаемых функциями из математической библиотеки). Третий, самый специализированный слой данной иерархии уточняет классы исключений: popOnEmpty и pushOnFull определяют два вида исключений работы со стеком, а ZeroOp и divideByZero – два вида исключений математических операций.
 В последующих разделах мы рассмотрим, как возбуждаются и обрабатываются исключения, представленные классами в нашей иерархии.
 19.2.2. Возбуждение исключения типа класса
 Теперь, познакомившись с классами, посмотрим, что происходит, когда функция-член push() нашего iStack возбуждает исключение:
 void iStack::push( int value )
 {
  if ( full() )
  // value сохраняется в объекте-исключении
  throw pushOnFull( value );
  // ...
 }
 Выполнение инструкции throw инициирует несколько последовательных действий:
 Инструкция throw создает временный объект типа класса pushOnFull, вызывая его конструктор.
 С помощью копирующего конструктора генерируется объект-исключение типа pushOnFull – копия временного объекта, полученного на шаге 1. Затем он передается обработчику исключения.
 Временный объект, созданный на шаге 1, уничтожается до начала поиска обработчика.
 Зачем нужно генерировать объект-исключение (шаг 2)? Инструкция
 throw pushOnFull( value );
 создает временный объект, который уничтожается в конце работы throw. Но исключение должно существовать до тех пор, пока не будет найден его обработчик, а он может находиться намного выше в цепочке вызовов. Поэтому необходимо скопировать временный объект в некоторую область памяти (объект-исключение), которая гарантированно существует, пока исключение не будет обработано. Иногда компилятор создает объект-исключение сразу, минуя шаг 1. Однако стандарт этого не требует, да и не всегда такое возможно.
 Поскольку объект-исключение создается путем копирования значения, переданного инструкции throw, то возбужденное исключение всегда имеет такой же тип, как и это значение:
 void iStack::push( int value ) {
  if ( full() ) {
  pushOnFull except( value );
  stackExcp *pse = &except;
  throw *pse; // объект-исключение имеет тип stackExcp
  }
  // ...
 }
 Выражение *pse имеет тип stackExcp. Тип созданного объекта-исключения – stackExcp, хотя pse ссылается на объект с фактическим типом pushOnFull. Фактический тип объекта, на который ссылается throw, при создании объекта-исключения не учитывается. Поэтому исключение не будет перехвачено catch-обработчиком pushOnFull.
 Действия, выполняемые инструкцией throw, налагают определенные ограничения на то, какие классы можно использовать для создания объектов-исключений. Оператор throw в функции-члене push() класса iStack вызовет ошибку компиляции, если:
 в классе pushOnFull нет конструктора, принимающего аргумент типа int, или этот конструктор недоступен;
 в классе pushOnFull есть копирующий конструктор или деструктор, но хотя бы один из них недоступен;
 pushOnFull – это абстрактный базовый класс. Напомним, что программа не может создавать объекты абстрактных классов (см. раздел 17.1).
 19.2.3. Обработка исключения типа класса
 Если исключения организуются в иерархии, то исключение типа некоторого класса может быть перехвачено обработчиком, соответствующим любому его открытому базовому классу. Например, исключение типа pushOnFull перехватывается обработчиками исключений типа stackExcp или Excp.
 int main() {
  try {
  // ...
  }
  catch ( Excp ) {
  // обрабатывает исключения popOnEmpty и pushOnFull
  }
  catch ( pushOnFull ) {
  // обрабатывает исключение pushOnFull
  }
 Здесь порядок catch-обработчиков желательно изменить. Напоминаем, что они просматриваются в порядке появления после try-блока. Как только будет найден обработчик, способный обработать данное исключение, поиск прекращается. В примере выше Excp может обработать исключения типа pushOnFull, а это значит, что специализированный обработчик таких исключений задействован не будет. Правильная последовательность такова:
 catch ( pushOnFull ) {
  // обрабатывает исключение pushOnFull

<< Пред.           стр. 94 (из 121)           След. >>

Список литературы по разделу