Лекции (1129116), страница 22
Текст из файла (страница 22)
type StrArray is array (range <>) of STRING;
procedure StrSort(A :inout StrArray) is new SORT(INTEGER, STRING,StrArray,"<");
У компилятора должно хватить ума, чтобы сообразить, что данная операция "<" относится именно к типу STRING (это возможно, поскольку мы указали, что она относится к типу Т). Но может быть тогда вообще можно не указывать операцию сравнения, а компилятор пускай сам подставит ее? Это возможно, но для этого четвертый параметр нужно описать иначе:
with function "<" (x,y:T) return BOOLEAN is <>;
Это означает, что при конкретизации программист не обязан указывать конкретную процедуру сравнения, а компилятор должен подставить соответствующую процедуру, профиль которой соответствует фактическому параметру Т.
Когда мы описываем статические абстракции на языке Ада, то мы должны заранее определять, что именно мы будем параметризовать, и при этом так, чтобы компилятору было доступно максимум информации, для того, чтобы он мог проконтролировать. Отсюда возникает невероятно большое количество возможных параметров:
-
Параметры переменные. (SIZE: INTEGER). Значение фактического параметра может быть только константным.
-
Параметры типы. (type T is private) По умолчанию к этому типу данных может применятся только операция присваивания. Если же нам нужны другие операции, то тогда эти операции необходимо передать явным образом.
-
Параметры подпрограммы. Эти параметры, в частности, нужны, когда необходима дополнительная информация о типе Т.
-
Регулярные типы. (array (INDEX range <>) of T). Спецификация регулярного типа очень похожа на спецификацию неограниченного массива. При этом, чаще всего, тип индекса и тип элементов должны тоже передаваться в списке параметров.
-
Дискретные типы. (type T is range <>, T is <>).
-
Вещественные типы. (type T is digits <>, type T is delta <>).
Обратим внимание на следующее свойство языка Ада – в языке нет подпрограммного типа данных. Возникает вопрос, как передавать параметры-функции? Если мы пишем функцию интеграла, то у нее должен быть параметр-функция. Если мы хотим написать подпрограмму, аргументом которой должна быть другая подпрограмма, то мы должны написать родовую подпрограмму. Это сделано, прежде всего, из соображений надежности. Интересно, что в Аде-95 от этого требования отказались, и ввели подпрограммный тип данных. В Аде-83 процедура интегрирования будет выглядеть примерно так:
generic
… // какие-то параметры
with function F(X:REAL) return REAL;
function INTEGRAL(…) return REAL;
В качестве самостоятельного упражнения попытайтесь написать полную спецификацию функции INTEGRAL.
Заметьте, что даже уже из процедуры SORT вытекает некоторая концептуальная несогласованность средств статической параметризации в языке Ада. Слишком много информации требуется указывать программисту. Когда мы попытались параметризовать тип элемента массива, то пришлось еще "тянуть" три статических параметра. Причем это необходимо, потому что это свойство вытекает из требований раздельной трансляции. Только при наличии этой информации есть возможность контроля. Но на самом деле проблемы возникают не только из-за этого. Когда происходит генерация новой процедуры SORT, то генерация нового тела не происходят, а создаются лишь некоторые таблицы компилятора, тело же используется одно и то же (но хитрым образом запрограммированное). Сложность языка Ада в частности связана с ее эффективностью, потому что мы можем иметь хоть сотню объектов Stack и пятьдесят объектов SORT, но тело у них будет соответственно одно и тоже.
Кроме того, реализация родового сегмента, как и спецификация, тоже не зависит от точки конкретизации. Мы можем оттранслировать тело родового сегмента совершенно не имея представления о контексте конкретизации. Это является безусловным достоинством, но за это достоинство приходится платить огромным количеством параметров. Механизм статической параметризации направлен на надежность и на эффективность реализации, но не направлен на удобство программирования.
Совершенно другая ситуация в языке С++ - все с точностью до наоборот: мощный и удобный механизм статической параметризации, но огромная нагрузка на компилятор. По сравнению с богатым набором статических параметров в языке Ада, в языке С++ есть только два вида статических параметров – это параметры-переменные и параметры-типы. О специфике типа компилятор догадывается только из контекста конкретизации и контекста тела. Т.е. чтобы компилятор мог сгенерировать код, ему должна быть доступна реализация тела, объявление и контекст конкретизации. Нагрузка на компилятор, особенно в режиме раздельной трансляции существенно возрастает. Кроме этого, возможна ситуация, когда не очень хороший компилятор будет порождать лишние тела функций. Программист, который неаккуратно будет писать программу, может обнаружить, что его программа почему-то невероятно разрастается.
Лекция 18
Мы разобрали механизм статической параметризации в Ada. Заметим, что только C++ и Ada поддерживают эти механизмы.
Достоинства статической параметризации в Ada являются:
-
эффективность работы с родовыми модулями, родовые модули транслируются только один раз.
-
имея спецификацию родового модуля в трансляционной библиотеке и имея оттранслированный родовой модуль в программной библиотеке, соответствующие библиотеки можно передавать сторонним разработчикам. Это привлекает тем, что программный код будет скрыт, а видны только соответствующие спецификации;
-
минимизация времени на компиляцию;
Разумеется, за все надо платить. Программисты на Ada платят отсутствием гибкости. Указание соответствующего контекста и всех типов, которые понадобятся для определения параметризованного типа – все это ложится на программиста.
Теперь рассмотрим механизм статической параметризации в C++.
C++. Шаблоны
Обратим внимание на то. что в C++ 1.0, который был выпущен в 1986 году, механизма шаблонов не было. Страуструп признал, что это было упущением.
К 1990 году Страуструп определился с концепцией шаблонов и она вошла в версию языка, но она еще не была обработана. Задержки с выпуском стандарта C++ были связаны с тем, что различные разработчики по-разному трактовали механизм шаблонов.
Существует два вида шаблонов: классы и функции. – объекты статической параметризации. Шаблон имеет вид:
template <список параметров> объявление_функции_или_класса
На первый взгляд, то же самое, что и в Ada. Основное же различие в том, что может быть списком параметром и каким образом конкретизируется соответствующий фрагмент. На Ada был явный механизм конкретизации ( с помощью конструктора New).
В C++ два вида параметров:
тип class имя
(следует помнить, что класс – всегда тип, но не наоборот)
переменная тип имя
Никакой больше информации в объявлении шаблона не передается. Остальную информацию компилятор «вытягивает» из определения. А потом сравнивает это с тем, что есть на самом деле. Если, например, используется операция, которая не может быть выполнена над фактическим параметром, то в момент конкретизации выдается сообщение об ошибке.
Эта схема гибка для программиста, но компилятору нужно: объявление шаблона, определения функций и класса, а также контекст конкретизаций. Это нужно, чтобы провести полный контроль и сгенерировать объекта.
В результате, шаблоны реализовывались дольше всего, так как очень большие задачи ложаться на компилятор.
Давайте посмотрим на примеры, так как интерпретации шаблонов-классов и шаблонов-функций немного различны. В Ada это шло одинаково.
Шаблонные классы:
template <class T, int size> class Stack {
T body [size];
int top;
public:
T.Pop();
void Push (T x);
заметим, что проблемы с типом, размером и размещением (нам не навязывается способ реализации) решены. Поговорим о том, как теперь это конкретизировать. В любом месте, где доступен данный шаблон, никакой специальной конструкции не предусмотрено. Мы можем написать:
typedef Stack <int, 20> IntStack20
и далее, можем писать:
Stack <int, 20> S;
IntStack20 S1;
Stack <char, 256> S2;
Stack <int, 16> S3;
Сколько здесь различных конкретных классов? Три:
-
Stack <int,20>
-
Stack <char, 256>
-
Stack <int, 16>
Для каждого из них генерируются свои функции Pop, Push. Возникает вопрос – как? В общем случае компилятор может сгенерировать функции только тогда, когда ему будет доступна вся информация. То есть в моменты, когда мы проводим объявление нового класса. Но ведь мы можем не один раз объявлять. Что же компилятору повторять генерацию функций? Конечно, нет. Ведь мы получим функции с одинаковыми именами и профилями, а это – ошибка. Здесь Страуструп отдал проблему на откуп среде разработки. Отчасти поэтому внедрение шаблонов шло медленно.
Таким образом загрузчик и компилятор должны правильно различать функции, которые ошибочно насоздавал одинаковыми программист и те, которые сгенерированы по шаблонам.
Различные среды поступают по-разному. В том же Borland C есть множество настроек на эту тему. Зато с точки зрения программиста все выглядит замечательно.
Если при реализации функций членов шаблонного класса требуется операция, например, «меньше» из определения (не объявления) класса. А мы применяем соответствующий шаблон в контексте конкретизации, в котором нет этой функции. Например, пусть у нас есть:
class X{
… // нет операции “<”
}
заметим, что операция “<” может быть только, если мы ее явно описали. И пусть у нас есть:
template <class T>
class Y {
…
T f( T &x, T &y) {… x < y };
};
Является ли этот шаблон правильным? Да. При анализе шаблона компилятор ничего не может сказать. В данном случае ничего страшного не будет. А вот в случае:
Y <X> a;
компилятор может выдать сообщение об ошибке, ему должны быть доступны: определение шаблона, определение функции. Будет выдано сообщение о том, что к классу X операция сравнения неприменима. Но компилятор проведет анализ только, если у него будет соответствующая информация, описанная выше. Из этого вытекает интересное следствие: при определении шаблонов мы поступаем так, как хорошие программисты на C++ никогда не поступали – все функции шаблонных классов мы обязаны определять в заголовках, так как компилятору нужна информация на уровне исходных текстов.
В результате – очень сложно компилятору, но очень легко программисту.
Как же определять шаблонные функции в случае, если хочется определить их за пределами определения класса. Пусть есть класс Stack, каким образом мы должны описать функцию Pop, если хотим описать ее не как inline, а отдельно. Синтаксис следующий:
template <class T, int size> T Stack <T, size>::Pop() {
if (top>=0) error (“…”);
return body[--top]; // здесь работает конструктор копирования
};
Если в соответствующем фактическом параметре конструктор копирования будет объявлен как приватный, то у нас возникнут проблемы.
Уже говорилось, что функции-члены класса в свою очередь можно рассматривать как шаблоны. Но здесь есть некая специфика языка в случае чистой шаблонной функции. В определении функции члена шаблонного класса явно имеет место отождествление параметров, мы говорим, что везде, где встретится идентификатор ”T” – это идентификатор класса, а где «size» – это константа, которую нужно интерпретировать, как константу.
В случае же обычных функций, ситуация более сложная. Рассмотрим пример функции сортировки. Имеет смысл написать, некоторый шаблонный класс:
template <class T> Vector {
…
public:
explicit Vector (int size);
T& operator [ ] (int i);
~vector( );
};
напишем определение шаблона для соответствующей функции шаблона:
template <class T>
void Sort (Vector <T> & x);
здесь в качестве параметра мы указываем массив. Можно абстрагироваться от класса Vector и написать функцию, имеющую дело с какими-то массивами:
template <class T>
void Sort (T *a, int l, int r);
l, r – границы массива.
В сортировке будут использоваться операции сравнения. Компилятор сгенерирует корректный текст функции сортировки в том случае, если он будет иметь какое-то из двух объявлений, написанных выше, будет иметь определение функции Sort и соответствующий контекст конкретизации. Если к объектам типа T не будет применима операция «меньше», то компилятор на строчке, где конкретизируется функция, выдаст сообщение об ошибке.
Осталось выяснить, каким образом конкретизируется функция. В отличии от классов, где присутствуют некие указания на конкретизацию (вместо “T” – “int”, вместо “size” – “20”), когда генерировать функции – решает компилятор, либо он дает возможность программисту указать место, где следует сгенерировать функции. Но в общем случае современные системы программирования обладают некоторым интеллектом, чтобы избегнуть избыточного дублирования кода. Речь идет о генерации функций членов шаблонного класса.
А вот в случае функций ситуация немного другая. Здесь используется то свойство, что функции можно перекрывать, поэтому на самом деле здесь идет речь о семействе функций Sort. Мы даже можем употреблять выписанные выше шаблоны вместе, так как у них совершенно различные типы параметров. В результате при различных объявлениях, мы получим следующие соответствия:
Vector <int> a1(20); Vector <char> a2(256); int a3[64]; char s[16] | - Sort(a1); - Sort(a2); - Sort(a3,0,63); - Sort(s,0,15); |
Мы не вводим новых имен функций – здесь работает механизм перекрытия. Вначале компилятор пытается отыскать точное отождествление (не шаблонную функцию, у которой в параметре Vector <int>), если не находит, то ищет шаблонную функцию с точным отождествлением. Это отождествление он находит. Каким образом? Он смотрит, у нас есть функция-шаблон Sort с одним параметром:
void Sort (Vector <T> & x);