Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
Скачиваний:
38
Добавлен:
16.04.2013
Размер:
198.14 Кб
Скачать

Шаблоны, обобщенное программирование и STL

В C++ ключевое слово template используется для обеспечения параметрического полиморфизма (parametric polymorphism). Он позволяет применять один и тот же код к разным типам, причем тип является параметром тела кода. Параметрический полиморфизм – форма обобщенного программирования. Многие из наших классов использовались для хранения данных конкретного типа. Данные обрабатывались одним и тем же способом независимо от типа. Определения шаблонных классов и шаблонных функций делают возможным повторное использование кода простым, безопасным с точки зрения типов образом, что позволяет компилятору автоматизировать процесс инстанцирования (instantiation) типа. Оно выполняется, когда фактический тип заменяет тип-параметр, присутствующий в коде шаблона.

Полиморфизм: способность принимать различные формы.

Шаблонный класс stack

Изменим класс ch_stack, чтобы получить параметризованный тип из раздела 6.2.1, «Копирующий конструктор», на стр. 160.

В файле stack_t1.cpp

//Реализация шаблона стека

template class TYPE

class stack {

public:

explicit stack (int size = 100)

: max_len (size) , top(EMPTY) , s(new TYPE [size])

( assert (s != 0;) ; }

stack ( ) { delete [ ] s; }

void reset ( ) { top = EMPTY; }

void push (TYPE c) { s[++top] = c; }

TYPE pop ( ) { retyrn s[top--]; }

TYPE top_of ( ) const { return s[top]; }

bool empty ( ) const { return top == EMPTY; }

bool full ( ) const { return top == max_len – 1; }

private:

enum { EMPTY = -1 };

TYPE* s;

int max_len;

int top;

};

Синтаксис объявления класса предваряется конструкцией:

template class идентификатор

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

stackchar stk_ch; //стек на 100 char

stackchar* stk_str(200); //стек на 200 char*

stackcomplex stk_cmplx(500); //стек на 500 complex

Механизм шаблонов спасает нас от необходимости переписывать объявления классов, единственное изменение в которых заключалось бы в объявлениях типа. Эта схема служит альтернативой использованию void* в качестве универсального типа указателя. При обработке такого типа в качестве части объявления в коде всегда должны использоваться угловые скобки  , как показано ниже.

В файле stack_t1.cpp

//Обращение набора строк, представленных как char*

void reverse (char* str [ ], int n)

{

stackchar* stk(n) ;

for (int i = 0; i < n; ++i)

stk.push(str[i]) ;

for (int i = 0; i < n; ++i)

str[i] = stk.pop ( ) ;

}

//Инициализация стека комплексных чисел из массива

void init (const complex c [ ] , stackcomplex& stk, const int n)

{

for (int i = 0; i < n; ++i)

stk.push(c[i]) ;

}

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

TYPE top_of ( ) const { return s[top] ; }

должна быть записана как

templateclass TYPE TYPE stackTYPE : : top_of ( ) const

{ return s[top] ; }

Да, это очень изящно и требует некоторой привычки, но иначе компилятор не знал бы, что TYPE является аргументом шаблона. В качестве другого примера напишем определение деструктора для templateclass TYPE  stack.

templateclass TYPE stackTYPE : : stack ( )

{ delete [ ] s; }

Шаблоны функций

Часто несколько функций сводятся к одному и тому же коду, если отвлечься от различий в типах. Например, функция инициализации содержимого массива другим массивом такого же типа. Суть ее такова:

for ( i = 0; i < n; ++i )

a[i] = b[i];

Большинство программистов на С автоматизируют подобные вычисления с помощью простого макро:

#define COPY (A, B, N) \

{int i; for (i = 0; i < (N); ++i) (A) [i] = (B) [i]; }

Программирование, которое работает независимо от типа, является формой обобщенного программирования. Макро define как правило правильно, но это макро небезопасно с точки зрения типов. Другая проблема с макро заключается в том, что оно может привести к многократному вычислению единственного параметра (см. Упражнение 3 на стр. 278). Пользователь легко может смешать типы там, где преобразования неуместны. Программисты на С++ могут использовать различные формы преобразований и перегрузки для достижения сходных результатов. Однако при отсутствии надлежащих преобразований и сигнатур не будут производиться никакие действия. Шаблоны представляют еще один механизм обобщенного программирования.

В файле copy1.cpp

templateclass TYPE

void copy (TYPE a[ ], TYPE b[ ], int n)

{

for (int i = 0; i < n; ++i)

a[i] = b[i];

}

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

В файле copy1.cpp

double f1[50], f2[50];

char c1[25], c2[50];

int i1[75], i2[75];

char* ptr1, *ptr2;

copy (f1, f2, 50);

copy (c1, c2, 10);

copy (i1, i2, 40);

copy (prt1, prt2, 100);

copy (i1, i2, 50);

copy (prt1, f2, 50);

Два последних вызова copy( ) не откомпилируются, так как их типы не могут смешиваться – произойдет ошибка компиляции. Тип фактических аргументов не соответствует шаблону. Если бы было выполнено приведение f2 в виде

copy(i1, (int*) f2, 50);

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

В файле copy2.cpp

templateclass T1, class T2

void copy (T1 a[ ], T2 b[ ], int n)

{

for (int i = 0; i < n; ++i)

a[i] = b[i];

}

При такой форме компилирования производится поэлементное преобразование. Обычно такое преобразование предпочтительнее, кроме того оно безопаснее.

Соответствие сигнатуре и перегрузка

Довольно часто обобщенные процедуры не срабатывают в некоторых особых случаях. Ниже приведена форма шаблона для перестановки, работающая с основными типами.

В файле swap.cpp

//Обобщенная перестановка

templateclass T

void swap (T& x, T&)

{

T temp;

temp = x;

x = y;

y = temp;

}

Любой вызов, обеспечивающий однозначное соответствие аргументам шаблона, приводит к созданию функции на основе данного шаблона.

int i, j;

char str1[100], str2[200], ch;

complex c1,c2;

swap (i, j); // правильно, i и j - целые

swap (c1, c2); // правильно, c1 и c2 - комплексные

swap (str1[50], str2[33]); // правильно, обе переменные -

// типа char

swap (i, ch2); // i int, ch char - недопустимо

swap (str1, str2); // недопустимо: попытка поменять

// местами массивы

Чтобы swap ( ) могла работать со строками, представленными в виде символьных массивов, напишем для этого особого случая следующий код:

void swap (char* s1, char* s2)

{

int max_len;

max_len = (strlen(s1) >= strlen(s2)) 

strlen(s1) : strlen(s2);

char*temp = new char[max_len + 1];

strcpy(temp, s1);

strcpy(s1, s2);

strcpy(s2, temp);

delete [ ]temp;

}

С добавлением этого кода для особого случая. Точное соответствие этой нешаблонной версии сигнатуре вызова swap ( ) имеет приоритет перед точным соответствием при постановке в шаблон.

Алгоритм выбора перегруженной функции

  1. Строгое соответствие (при допущении некоторых тривиальных преобразований) для нешаблонных функций.

  2. Строгое соответствие для шаблонов функций.

  3. Обычное разрешение аргументов для нешаблонных функций.

Шаблоны классов

В примере stackT из раздела 9.1. «Шаблонный класс stack», на стр. 245, мы показали обычный случай параметризации классов. В этом разделе мы хотим обсудить различные особенности параметризации классов.

Друзья

Шаблонные классы могут содержать «друзей». Дружественная функция, которая не использует спецификацию шаблона, универсальна – имеется единственный ее экземпляр для всех инстанцирований шаблонного класса. Дружественная функция, которая задействует аргументы шаблона, специфична для каждого инстанцирования

класса.

templateclass T

class matrix {

public:

friend void foo_bar( ) ; // универсальна

friend vectT product (vectT v); // специфична

. . . . .

};

Статистические члены

Статистические члены не универсальны, они специфичны для инстанцирования:

templateclass T

class foo {

public:

static int count;

. . . . .

};

. . . . .

fooint a;

foodouble b;

Статистические переменные fooint : : count и foodouble : : count различны.

Аргументы шаблона класса

И классы, и функции могут иметь несколько аргументов шаблона. Напишем функцию, которая будет преобразовывать один тип значения к другому, при условии, что первый тип не «уже» второго.

В файле coerce.cpp

templateclass T1, class T2

bool coerce (T1& x, T2& y)

{

if (sizeof (x) < sizeof (y))

return false;

x = static_castT1 (y) ;

return true;

}

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

Аргументами шаблона могут, в частности, быть выражения-константы, имена функций и символьные строки.

В файле array_tm.cpp

templateclass T, int n

class assign_array {

public:

T a[n];

};

. . . . .

assign_arraydouble, 50 x, y;

. . . . .

x = y; //должно работать эффективно

Выгода от такой параметризации состоит в том, что происходит размещение в стеке, а не в свободной памяти (в куче). На многих системах такой режим более эффективен. Допустимый диапазон ограничен конкретной константой, так что операции, требующие соответствия длин массивов, безопасны с точки зрения типов и проверяются на этапе компиляции.

Параметризация класса vector

Класс vector – единственный кандидат для параметризации. Давайте выполним параметризацию и обсудим, как vector используется в сочетании с итераторами и алгоритмами. Этот класс является типичным последовательным контейнерным классом, похожим на подобный класс из стандартной библиотеки шаблонов:

В файле vect_it.h

//Тип vector на основе шаблона

templateclass T

class vector {

public:

typedef T* iterator;

explicit vector(int n=100); // создание массива

// из n элементов

vector(const vectorT& v); // копия вектора

vector(const T a[ ], int n); // копия массива

vector( ) { delete [ ]p;}

iterator begin( ) { return p;}

iterator end( ) { return p + size;}

T& operator[ ] (int i); // элемент в пределах границ

vectorT& operator=(const vectorT& v);

private:

T* p; // базовый указатель

int size; // число элементов

};

Везде, где ранее класс vector использовал int как значение, которое должно храниться в отдельных элементах, определение template использует T. Так что закрытый базовый указатель p теперь объявлен как тип T*.

Помещаемое вне класса определение функции-члена должно включать метку разрешения области видимости имя_классаT. Следующие конструкторы для vectT используют T как спецификацию типа для new:

templateclass T

vectorT : : vector (int n = 100) : size(n)

{

assert(n > 0);

p = new T[size];

assert(p != 0);

}

Этот конструктор является конструктором по умолчанию, поскольку имеетаргумент по умолчанию, равный 100. Ключевое слово explicit используется для запрета применения его в качестве преобразователя из int в vector. Проверка утверждений используется чтобы гарантировать, что конструктор выполняет свой «договорные обязательства», когда принимает соответствующие данные на вводе.

templateclass T

vectorT : : vector (const T a[ ], int n)

{

assert(n > 0);

size = n;

p = new T[size];

assert(p != 0);

for (int i = 0; i < size; ++i)

p[i] = a[i];

}

Этот конструктор преобразует обычный массив в вектор. Копирующий конструктор определяет глубокую копию (см. раздел 6.2.1. «Копирующий конструктор», на стр. 160) вектора v.

template class T

vectorT : : vector (const vectorT& v)

{

size = v. size;

p = new T[size];

assert(p != 0);

for (int i = 0; i < size; ++i)

p[i] = v.p[i];

}

Следующий код определяет индексирование вектора с помощью перегрузки оператора «квадратные скобки» []. Возвращаемый тип для этого оператора – ссылка на Т, поскольку он является псевдонимом для элемента, хранящегося в контейнере. Использование такого возвращаемого типа позволяет оператору [ ] иметь доступ к элементу контейнера в качестве lvalue.

templateclass T T&::vectorT : : operator[ ], int i)

{

assert(i >= 0 && I<size );

return (p[I]);

}

Обратите внимание: можем проверить, что границы массива не нарушаются. Мы должны позаботиться и о перегружаемом операторе присваивания (см. Упражнение 7 на стр 278).

template class T

vectorT : : vector (const vectorT& v)

{

assert(v. size = = size);

for (int i = 0; i < size; ++i)

p[i] = v.p[i];

return *this;

}

Клиентский код почти также прост,как и при непараметризованных объявлениях. Чтобы использовать наши объявления, просто добавьте конкретный тип в угловых скобках, который подставляется в шаблон. Это может быть собственный тип, например, int, или тип, определенный пользователем. Следующий код использует приведенные выше шаблоны.

В файле vect_it.cpp

int main ( )

{

vector<double> v(5);

vector<double> : : iterator p;

int I = 0;

for (p = v.begin(); p != v.end ( ) ; ++p)

*p = 1.5 + i++;

do {

--p;

} while (p != v.begin( ));

cout << endl;

}

Вот, что будет выведено этой программой:

5.5, 4.5, 3.5, 2.5, 1.5,

Значения выведены в порядке, обратном порядку их хранения. Это – результат итерации в обратную сторону от значений итератора v.end ( ).

Параметризация quicksort()

Используем vector<T> и шаблоны для построения параметризованной процедуры quicksort(). Будет параметризована каждая из составляющих традиционной quicksort.

В файле quicksort.cpp

//QUICKSORT с использованием класса vector

template class T

void swap(T& i, T& j) { T temp = I; I = j; j = temp; }

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

Сама процедура quicksort – это просто рекурсия. Она использует процедуру разбиения для того, чтобы разделить параметризованный массив vector<T> на две части. Элементы в диапазоне от from до mid – 1 меньше элементов в диапазоне от mid + 1 до to.

template<class T>

void quicksort(T* from, T* to)

{

T* mid;

if (from < to –1) {

mid = partition(from, to);

quicksort(from, mid);

quicksort(mid + 1, to);

}

}

Процедура partition() (разбиение) параметризуется и использует итераторы для отслеживания того, где она находится при перестановке элементов «не на месте». Итераторы front и back отвечают за текущую позицию в клиентском коде. Мы просто инстанцируем нужный тип с угловыми скобками, например:

vector<int> v(n); //создает вектор из n целых

и вызываем инстанцированный алгоритм сортировки как

quicksort(v.begin() , v.end ( ) );

Заметьте, что сортируемый параметризованный тип нуждается в процедуре сравнения, которая определена для него.

Параметризованное дерево двоичного поиска.

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

, «Повторное использование кода: класс двоичного дерева», мы вернемся к рассмотрению этой структуры данных и используем наследование для достижения тех же результатов в области повторного использования кода и по­лиморфизма, которых мы достигли здесь с помощью шаблонов. Два этих подхода имеют различные преимущества и недостатки с точки зрения легкости использова­ния, эффективности и расширяемости.

STL

Стандартная библиотека шаблонов (STL, Standard Template Library) является стан­дартной библиотекой C++, предоставляющей возможности обобщенного програм­мирования для многих стандартных структур данных и алгоритмов. Тремя ее компо­нентами являются: контейнеры, итераторы и алгоритмы, которые поддерживают возможности обобщенного программирования.

Библиотека построена с использованием шаблонов, ее дизайн вполне ортогона­лен. Компоненты можно комбинировать друг с другом, используя в качестве пара­метров как собственные типы C++, так и типы, определяемыми пользователем. Не­обходимо только следить за правильностью инстанцирования различных элементов библиотеки STL.

В нашей первой программе, использующей STL, мы применим списочный кон­тейнер, итератор и обобщенный алгоритм accumulate () (накопление).

В файле stl_cont.cpp

//Применение списочного контейнера

#include <iostream>

#include <list> //списочный контейнер

#include <numeric> //необходимо для accumulate

using namespace std;

void print(const list<double> &lst)

{ //используем итератор для прохода по 1st list<double>::const_iterator p;

for (p = lst.begin( ); p != lst.end( ); ++p)

cout « *p « '\t' ;

cout « endl;

int main()

{

double w[4] = { 0.9, 0.8, 88, -99.99 };

list<double> z;

for( int i = 0; i < 4; ++i)

z.push_front(w[i]);

print(z) ;

z.sort() ;

print(z);

cout « "сумма равна "

« accumulate (z. begin ( ), z.end( ), 0.0) « endl;

}

В этом примере списочный контейнер должен хранить переменные с двойной точностью. Массив из таких переменных помещается в список. Функция print ( ) использует итератор для печати всех элементов списка по очереди. Обратите внимание, что итератор работает как указатель. Итераторы имеют стандартный интерфейс, часты которого являются функции-члены begin () и end (), определяющие начало и конец контейнера. Кроме того, интерфейс списка включает в себя надежный алгоритм сортировки — функцию-член sort (). Функция accumulate() является обобщенной функцией из пакета numeric. Она использует 0.0 в качестве начального значения и вычисляет сумму элементов списочного контейнера путем прихода от начальной позиции z . begin () до конечной z . end ().

Заметьте, что и саму функцию print () можно параметризовать, превратив ее обобщенный алгоритм. Попробуйте выполнить это, используя наиболее общий подход (см. упражнение 14, на стр. 279).

Контейнеры

Контейнеры подразделяются на два основных семейства: последовательные контейнеры и ассоциативные контейнеры. Последовательные контейнеры включают вектор (vectors), списки (lists) и двусторонние очереди (deques). Эти контейнеры упорядочиваются заданием последовательности элементов. Ассоциативные контейнеры включай множества (sets), мультимножества (multisets), отображения (maps), мультиотображения (multimaps) и содержат ключи для поиска элементов. Контейнер-отображение является ассоциативным массивом. Ему необходимо, чтобы была определена операция cpaвенения для хранимых элементов. Все варианты контейнеров имеют похожий интерфейс.

Интерфейсы типичных контейнеров STL

  • конструкторы, включая конструкторы по умолчанию и копирующие конст­рукторы

  • доступ к элементу

  • вставка элемента

  • удаление элемента

  • деструктор

  • итераторы

Проход по контейнеру осуществляется с помощью итераторов. Это «указателеподобные» объекты, доступные в виде шаблонов, и оптимизированные для использования с контейнерами STL.

В файле stl_deq.cpp

//Типичный алгоритм контейнера

double sum (const deque<double> &dq)

{

deque<double>::const_iterator p;

double s = 0.0;

for (p = dq.begin( ); p i= dq.end( ); ++p)

s += *p;

return s;

}

Проход по контейнеру deque (double-ended queue — двусторонняя очередь) произво­дится с помощью const_iterator. Итератор р разыменовывается для получения по очереди каждого хранимого элемента. Такой алгоритм будет работать для последова­тельных контейнеров и со всеми типами, для которых определен operator+= ().

В следующей таблице, описывающей интерфейс контейнерных классов, они обо­значены как CAN.

Определения контейнеров STL

CAN :: value_type что содержит CAN

CAN :: reference тип-ссылка на значение

CAN :: const_reference константная ссылка -

can :: pointer указатель на значение

can :: iterator тип-итератор

CAN :: const_iterator константный итератор

can :: reverse_iterator обратный итератор

CAN :: const_reverse_iterator константный обратный итератор

CAN :: difference_type разница между двумя значениями

CAN:: iterator

CAN :: size_type размер CAN

Эти определения доступны во всех контейнерных классах. Например, vector<char>: :value_type расскажет, что в векторном контейнере храниться символьное значение. Проход по такому контейнеру может быть выполнен с помо­щью vector<char>::iterator.

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

Члены контейнера STL

CAN::CAN( )

конструктор по умолчанию

CAN::CAN(c)

копирующий конструктор

с.begin ( )

начальная позиция контейнера с

с.end( )

конечная позиция с'

с.rbegin( )

с.rend( )

начало для обратного итератора

конец для обратного итератора

с.size( )

число элементов в CAN

с.maxsize( )

наибольший размер

с.empty( ) с.swap(d)

истина, если CAN пуст

обмен элементами двух CAN

' Точнее, с.end () указывает не на последний элемент контейнера, а на (несуществую­щий) элемент, «следующий за последним». Такое соглашение C++ облегчает кодирование операций над контейнерами. — Примеч. перев.

Последовательные контейнеры

Последовательные контейнеры — это вектор, список и двусторонняя очередь. Они содержат последовательность доступных элементов. В C++ тип массива во многих случаях может рассматриваться как последовательный контейнер.

В файле stl__vect.cpp

//Последовательны контейнеры — вставка вектора в deque

#include <iostream.h>

#include <deque>

#include <vector>

using namespace std;

int main()

{

int data [5] =(6,8,7,6,5};

vector<int> v(5, 6); //вектор из 5 элементов

deque<int> d(data, data + 5);

deque<int>::iterator p;

cout << "\п3начения очереди " << endl;

for (p = d.begin(); p != d.end(); ++p)

cout << *p << '\t'; //печать: 6 8765 cout « endl;

d.insert(d.begin(), v.begin(), v.end());

for (p = d.begin(); p 1= d.end(); p++)

cout « *p « '\t'; //печать: 6 6 6 6 6 6 8 7 6 5

}

Пятиэлементный вектор v инициализуется значением 6. Двусторонняя очередь < инициализуется значениями, получаемыми из массива data. Функция-член insert () помещает значения v в диапазоне от v. begin () до v. end () с положения d. begin ().

Разбор программы stl_vect

• int data [5] ={6, 8, 1, 6, 5};

1 vector<int> v(5, 6); //вектор из 5 элементов

deque<int> d(data, data +5);

deque<int>::iterator p;

Вектор v инициализует контейнер из пяти целых элементов значениями 6. Двусторонняя очередь d использует значения итератора data и data + 5 для инициализации контейнера двусторонней очереди. В качестве итераторов могут быть использованы обычные указатели массива. Итератор р объявлен, но не инициализован.

for (p = d.begin(); p != d.end(); ++p)

cout << *p << '\t'; //печать: 6 8 7 6 5 .

Это стандартная идиома прохода при использовании контейнеров и итераторов. За­метьте, что d. end () используется для выхода из цикла, поскольку является значе­нием итератора «конец контейнера». Обратите также внимание, что автоинкремент ++ имеет семантику указателя, продвигая итератор к следующей позиции в контей­нере. Разыменование работает аналогично семантике указателей.

• d. insert (d.begin (), v.begin(), v.end());

Функция-член insert () помещает диапазон значений итератора от v. begin () до v. end () (исключая само v. end () ) начиная с позиции d. beg in ().

• for (p = d.begin(); p i= d.end(); p++)

cout << *p << ' \t ' ; //печать: 6 6 6 6 6 6 8 7 6 5

Вследствие вставки пяти новых элементов со значением 6 в начало двусторонней очереди d, теперь цикл прохода для d выведет 10 элементов, что и показано в ком­ментарии.

В следующей таблице последовательные классы обозначены как SEQ. Имейте в виду, что приведенные ниже возможности дополняют уже описанный интерфейс любого CAN.

Соседние файлы в папке Тельминов (мб)