НА ГЛАВНУЮ
Меню сайта
Категория
Ghost++ [1]
С++ [55]
Развлечение
ON - LINE
Опрос
На какие on-line игры вы тратите больше времени?
Всего ответов: 244
Оbserver Ward

Онлайн всего: 1
Гостей: 1
Пользователей: 0


Друзья сайта
Заведи себе Бота
Hаша кнопка
Для обмена банерами , наша кнопка для размещения у вас на сайте

Клансайт USSR


Главная » Статьи » Программирование » С++

19. Применение наследования в C++ (2)
19.2.5. Раскрутка стека и вызов деструкторов

Когда возбуждается исключение, поиск его catch-обработчика – раскрутка стека – начинается с функции, возбудившей исключение, и продолжается вверх по цепочке вложенных вызовов (см. раздел 11.3).

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

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

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

class PTR {
public:
   PTR() { ptr = new int[ chunk ]; }
   ~PTR { delete[] ptr; }
private:
   int *ptr;
};

Локальный объект такого типа создается в функции manip() перед вызовом mathFunc():

void manip( int parm ) {
   PTR localPtr;
   // ...
   mathFunc( parm );   // возбуждает исключение divideByZero
   // ...
}

Если mathFunc() возбуждает исключение типа divideByZero, то начинается раскрутка стека. В процессе поиска подходящего catch-обработчика проверяется и функция manip(). Поскольку вызов mathFunc() не заключен в try-блок, то manip() нужного обработчика не содержит. Поэтому стек раскручивается дальше по цепочке вызовов. Но перед выходом из manip() с необработанным исключением процесс раскрутки уничтожает все объекты типа классов, которые локальны в ней и были созданы до вызова mathFunc(). Таким образом, локальный объект localPtr уничтожается до того, как поиск пойдет дальше, а следовательно, память, на которую он указывает, будет освобождена и утечки не произойдет.

Поэтому говорят, что процесс обработки исключений в C++ поддерживает технику программирования, основной принцип которой можно сформулировать так: "захват ресурса – это инициализация; освобождение ресурса – это уничтожение”. Если ресурс реализован в виде класса и, значит, действия по его захвату сосредоточены в конструкторе, а действия по освобождению – в деструкторе (как, например, в классе PTR выше), то локальный для функции объект такого класса автоматически уничтожается при выходе из функции в результате необработанного исключения. Действия, которые должны быть выполнены для освобождения ресурса, не будут пропущены при раскрутке стека, если они инкапсулированы в деструкторы, вызываемые для локальных объектов.

Класс auto_ptr, определенный в стандартной библиотеке (см. раздел 8.4), ведет себя почти так же, как наш класс PTR. Это средство для инкапсуляции выделения памяти в конструкторе и ее освобождения в деструкторе. Если для выделения одиночного объекта из хипа используется auto_ptr, то гарантируется, что при выходе из составной инструкции или функции из-за необработанного исключения память будет освобождена.
19.2.6. Спецификации исключений

С помощью спецификации исключений (см. раздел 11.4) в объявлении функции указывается множество исключений, которые она может возбуждать прямо или косвенно. Спецификация позволяет гарантировать, что функция не возбудит не перечисленные в ней исключения.

Такую спецификацию разрешается задавать для функций-членов класса так же, как и для обычных функций; она должна следовать за списком параметров функции-члена. Например, в определении класса bad_alloc из стандартной библиотеки C++ функции-члены имеют пустую спецификацию исключений throw(), т.е. гарантированно не возбуждают никаких исключений:

class bad_alloc : public exception {
   // ...
public:
   bad_alloc() throw();
   bad_alloc( const bad_alloc & ) throw();
   bad_alloc & operator=( const bad_alloc & ) throw();
   virtual ~bad_alloc() throw();
   virtual const char* what() const throw();
};

Отметим, что если функция-член объявлена с модификатором const или volatile, как, скажем, what() в примере выше, то спецификация исключений должна идти после него.

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

#include <stdexcept>
// <stdexcept> определяет класс overflow_error

class transport {
   // ...
public:
   double cost( double, double ) throw ( overflow_error );
   // ...
};

// ошибка: спецификация исключений отличается от той, что задана
//         в объявлении в списке членов класса
double transport::cost( double rate, double distance ) { }

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

class Base {
public:
   virtual double f1( double ) throw();
   virtual int f2( int ) throw( int );
   virtual string f3() throw( int, string );
   // ...
}
class Derived : public Base {
public:
   // ошибка: спецификация исключений накладывает меньше ограничений,
   //         чем на Base::f1()
   double f1( double ) throw( string );

   // правильно: та же спецификация исключений, что и для Base::f2()
   int f2( int ) throw( int );

   // правильно: спецификация исключений f3() накладывает больше ограничений
   string f3( ) throw( int );
   // ...
};

Почему спецификация исключений в производном классе должна накладывать не меньше ограничений, чем в базовом? В этом случае мы можем быть уверены, что вызов виртуальной функции из производного класса по указателю на тип базового не нарушит спецификацию исключений функции-члена базового класса:

// гарантируется, что исключения возбуждены не будут
void compute( Base *pb ) throw()
{
   try {
      pb->f3( );  // может возбудить исключение типа int или string
   }
   // обработка исключений, возбужденных в Base::f3()
   catch ( const string & ) { }
   catch ( int ) { }
}

Объявление f3() в классе Base гарантирует, что эта функция возбуждает лишь исключения типа int или string. Следовательно, функция compute() включает catch-обработчики только для них. Поскольку спецификация исключений f3() в производном классе Derived накладывает больше ограничений, чем в базовом Base, то при программировании в согласии с интерфейсом класса Base наши ожидания не будут обмануты.

В главе 11 мы говорили о том, что между типом возбужденного исключения и типом, заданным в спецификации исключений, не допускаются никакие преобразования. Однако если там указан тип класса, то функция может возбуждать исключения в виде объекта класса, открыто наследующего заданному. Аналогично, если имеется указатель на класс, то функции разрешено возбуждать исключения в виде указателя на объект класса, открыто наследующего заданному. Например:

class stackExcp : public Excp { };
class popObEmpty : public stackExcp { };

class pushOnFull : public stackExcp { };

void stackManip() throw( stackExcp )
{
   // ...
}

Спецификация исключений указывает, что stackManip() может возбуждать исключения не только типа stackExcp, но также popOnEmpty и pushOnFull. Напомним, что класс, открыто наследующий базовому, представляет собой пример отношения ЯВЛЯЕТСЯ, т.е. является частным случае более общего базового класса. Поскольку popOnEmpty и pushOnFull – частные случаи stackExcp, они не нарушают спецификации исключений функции stackManip().
19.2.7. Конструкторы и функциональные try-блоки

Можно объявить функцию так, что все ее тело будет заключено в try-блок. Такие try-блоки называются функциональными. (Мы упоминали их в разделе 11.2.) Например:

int main() {
try {
   // тело функции main()
}
catch ( pushOnFull ) {
   // ...
}
catch ( popOnEmpty ) {
   // ...
}

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

Функциональный try-блок необходим для конструкторов класса. Почему? Определение конструктора имеет следующий вид:

имя_класса( список_параметров )

// список инициализации членов:
: член1(выражение1 ) ,    // инициализация член1
  член2(выражение2 ) ,    // инициализация член2
// тело функции:
{ /* ... */ }

выражение1 и выражение2 могут быть выражениями любого вида, в частности функциями, которые возбуждают исключения.

Рассмотрим еще раз класс Account, описанный в главе 14. Его конструктор можно переопределить так:

nline Account::
Account( const char* name, double opening_bal )
       : _balance( opening_bal - ServiceCharge() )
{
   _name = new char[ strlen(name) + 1 ];
   strcpy( _name, name );

   _acct_nmbr = get_unique_acct_nmbr();
}

Функция ServiceCharge(), вызываемая для инициализации члена _balance, может возбуждать исключение. Как нужно реализовать конструктор, если мы хотим обрабатывать все исключения, возбуждаемые функциями, которые вызываются при конструировании объекта типа Account?

Помещать try-блок в тело функции нельзя:

inline Account::
Account( const char* name, double opening_bal )
       : _balance( opening_bal - ServiceCharge() )
{
   try {
      _name = new char[ strlen(name) + 1 ];
      strcpy( _name, name );

      _acct_nmbr = get_unique_acct_nmbr();
   }
   catch (...) {
      // специальная обработка
      // не перехватывает исключения,
      // возбужденные в списке инициализации членов
   }
}

Поскольку try-блок не охватывает список инициализации членов, то catch-обработчик, находящийся в конце конструктора, не рассматривается при поиске кандидатов, которые способны перехватить исключение, возбужденное в функции ServiceCharge().

Использование функционального try-блока – это единственное решение, гарантирующее, что все исключения, возбужденные при создании объекта, будут перехвачены в конструкторе. Для конструктора класса Account такой try-блок можно определить следующим образом:

inline Account::
Account( const char* name, double opening_bal )
try
       : _balance( opening_bal - ServiceCharge() )
{
   _name = new char[ strlen(name) + 1 ];
   strcpy( _name, name );
   _acct_nmbr = get_unique_acct_nmbr();
}
catch (...) {
   // теперь специальная обработка
   // перехватывает исключения,
   // возбужденные в ServiceCharge()
}

Обратите внимание, что ключевое слово try находится перед списком инициализации членов, а составная инструкция, образующая try-блок, охватывает тело конструктора. Теперь предложение catch(...) принимается во внимание при поиске обработчика исключения, возбужденного как в списке инициализации членов, так и в теле конструктора.
19.2.8. Иерархия классов исключений в стандартной библиотеке C++

В начале этого раздела мы определили иерархию классов исключений, с помощью которой наша программа сообщает об аномальных ситуациях. В стандартной библиотеке C++ есть аналогичная иерархия, предназначенная для извещения о проблемах при выполнении функций из самой стандартной библиотеки. Эти классы исключений вы можете использовать в своих программах непосредственно или создать производные от них классы для описания собственных специфических исключений.

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

namespace std {
   class exception
   public:
      exception() throw();
      exception( const exception & ) throw();
      exception& operator=( const exception & ) throw();
      virtual ~exception() throw();
      virtual const char* what() const throw();
   };
}

Как и всякий другой класс из стандартной библиотеки C++, exception помещен в пространство имен std, чтобы не засорять глобальное пространство имен программы.

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

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

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

Помимо корневого exception, в стандартной библиотеке есть и другие классы, которые допустимо использовать в программе для извещения об ошибках, обычно подразделяемых на две больших категории: логические ошибки и ошибки времени выполнения.

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

namespace std {
   class logic_error : public exception {  // логическая ошибка
   public:
      explicit logic_error( const string &what_arg );
   };
   class invalid_argument : public logic_error { // неверный аргумент
   public:
      explicit invalid_argument( const string &what_arg );
   };
   class out_of_range : public logic_error { // вне диапазона
   public:
      explicit out_of_range( const string &what_arg );
   };
   class length_error : public logic_error { // неверная длина
   public:
      explicit length_error( const string &what_arg );
   };
   class domain_error : public logic_error { // вне допустимой области
   public:
      explicit domain_error( const string &what_arg );
   };
}

Функция может возбудить исключение 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 <stdexcept>
#include <string>

template <class elemType>
class Array {
public:
   // ...
   elemType& operator[]( int ix ) const
   {
      if ( ix < 0 || ix >= _size )
      {
         string eObj =
         "ошибка: вне диапазона в Array<elemType>::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>elemType>::operator[]()
      cerr << excep.what() <<  "\n ";
      return -1;
   }
}

В данной реализации выход индекса за пределы массива в функции try_array() приводит к тому, что оператор взятия индекса operator[]() класса Array возбуждает исключение типа out_of_range, которое перехватывается в main().

Упражнение 19.5

Какие исключения могут возбуждать следующие функции:

#include <stdexcept>
(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 <stdexcept>

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). Напомним, что эта процедура состоит из трех шагов:

   1. Отбор функций-кандидатов.
   2. Отбор устоявших функций.
   3. Выбор наилучшей из устоявших функции.

Отбор функций-кандидатов зависит от наследования потому, что на этом шаге принимаются во внимание функции, ассоциированные с базовыми классами, – как их функции-члены, так и функции, объявленные в тех же пространствах имен, где определены базовые классы. Отбор устоявших функций также зависит от наследования, поскольку множество преобразований формальных параметров функции в фактические аргументы расширяется пользовательскими преобразованиями. Кроме того, наследование оказывает влияние на ранжирование последовательностей трансформаций аргументов, а значит, и на выбор наилучшей из устоявших функции. В данном разделе мы рассмотрим влияние наследования на эти три шага разрешения перегрузки более подробно.
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*):

Class ZooAnimal {
public:
   // преобразование: ZooAnimal ==> const char*
   operator const char*();

   // ...
};

extern void release( const char* );
extern void release( const Bear& );

ZooAnimal za;

// za ==> const char*
// правильно: release( const char* )
release( za );

Упражнение 19.9

Дана такая иерархия классов:

class Base1 {
public:
   ostream& print();
   void debug();
   void writeOn();
   void log( string );
   void reset( void *);
   // ...
};

class Base2 {
public:
   void debug();
   void readOn();
   void log( double );
   // ...
};

class MI : public Base1, public Base2 {
public:
   ostream& print();
   using Base1::reset;
   void reset( char * );
   using Base2::log;
   using Base2::log;
   // ...
};

Какие функции входят в множество кандидатов для каждого из следующих вызовов:

MI *pi = new MI;
(a) pi->print();   (c) pi->readOn();   (e) pi->log( num );
(b) pi->debug();   (d) pi->reset(0);   (f) pi->writeOn();

Упражнение 19.10

Дана такая иерархия классов:

class Base {
public:
   operator int();
   operator const char *();
   // ...
};
class Derived : public Base {
public:
   operator double();
   // ...
};

Удастся ли выбрать наилучшую из устоявших функций для каждого из следующих вызовов? Назовите кандидаты, устоявшие функции и преобразования типов аргументов для каждой из них, наилучшую из устоявших (если она есть):

(a) void operate( double );
    void operate( string );
    void operate( const Base & );

    Derived *pd = new Derived;
    operate( *pd );
(b) void calc( int );
    void calc( double );
    void calc( const Derived & );

    Base *pb = new Derived;
    operate( *pb );
Категория: С++ | Добавил: r2d2 (29.09.2011)
Просмотров: 474 | Рейтинг: 0.0/0
Всего комментариев: 0
Добавлять комментарии могут только зарегистрированные пользователи.
[ Регистрация | Вход ]
Born in Ussr
Залогиниться
Турниры

/j clan ussr /j clan cccp