<< Пред. стр. 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
throw out_of_range( eObj );
}
return _ia[ix];
}
// ...
private:
int _size;
elemType *_ia;
};
Для использования предопределенных классов исключений в программу необходимо включить заголовочный файл
int main()
{
try {
// функция main() такая же, как в разделе 16.2
}
catch ( const out_of_range &excep ) {
// печатается:
// ошибка: вне диапазона в Array
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() (она определена в заголовочном файле
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*):