Лекции (1129116), страница 23
Текст из файла (страница 23)
у нее есть ссылка на вектор T. Что выступает в качестве класса T? В качестве класса T выступает int. Действительно происходит некое отождествление с образцов, а именно, вектор “T&” отождествляется с вектором “int”. Следовательно, можно брать первый шаблон для функции Sort для int и генерировать код.
Аналогично для Sort(a2).
В случае трех параметров компилятор находит прототип с тремя параметрами и использует их.
Мы имеем большую нагрузку на компилятор, но для программиста – все прозрачно.
Интересно то, что в первой версии шаблонов запрещались параметры-переменные в шаблонных функциях. Говорилось, что параметрами шаблонных функций могут быть только классы. То есть типовые параметры. Требовалось (и требуется), чтобы формальный парамет «тип» находился где-то в списке формальных параметров шаблонных функций. Ошибочным будет следующее определение шаблона:
template <class T> T& f( );
Почему? Механизм конкретизации шаблонов основан на правилах перекрытия. В начале наиболее точные (тождественные) совпадения. Кстати, может быть описана и не шаблонная функция Sort, главное, чтобы не было соответствия по параметрам. Следует вспомнить, что в C++ правила перекрытия определяются только по контексту параметров, но не по типу возвращаемых значений (чтобы не анализировать всевозможные контексты, в которых можно обращаться к функции).
Если мы пишем:
f( );
как мы можем провести здесь отождествление? Никак. Это означает, что T должен появляться в списке параметров. Ведь компилятор смотрит на прототип шаблона, затем на список параметров, и пытается произвести отождествление. В принципе, все просто, но вот списочек возможных контекстов, по которым производится отождествление – длинноват (порядка 15 пунктов).
Сложный вопрос – почему были проблемы с параметрами-переменными. Потому что произвести отождествление с именем просто, а что делать с размером?
Сейчас это ограничение снято, потому что в некоторых случаях можно написать:
int a[20]
f(a);
здесь компилятор может понять размер массива. Если у нас:
int *b;
или
extern c [];
когда размер переменной не известен, компилятор выдаст ошибку. Поэтому в первых версиях параметр-переменная запрещалась, как вид.
Ну и есть контексты, для которых все корректно:
template <class T, int S>
T & f (Stack <T,S> &S);
теперь
Stack <int, 64> S; - корректно
Мы помним, что определение:
template <class T> T& f( ); - ошибочно
Но если очень хочется, то мы можем определить некоторый класс, который будет содержать лишь одну функцию f ( ). Вместо ошибочного объявления мы можем написать несколько сложных:
template <class T> class F {
public:
static T & f( );
}
здесь уже никаких проблем нет, и мы можем писать:
F<int>::f( );
Мы явным образом указываем контекст. С помощью такого трюка мы обходим ограничение.
С одной стороны механизм шаблонов в C++ очень сильный. В случае, если система программирования позволяет эффективно реализовать вышеописанную схему, то шаблонами можно и даже нужно пользоваться. Конечно же, эта схема удобнее и лучше системы родовых сегментов в Ada.
Возникает вопрос, почему, если концепция статической параметризации настолько хороша, другие языки (Delphi, Java) не поддерживают статическую параметризацию? А можно ли что-то заменить? Можно ли шаблоны смоделировать через другие средства языка. Когда мы говорили о контейнерах, то их следует рассматривать, как шаблонные классы, более того, в любой реализации C++ будет некая библиотека контейнеров, которая позволяет генерировать произвольные контейнеры. В чем достоинства и недостатки?
Достоинства очевидны, если у нас есть хэш-таблица, то достаточно реализовать код работы с ней (достаточно нетривиальный код) только один раз, а затем лишь пользоваться.
Недостатки в том, что она все же статическая. Мы не можем написать гетерогенные контейнеры, которые будут содержать объекты разных классов. Путь через (void *) делает большую дыру в защите. Да и само использование (void *) делает контейнеры ненужными.
Механизм шаблонов – очень сильное оружие, которое может рвануть в руках. Например, если мы пишем вектора для 50 типов данных, то у нас будут сгенерированы 50 одинаковых функций, и компилятор не обладает интеллектом, чтобы увидеть эту общность. Заметим, что контейнеры типа вектора на Ada можно написать, не теряя общности так, что будет сгенерирована только одна порция кода – эффективность значительно больше. Когда же программист начинает писать шаблоны сам, особенно не задумываясь над тем, что же нужно делать и что происходит, то обнаруживает в конце невероятное разбухание кода. Например:
template <class T> class Vector {
T** body; };
T1* Vector <T1*> V1;
T2* Vector <T2*> V2;
…
T5* Vector <T5*> V5;
Мы получим 5 различных функций.
Глава 7. Исключительные ситуации в языках программирования.
Исключения имеют своеобразную языковую природу, потому что с одной стороны, они вроде бы типы данных, а с другой стороны, и нет. Они определяют некий алгоритм обработки, но с другой стороны, не являются процедурами или функциями.
Интересно, что понятие исключения возникло в языках программирования достаточно рано. Уже в ранних языках программирования были так называемые ON-ситуации:
ON ситуация оператор
Примерно то же самое есть сейчас в языке Visual Basic. При возникновении ситуации (набор которых обычно был в базисе языка), выполнялся некий оператор (обычно это вызов подпрограммы), после которого, программа возобновлялась после точки его вызова, либо останавливалась. Ситуация определяет некоторое условие, при выполнении которого (далее в программе) сразу выполнялся оператор. К ситуациям, например, относились следующие константы: OVERFLOW, ERROR, ENDFILE. Что является ситуацией? С одной стороны – это набор некоторых ошибочных условий, т.е. аварийная ситуация, с другой стороны – это условия, которые зависят от внешней среды, такие как ENDFILE. С этой точки зрения, ON-ситуации можно рассматривать, как реакции на события, которые в принципе запланированы, но которые хотелось бы обрабатывать несколько нестандартным образом. Это очень похоже на те механизмы выхода из середины цикла, которые мы изучали, когда говорили о структурах управления. Сейчас существует устоявшаяся точка зрения, что исключения – это действительно аварийные ситуации, которые требуют нестандартного подхода, хотя их можно использовать и для какого-то нестандартного способа передачи управления, но этот подход является несколько порочным.
Механизм исключительных ситуаций практически одинаков во всех языках программирования, которые мы сейчас рассматриваем, что свидетельствует о том, что программисты пришли к некоторому консенсусу. Исключения, в отличие от шаблонов, есть в большом количестве языков, более того, языки, в которых они не поддерживаются, не являются языками индустриального программирования. В частности, отсутствие этой концепции в системе визуального программирования Power Builder значительно сводит на нет все достоинства этой системы.
В ранних языках программирования, исключения очень напоминали механизм обработки прерываний, реализованный на несколько более высоком уровне. Но такой механизм устарел. Современный подход, мы видим, прежде всего, в языке Ада, который с этой точки зрения повлиял и на С++. И практически в неизменном виде этот механизм присутствует в таких системах, как Delphi и Java. Самый простой механизм обработки исключений в Аде, потому что там нет наследования. Основное отличие языков Delphi и Java от C++ и Ады, состоит в том, что в первом случае, исключения являются имманентной частью языка. Мы будем рассматривать исключения во всех языках сразу, но по аспектам.
1. Объявление исключений.
В языке Ада введено специально ключевое слово exception, которое можно считать, как бы, объявлением некоторого специального типа исключений: имя:exception. На имя исключения распространяются те же самые правила видимости, что и на обычные имена объектов данных. Есть предопределенные имена исключений, которые связаны с какой-то аварийной ситуацией, которая может возбуждаться либо аппаратурой, либо механизмом квазистатического контроля. В Аде, как и в остальных языках, можно определять и свои исключительные ситуации.
В языках Delphi и Java исключения представляют собой классы. В С++ исключения – это произвольный тип данных, т.е. исключения могут быть сопоставлены типу int или char*, и т.д. С++ ввел совершенно новый подход, потому что просто идентификатор исключения несет в себе слишком мало информации. Если, например, произошел выход за границу массива, то хотелось бы знать, за границу какого массива, в какой точке, и т.д. В С++ мы можем в момент возникновения ошибки передать нужную информацию через объект исключения, туда, где она может пригодится. Java и Delphi отчасти разделили подход С++, но ограничились только классами, которые являются наследниками базовых классов (Throwable и Exception соответственно).
В классе Exception языка Delphi, например, уже есть конструктор Create, параметром которого является строка, в которую можно записать сообщение об ошибке. Если нам нужна более содержательная информация об ошибке, то мы можем вывести из этого класса свой класс, добавив в него новые возможности. Java разделяет все исключения на два класса – пользовательские, которые возбуждает пользователь, и системные, которые возбуждаются аппаратно, либо виртуальной машиной. На системные исключения программист может не реагировать, а на пользовательские исключения – обязан реагировать. Наличие базисного класса исключений дает очень много, и из-за отсутствия такого класса в С++ программу приходится начинать с конструирования своего механизма исключений.
Исключение – объявляются как некие типы данных, но в то же время, исключения не являются типами данных. Исключениям лишь соответствуют некоторые типы данных. Объекты данных соответствующего типа возникают лишь в момент возбуждения исключения.
2. Определение исключения.
Для того, чтобы определить исключение, мы должны задать некий код, который обрабатывает исключения. Зачем исключения понадобилось вводить как особую языковую конструкцию? Прежде всего, для того, чтобы отделить нормальный код (код, в котором возникают ошибки) от кода по устранению ошибок.
В языках, где нет механизма исключений (Си, стандартный Паскаль), единственный способ писать надежные программы – это рассматривать некоторое дерево вариантов, которое содержит множество ветвей обработки ошибок. В любой точке, где только может произойти ошибка, программист должен вставлять оператор if. В результате – линейная программа превращается в дерево.
Определение исключений состоит в том, что мы объявляем исключение, и пишем к нему какой-то код, который и будет распределен по программе. Нужно указать область, где может произойти ошибка, и указать реакции на конкретные ошибки.
В языке Ада предполагается, что ошибка может возникнуть в любом месте программы и в любом месте программы ее можно обработать. Единицей кода, в которой может произойти исключение, является блок операторов, ограниченный begin и end. Блоком является тело подпрограммы, блоком является инициализационная часть тела пакета, и т.д. Везде, где могут стоять операторы, в конце некоторой их последовательности может вставляться блок по обработке исключений:
exception
when список_имен_исключений =>
операторы
when список_имен_исключений_2 =>
операторы
when others =>
операторы
Определением данного исключения является совокупность всех реакции на это исключение в зоне его видимости. Получается, что определение исключения как бы "размазано" по программе.
В языке С++ все сложнее. Страуструп допускает наличие программистов, которые не используют исключения. Поэтому блок, который может, по мнению программиста, содержать ошибки, специально помечается ключевым словом try. В конце try-блока выписываются обработчики исключений, которые могут быть трех видов:
catch( тип ) { блок_реакции }
catch( тип имя ) { блок_реакции }
catch( … ) { блок_реакции }
Т.е. обработчик реагирует на ошибку данного типа и при этом может принимать дополнительную информацию. Catch c тремя точками принимает оставшиеся исключения:
try {
f();
g();
…
} catch( int ) { printf("int error"); }
catch( char* p ) { printf(p); }
catch( … ) { printf("error"); }
Языки Java и Delphi пошли по пути, начертанном Страуструпом. В языке Java также определяется try-блок, за которым стоят операторы catch(тип имя), но в конце также может стоять оператор finally, о котором мы поговорим несколько позже, потому что он к механизму обработки исключительных ситуаций имеет достаточно опосредованное отношение. В Java исключительные ситуации являются прямыми или косвенными наследниками класса Throwable (этот класс интегрирован в язык, и компилятор знает, что его обрабатывать нужно иначе). Поэтому компилятор следит, чтобы параметр оператора catch был наследником этого класса. Конструкцию С++ catch(…) в Java моделируется следующим образом: catch(Throwable any).
В языке Delphi все организовано почти точно также, но отличается несколько синтаксически:
try
операторы
except
on имя : тип do operator1
on имя2: тип2 do operator2
except