sp-dd1 (1119738), страница 6
Текст из файла (страница 6)
try { throw x; }
catch (___) { throw; }
Перевозбуждение исключений позволяет рассредоточить обработку исключения по цепочке вызовов: каждая функция обрабатывает исключения в силу своей компетенции.
Пример.
class Vect {
int *p, size;
public:
Vect (int u = 10);
Vect (const Vect &a);
~Vect();
int &operator[] (int i);
};
/* перегрузка операции индексирования возможна только с помощью нестатической функции-члена от целой переменной, возвращающей ссылку, чтобы можно было менять элементы */
Vect::Vect (int n){
if (n<=0) throw n;
p = new int [size = n];
/* если оператор new выполняется ошибочно, стандарт предписывает ему возбуждать исключение bad_alloc. Однако старые компиляторы просто возвращают NULL. Поэтому стандарт также рекомендует это делать в целях совместимости. */
if (!p) throw “Свободная память исчерпана”;
}
int &Vect::operator[] (int i){
if (i<0 || i>=size) throw “ошибочный индекс”;
return p[i];
}
void g (int k){
try {
Vect x(k);
int l, i;
l = x[10]; x[i] = 100;}
catch (int n){ cout<<”Неверный размер ”<<n<<endl; throw;}
catch (const char *str) {cout<<str<<endl; }
}
Спецификация исключений
Мы не всегда пишем код с нуля. В библиотечных классах тоже есть исключения.
void f() throw (X,int) { ... }
Такое об’явление означает, что функция может вызывать throw X, throw int
void f() throw (X,int) { ... }
А это значит, что функция не должна возбуждать исключений совсем.
По умолчанию функция может возбуждать любое исключение. Если функция попытается возбудить исключение, которого не должна, вызывается функция unexpected(), прекращающая выполнение программы. Её также можно переопределить:
set_unexpected(handle);
Спецификация исключений не входит в профиль функции, поэтому она не наследуется. При использовании виртуальных функций мы можем повторить или уменьшить список возбуждаемых исключений.
Перегрузка функций.
Статический полиморфизм позволяет давать одно имя нескольким функциям. Как правило, эти функции имеют схожую семантику, но отличаются списком формальных параметров. Какая функция будет вызвана, определяется на этапе трансляции. О перегрузке функций можно говорить только в пределах одной области видимости. Кстати, когда мы об’являем несколько конструкторов одного класса – это тоже перегрузка функций.
Проблема поиска подходящей перегруженной функции (best matching) – нетривиальная задача. Для начала опишем этот алгоритм для функции одного аргумента.
-
Поиск функции, точно совпадающей по типу параметра (точное отождествление). Если функция вызывается от параметра типа T, то может быть вызвано описание с прототипом от T, T&, const T, const T&, переопределения этих типов с помощью typedef, T[] эквивалентно T*, функция эквивалентна указателю на функцию.
-
Если не найдено точное соответствие, то пробуем применить стандартные преобразования. На втором шаге могут сработать безопасные преобразования – целочисленное или вещественное расширение (integral/floating promotion). Тут bool, char, short, enum (знаковые или беззнаковые) преобразуются к int или unsigned, float преобразуется к double.
-
Если не получилось выполнить шаг 2, пробуем все остальные стандартные преобразования: оставшиеся арифметические преобразования и преобразования указателей и ссылок (указатель на производный класс приводится к указателю на однозначный доступный базовый класс, любой указатель приводится к void*, 0 воспринимается как NULL).
-
Пользовательские преобразования - рассматриваются конструкторы, которые могут быть вызваны с одним параметром. Также рассматриваются специальные функции преобразования типов.
-
Если ничего не помогло, придётся вызывать функцию с ‘…’.
Особенности четвёртого шага:
1. Отсутствие транзитивности пользовательских преобразований. То есть, за один раз не может выполниться более одного преобразования типа.
class X { public: operator int(); ... };
class Y { public: operator X(); ... };
void f(){ Y a; int b; ...
b = a; // нельзя
}
Можно явно указать b = a.operator X().operator int();.
2. Пользовательские преобразования могут применяться неявно, только если они однозначны.
class B {
public: B (int i);
operator int();
B operator+ (int B);
};
void f(){
B l(1); ... l+1 ...
}
Возникает неоднозначность: то ли l стоит преобразовать к int с помощью определённого преобразования и складывать числа, то ли вызвать конструктор от int и складывать об’екты типа B.
3. Конструктор должен быть описан так, чтобы он допускал неявный вызов. То есть, конструктор не может быть описан как explicit.
class X { public: X(int); };
X a(1); X b = 2; // так можно
Теперь изменим об’явление:
class X { public: explicit X(int); };
X a(1); // так можно
X b = 2; // так нельзя!
X с = X(2); // так можно
Зачем же нужна такая конструкция? Вспомним наш класс String.
class String { String (int n); ... };
String s1 = 10;
String s2 = ‘a’;
Этого нам не запрещает так делать. Но, если мы допишем explicit к конструктору, то такая нелогичная запись не прокатит и придётся вызывать через скобочки.
Алгоритм поиска наилучшего соответствия для вызова функции с произвольным числом параметров N:
-
По каждому из параметров ищется best matching по пятишаговому алгоритму за тем исключением, что если на каком-то шаге несколько кандидатов, способных обслужить вызов, запоминаем все их. В итоге получаем N множеств возможных функций.
-
Ищем пересечение этих множеств. Если оно пусто, то нет подходящей функции. Если пересечение содержит 2 или более элемента, то неоднозначность. Но если там одна функция, она и обслужит вызов.
Пример.
class X { public: X (int); ... };
class Y { ... };
void f (X, int); /* 1 */
void f (X, double); /* 2 */
void f (Y, double); /* 3 */
Пусть мы вызываем f(1,5);. По первому параметру мы оставляем 1 и 2 (пользовательские преобразования), по второму – 1 (точное соответствие). Пересечение даёт первый вариант.
Теперь попробуем вызвать f(1,5.0);. По первому параметру мы оставляем 1 и 2 (пользовательские преобразования), по второму – 2 и 3 (точное соответствие). Пересечение даёт второй вариант.
Пример на *пятый шаг*.
class R { public: R (double); ... };
void f (int, R);
void f (int, ...);
void g () {
f (1, 1); // первый обработчик
f (1, “preved!”); // второй обработчик
}
Шаблоны функций.
Настало время поговорить о типовом полиморфизме. Эта идея в C++ появилась не впервые, она была реализована ещё в языка Ada.
Многие функции делают примерно одно и то же, но работают с разными типами данных. Например, алгоритм сортировки почти не зависит от типа сортируемых элементов. Мы можем описать шаблон, который можно настраивать на типы. Просто надо будет написать соответствующие функции сравнения и т.д.
Пример.
int max (int x, int y) { return x>y? x: y; }
Сделаем теперь функцию независимой от типов.
template<class T> T max (T x, T y) { return x>y? x: y; }
У этой функции только типовые параметры. Ключевое слово class не значит, что T должен быть пользовательским типом. Можно передавать данные встроенных типов также. (Создатели языка опять не придумали нового ключевого слова)
Параметров шаблона может быть несколько, тогда они перечисляются через запятую.
void f() {
max (1,2);
/* по параметру автоматически определяется тип шаблона.
вызовется int max<int> (1,2) */
max (‘0’,’a’); // от char
max (‘a’,100); // неоднозначность. так нельзя!!
max (2.5,1); // неоднозначность. int или double ???
Есть возможности разрешить неоднозначность:
-
явная квалификация: max<int> (‘a’,100);
-
перегрузка шаблонных функций:
int max (char c, int i) { return c>i? c: i; }
Можно перегрузить шаблон и так:
int max (int x, int y) { return x>y? x: y; }
Теперь нам непонятно, что вызывать: шаблонную функцию или явную? Модифицируем наш алгоритм поиска best matching функции, добавив шаг между 1 и 2. На первом шаге, не думая о шаблонах, ищем точное соответствие. На новом, *полуторном* шаге, пытаемся сгенерировать подходящую функцию по шаблону, чтобы получить точное соответствие. Если попытка неудачная, переходим к шагу 2 и далее выполняем алгоритм, как и раньше.
Тип параметра шаблона можно указать и явно. Эта возможность относительно новая в языке. Об этом пишет Бьярн Страуструп в интереснейшей книге ‘Дизайн и эволюция языка C++’. Однако в некоторых ситуациях не обойтись без явного указания.
Пример такой ситуации.
template<class T, class U> T convert (U u) { ... return t; };
void g (int i){
convert (i); // U = int; T – непонятно L
}
Надо учитывать, что при поиске соответствия компилятор не обращает внимания на то, чему мы присваиваем результат работы функции. Контекст не рассматривается!
Из этой ситуации можно выйти так:
convert<double> (i); // U = int; T = double
При спецификации типа указанный тип подставляется на место первого параметра, если указаны два типа – на место первого и второго, и так далее.
Мы можем придать новый смысл функции, например, заставить искать максимальный элемент в массиве. Для этого перегрузим её так:
template<class T > T max (T *p, int size) { ... };
А сможем ли мы искать с помощью нашей шаблонной функции максимум среди об’ектов класса Complex, описанного нами выше? То есть, можно ли написать так:
Complex a(1,2), b(3,4);
max (a,b); // T = Complex
Проблема, очевидно, в том, что в классе не определены операции сравнения. Однако у ТВ как-то получилось это скомпилировать. Программа даже запустилась и выдала результат! Но результат совершенно не подвластен здравому смыслу. Вы можете покопаться в исходниках и удовлетворить всеобщий интерес рассказом о том, что там происходит.
Вообще, к сожалению, компиляторы далеко не всегда хорошо держат стандарт при работе с шаблонами.
Шаблоны классов.
Шаблоны классов тоже зачастую бывают разумными. Как правило, они применяются при описании контейнеров, то есть типов, которые содержат в себе каким-то образом структурированные об’екты других типов. Например, стек можно реализовать как контейнер. Операции над ним, вообще говоря, не зависят от типа его элементов.
Пример. Контейнер-вектор.
template<class T> class Vector {
T *p;
int size;
public: explicit Vector (int);
T& operator[] (int i);
};
Vector<int> x(20); // вектор из 20 целых чисел
Vector<Complex> y(100); // вектор из 20 комплексных чисел
Инстанцирование – процесс генерации класса по шаблону.
Описывать функции вне класса нужно так:
template<class T> T& Vector<T>::operator[] (int i) { ... }
Пример. Класс с нетиповым параметром.
template<class T, int size> class buffer { ... }
buffer <char,1024> x;
buffer <char,512*2> y;
buffer <int,1024> z;
Возникает вопрос, какие переменные будут иметь одинаковые типы? Есть общее правило: инстанцированные классы будут считаться одинаковыми, если типовые параметры совпадают, а нетиповые равны по значению. x и z, очевидно, разного типа, так как типовые параметры не совпадают. Для нетиповых параметров должна быть определена операция сравнения на равенство. Получается, что параметры шаблона не могут быть вещественными.
/* на этом изложение теории языка C++ считается законченным. На лекциях ещё будет рассказ о STL, но это хз когда. */
Удачи всем в субботу! ;)














