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

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

 }
 Функция может возбудить исключение invalid_argument, если получит аргумент с некорректным значением; в конкретной ситуации, когда значение аргумента выходит за пределы допустимого диапазона, разрешается возбудить исключение out_of_range, а length_error используется для оповещения о попытке создать объект, длина которого превышает максимально возможную.
 Ошибки времени выполнения, напротив, вызваны событием, с самой программой не связанным. Предполагается, что их нельзя обнаружить, пока программа не начала работать. В стандартной библиотеке определены следующие такие ошибки:
 namespace std {
  class runtime_error : public exception { // ошибка времени выполнения
  public:
  explicit runtime_error( const string &what_arg );
  };
  class range_error : public runtime_error { // ошибка диапазона
  public:
  explicit range_error( const string &what_arg );
  };
  class overflow_error : public runtime_error { // переполнение
  public:
  explicit overflow_error( const string &what_arg );
  };
  class underflow_error : public runtime_error { // потеря значимости
  public:
  explicit underflow_error( const string &what_arg );
  };
 }
 Функция может возбудить исключение range_error, чтобы сообщить об ошибке во внутренних вычислениях. Исключение overflow_error говорит об ошибке арифметического переполнения, а underflow_error – о потере значимости.
 Класс exception является базовым и для класса исключения bad_alloc, которое возбуждает оператор new(), когда ему не удается выделить запрошенный объем памяти (см. раздел 8.4), и для класса исключения bad_cast, возбуждаемого в ситуации, когда ссылочный вариант оператора dynamic_cast не может быть выполнен (см. раздел 19.1).
 Переопределим оператор operator[] в шаблоне Array из раздела 16.12 так, чтобы он возбуждал исключение типа range_error, если индекс массива Array выходит за границы:
 #include
 #include
 
 template
 class Array {
 public:
  // ...
  elemType& operator[]( int ix ) const
  {
  if ( ix < 0 || ix >= _size )
  {
  string eObj =
  "ошибка: вне диапазона в Array::operator[]()";
 
  throw out_of_range( eObj );
  }
  return _ia[ix];
  }
 
  // ...
 private:
  int _size;
  elemType *_ia;
 };
 Для использования предопределенных классов исключений в программу необходимо включить заголовочный файл . Описание возбужденного исключения содержится в объекте eObj типа string. Эту информацию можно извлечь в обработчике с помощью функции-члена what():
 int main()
 {
  try {
  // функция main() такая же, как в разделе 16.2
  }
  catch ( const out_of_range &excep ) {
  // печатается:
  // ошибка: вне диапазона в Array::operator[]()
  cerr << excep.what() << "\n";
  return -1;
  }
 }
 В данной реализации выход индекса за пределы массива в функции try_array() приводит к тому, что оператор взятия индекса operator[]() класса Array возбуждает исключение типа out_of_range, которое перехватывается в main().
 Упражнение 19.5
 Какие исключения могут возбуждать следующие функции:
 #include
 
 (a) void operate() throw( logic_error );
 (b) int mathErr( int ) throw( underflow_error, overflow_error );
 (c) char manip( string ) throw( );
 Упражнение 19.6
 Объясните, как механизм обработки исключений в C++ поддерживает технику программирования “захват ресурса – это инициализация; освобождение ресурса – это уничтожение”.
 Упражнение 19.7
 Исправьте ошибку в списке catch-обработчиков для данного try-блока:
 #include
 
 int main() {
  try {
  // использование функций из стандартной библиотеки
  }
  catch( exception ) {
  }
  catch( runtime_error &re ) {
  }
  catch( overflow_error eobj ) {
  }
 }
 Упражнение 19.8
 Дана программа на C++:
 int main() {
  // использование стандартной библиотеки
 }
 Модифицируйте main() так, чтобы она перехватывала все исключения, возбуждаемые функциями стандартной библиотеки. Обработчики должны печатать сообщение об ошибке, ассоциированное с исключением, а затем вызывать функцию abort() (она определена в заголовочном файле ) для завершения main().
 19.3. Разрешение перегрузки и наследование A
 Наследование классов оказывает влияние на все аспекты разрешения перегрузки функций (см. раздел 9.2). Напомним, что эта процедура состоит из трех шагов:
 Отбор функций-кандидатов.
 Отбор устоявших функций.
 Выбор наилучшей из устоявших функции.
 Отбор функций-кандидатов зависит от наследования потому, что на этом шаге принимаются во внимание функции, ассоциированные с базовыми классами, – как их функции-члены, так и функции, объявленные в тех же пространствах имен, где определены базовые классы. Отбор устоявших функций также зависит от наследования, поскольку множество преобразований формальных параметров функции в фактические аргументы расширяется пользовательскими преобразованиями. Кроме того, наследование оказывает влияние на ранжирование последовательностей трансформаций аргументов, а значит, и на выбор наилучшей из устоявших функции. В данном разделе мы рассмотрим влияние наследования на эти три шага разрешения перегрузки более подробно.
 19.3.1. Функции-кандидаты
 Наследование влияет на первый шаг процедуры разрешения перегрузки функции – формирование множества кандидатов для данного вызова, причем это влияние может быть различным в зависимости от того, рассматривается ли вызов обычной функции вида
 func( args );
 или функции-члена с помощью операторов доступа “точка” или “стрелка”:
 object.memfunc( args );
 pointer->memfunc( args );
 В данном разделе мы изучим оба случая.
 Если аргумент обычной функции имеет тип класса, ссылки или указателя на тип класса, и класс определен в пространстве имен, то кандидатами будут все одноименные функции, объявленные в этом пространстве, даже если они невидимы в точке вызова (подробнее об этом говорилось в разделе 15.10). Если аргумент при наследовании имеет тип класса, ссылки или указателя на тип класса, и у этого класса есть базовые, то в множество кандидатов добавляются также функции, объявленные в тех пространствах имен, где определены базовые классы. Например:
 namespace NS {
  class ZooAnimal { /* ... */ };
  void display( const ZooAnimal& );
 }
 
 // базовый класс Bear объявлен в пространстве имен NS
 class Bear : public NS::ZooAnimal { };
 
 int main() {
  Bear baloo;
 
  display( baloo );
  return 0;
 }
 Аргумент baloo имеет тип класса Bear. Кандидатами для вызова display() будут не только функции, объявления которых видимы в точке ее вызова, но также и те, что объявлены в пространствах имен, в которых объявлены класс Bear и его базовый класс ZooAnimal. Поэтому в множество кандидатов добавляется функция display(const ZooAnimal&), объявленная в пространстве имен NS.
 Если аргумент имеет тип класса и в определении этого класса объявлены функции-друзья с тем же именем, что и вызванная функция, то эти друзья также будут кандидатами, даже если их объявления не видны в точке вызова (см. раздел 15.10). Если аргумент при наследовании имеет тип класса, у которого есть базовые, то в множество кандидатов добавляются одноименные функции-друзья каждого из них. Предположим, что в предыдущем примере display() объявлена как функция-друг ZooAnimal:
 namespace NS {
  class ZooAnimal {
  friend void display( const ZooAnimal& );
  };
 }
 
 // базовый класс Bear объявлен в пространстве имен NS
 class Bear : public NS::ZooAnimal { };
 
 int main() {
  Bear baloo;
 
  display( baloo );
  return 0;
 }
 Аргумент baloo функции display() имеет тип Bear. В его базовом классе ZooAnimal функция display() объявлена другом, поэтому она является членом пространства имен NS, хотя явно в нем не объявлена. При обычном просмотре NS она не была бы найдена. Однако поскольку аргумент display() имеет тип Bear, то объявленная в ZooAnimal функция-друг добавляется в множество кандидатов.
 Таким образом, если при вызове обычной функции задан аргумент, который представляет собой объект класса, ссылку или указатель на объект класса, то множество функций-кандидатов является объединением следующих множеств:
 функций, видимых в точке вызова;
 функций, объявленных в тех пространствах имен, где определен тип класса или любой из его базовых;
 функций, являющихся друзьями этого класса или любого из его базовых.
 Наследование влияет также на построение множества кандидатов для вызова функции-члена с помощью операторов “точка” или “стрелка”. В разделе 18.4 мы говорили, что объявление функции-члена в производном классе не перегружает, а скрывает одноименные функции-члены в базовом, даже если их списки параметров различны:
 class ZooAnimal {
 public:
  Time feeding_time( string );
  // ...
 };
 class Bear : public ZooAnimal {
 public:
  // скрывает ZooAnimal::feeding_time( string )
  Time feeding_time( int );
  // ...
 };
 
 Bear Winnie;
 
 // ошибка: ZooAnimal::feeding_time( string ) скрыта
 Winnie.feeding_time( "Winnie" );
 Функция-член feeding_time(int), объявленная в классе Bear, скрывает feeding_time(string), объявленную в ZooAnimal, базовом для Bear. Поскольку функция-член вызывается через объект Winnie типа Bear, то при поиске кандидатов для этого вызова просматривается только область видимости класса Bear, и единственным кандидатом будет feeding_time(int). Так как других кандидатов нет, вызов считается ошибочным.
 Чтобы исправить ситуацию и заставить компилятор считать одноименные функции-члены базового и производного классов перегруженными, разработчик производного класса может ввести функции-члены базового класса в область видимости производного с помощью using-объявлений:
 class Bear : public ZooAnimal {
 public:
  // feeding_time( int ) перегружает экземпляр из класса ZooAnimal
  using ZooAnimal::feeding_time;
  Time feeding_time( int );
  // ...
 };
 Теперь обе функции feeding_time() находятся в области видимости класса Bear и, следовательно, войдут в множество кандидатов:
 // правильно: вызывается ZooAnimal::feeding_time( string )
 Winnie.feeding_time( "Winnie" );
 В такой ситуации вызывается функция-член feeding_time( string ).
 В случае множественного наследования при формировании совокупности кандидатов объявления функций-членов должны быть найдены в одном и том же базовом классе, иначе вызов считается ошибочным. Например:
 class Endangered {
 public:
  ostream& print( ostream& );
  // ...
 {;
 
 class Bear : public( ZooAnimal ) {
 public:
  void print( );
  using ZooAnimal::feeding_time;
  Time feeding_time( int );
  // ...
 };
 
 class Panda : public Bear, public Endangered {
 public:
  // ...
 };
 
 int main()
 {
  Panda yin_yang;
 
  // ошибка: неоднозначность: одна из
  // Bear::print()
  // Endangered::print( ostream& )
  yin_yang.print( cout );
 
  // правильно: вызывается Bear::feeding_time()
  yin_yang.feeding_time( 56 );
 }
 При поиске объявления функции-члена print() в области видимости класса Panda будут найдены как Bear::print(), так и Endangered::print(). Поскольку они не находятся в одном и том же базовом классе, то даже при разных списках параметров этих функций множество кандидатов оказывается пустым и вызов считается ошибочным. Для исправления ошибки в классе Panda следует определить собственную функцию print(). При поиске объявления функции-члена feeding_time() в области видимости Panda будут найдены ZooAnimal::feeding_time() и Bear::feeding_time() – они расположены в области видимости класса Bear. Так как эти объявления найдены в одном и том же базовом классе, множество кандидатов для данного вызова включает обе функции, а выбирается Bear::feeding_time().
 19.3.2. Устоявшие функции и последовательности пользовательских преобразований
 Наследование оказывает влияние и на второй шаг разрешения перегрузки функции: отбор устоявших из множества кандидатов. Устоявшей называется функция, для которой существуют приведения типа каждого фактического аргумента к типу соответственного формального параметра.
 В разделе 15.9 мы показали, как разработчик класса может предоставить пользовательские преобразования для объектов этого класса, которые неявно вызываются компилятором для трансформации фактического аргумента функции в тип соответственного формального параметра. Пользовательские преобразования бывают двух видов: конвертер или конструктор с одним параметром без ключевого слова explicit. При наследовании на втором шаге разрешения перегрузки рассматривается более широкое множество таких преобразований.
 Конвертеры наследуются, как и любые другие функции-члены класса. Например, мы можем написать следующий конвертер для ZooAnimal:
 class ZooAnimal {
 public:
 
  // конвертер: ZooAnimal ==> const char*
  operator const char*();
 
  // ...
 };
 Производный класс Bear наследует его от своего базового ZooAnimal. Если значение типа Bear используется в контексте, где ожидается const char*, то неявно вызывается конвертер для преобразования Bear в const char*:
 extern void display( const char* );
 
 Bear yogi;
 
 // правильно: yogi ==> const char*
 display( yogi );
 Конструкторы с одним аргументом без ключевого слова explicit образуют другое множество неявных преобразований: из типа параметра в тип своего класса. Определим такой конструктор для ZooAnimal:
 class ZooAnimal {
 public:
  // преобразование: int ==> ZooAnimal
  ZooAnimal( int );
 
  // ...
 };
 Его можно использовать для приведения значения типа int к типу ZooAnimal. Однако конструкторы не наследуются. Конструктор ZooAnimal нельзя применять для преобразования объекта в случае, когда целевым является тип производного класса:
 const int cageNumber = 8788l
 
 void mumble( const Bear & );
 
 // ошибка: ZooAnimal( int ) не используется
 mumble( cageNumber );
 Поскольку целевым типом является Bear – тип параметра функции mumble(), то рассматриваются только его конструкторы.
 19.3.3. Наилучшая из устоявших функций
 Наследование влияет и на третий шаг разрешения перегрузки – выбор наилучшей из устоявших функций. На этом шаге ранжируются преобразования типов, с помощью которых можно привести фактические аргументы функции к типам соответственных формальных параметров. Следующие неявные преобразования имеют тот же ранг, что и стандартные (стандартные преобразования рассматривались в разделе 9.3):
 преобразование аргумента типа производного класса в параметр типа любого из его базовых;
 преобразование указателя на тип производного класса в указатель на тип любого из его базовых;
 инициализация ссылки на тип базового класса с помощью l-значения типа производного.
 Они не являются пользовательскими, так как не зависят от конвертеров и конструкторов, имеющихся в классе:
 extern void release( const ZooAnimal& );
 Panda yinYang;
 
 // стандартное преобразование: Panda -> ZooAnimal
 release( yinYang );
 Поскольку аргумент yinYang типа Panda инициализирует ссылку на тип базового класса, то преобразование имеет ранг стандартного.
 В разделе 15.10 мы говорили, что стандартные преобразования имеют более высокий ранг, чем пользовательские:
 class Panda : public Bear,
  public Endangered
 {
  // наследует ZooAnimal::operator const char *()
 };
 
 Panda yinYang;
 
 extern void release( const ZooAnimal& );
 extern void release( const char * );
 
 // стандартное преобразование: Panda -> ZooAnimal
 // выбирается: release( const ZooAnimal& )
 release( yinYang );
 Как release(const char*), так и release(ZooAnimal&) являются устоявшими функциями: первая потому, что инициализация параметра-ссылки значением аргумента – стандартное преобразование, а вторая потому, что аргумент можно привести к типу const char* с помощью конвертера ZooAnimal::operator const char*(), который представляет собой пользовательское преобразование. Так как стандартное преобразование лучше пользовательского, то в качестве наилучшей из устоявших выбирается функция release(const ZooAnimal&).
 При ранжировании различных стандартных преобразований из производного класса в базовые лучшим считается приведение к тому базовому классу, который ближе к производному. Так, показанный ниже вызов не будет неоднозначным, хотя в обоих случаях требуется стандартное преобразование. Приведение к базовому классу Bear лучше, чем к ZooAnimal, поскольку Bear ближе к классу Panda. Поэтому лучшей из устоявших будет функция release(const Bear&):
 extern void release( const ZooAnimal& );
 extern void release( const Bear& );
 
 // правильно: release( const Bear& )
 release( yinYang );
 Аналогичное правило применимо и к указателям. При ранжировании стандартных преобразований из указателя на тип производного класса в указатели на типы различных базовых лучшим считается то, для которого базовый класс наименее удален от производного. Это правило распространяется и на тип void*.
 Стандартное преобразование в указатель на тип любого базового класса всегда лучше, чем преобразование в void*. Например, если дана пара перегруженных функций:
 void receive( void* );
 void receive( ZooAnimal* );
 то наилучшей из устоявших для вызова с аргументом типа Panda* будет receive(ZooAnimal*).
 В случае множественного наследования два стандартных преобразования из типа производного класса в разные типы базовых могут иметь одинаковый ранг, если оба базовых класса равноудалены от производного. Например, Panda наследует классам Bear и Endangered. Поскольку они равноудалены от производного Panda, то преобразования объекта Panda в любой из этих классов одинаково хороши. Но тогда единственной наилучшей из устоявших функции для следующего вызова не существует, и он считается ошибочным:
 extern void mumble( const Bear& );
 extern void mumble( const Endangered& );
 
 /* ошибка: неоднозначный вызов:
  * может быть выбрана любая из двух функций
  * void mumble( const Bear& );
 * void mumble( const Endangered& );
 */
 mumble( yinYang );
 Для разрешения неоднозначности программист может применить явное приведение типа:
 mumble( static_cast< Bear >( yinYang ) ); // правильно
 Инициализация объекта производного класса или ссылки на него объектом типа базового, а также преобразование указателя на тип базового класса в указатель на тип производного никогда не выполняются компилятором неявно. (Однако их можно выполнить с помощью явного применения dynamic_cast, как мы видели в разделе 19.1.) Для данного вызова не существует наилучшей из устоявших функции, так как нет неявного преобразования аргумента типа ZooAnimal в тип производного класса:
 extern void release( const Bear& );
 extern void release( const Panda& );
 
 ZooAnimal za;
 
 // ошибка: нет соответствия
 release( za );
 В следующем примере наилучшей из устоявших будет release(const char*). Это может показаться удивительным, так как к аргументу применена последовательность пользовательских преобразований, в которой участвует конвертер const char*(). Но поскольку неявного приведения от типа базового класса к типу производного не существует, то release(const Bear&) не является устоявшей функцией, так что остается только release(const char*):

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

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