Добавил:
Upload Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
Шилдт c++_базовый_курс издание 3.pdf
Скачиваний:
3062
Добавлен:
27.03.2016
Размер:
9.82 Mб
Скачать

Глава 16: Шаблоны

Шаблон — это одно из самых сложных и мощных средств в C++. Он не вошел в исходную спецификацию C++, и лишь несколько лет назад стал неотъемлемой частью программирования на C++. Шаблоны позволяют достичь одну из самых трудных целей в программировании — создать многократно используемый код.

Используя шаблоны, можно создавать обобщенные функции и классы. В обобщенной функции (или классе) обрабатываемый ею (им) тип данных задается как параметр. Таким образом, одну функцию или класс можно использовать для разных типов данных, не предоставляя явным образом конкретные версии для каждого типа данных. Рассмотрению обобщенных функций и обобщенных классов посвящена данная глава.

Обобщенные функции

Обобщенная функция это функция, перегружающая сама себя.

Обобщенная функция определяет общий набор операций, которые предназначены для применения к данным различных типов. Тип данных, обрабатываемых функцией, передается ей как параметр. Используя обобщенную функцию, к широкому диапазону данных можно применить единую общую процедуру. Возможно, вам известно, что многие алгоритмы имеют одинаковую логику для разных типов данных. Например, один и тот же алгоритм сортировки Quicksort применяется и к массиву целых чисел, и к массиву значений с плавающей точкой. Различие здесь состоит только в типе сортируемых данных. Создавая обобщенную функцию, можно определить природу алгоритма независимо от типа данных. После этого компилятор автоматически сгенерирует корректный код для типа данных, который в действительности используется при выполнении этой функции. По сути, создавая обобщенную функцию, вы создаете функцию, которая автоматически перегружает себя саму.

Обобщенная функция создается с помощью ключевого слова template. Обычное значение слова "template" точно отражает цель его применения в C++. Это ключевое слово используется для создания шаблона (или оболочки), который описывает действия, выполняемые функцией. Компилятору же остается "дополнить недостающие детали" в соответствии с заданным значением параметра. Общий формат определения шаблонной функции имеет следующий вид.

template <class Ttype> тип имя_функции (список_параметров)

{

// тело функции

}

Определение обобщенной функции начинается с ключевого слова template.

Здесь элемент Ttype представляет собой "заполнитель" для типа данных, обрабатываемых функцией. Это имя может быть использовано в теле функции. Но оно означает всего лишь заполнитель, вместо которого компилятор автоматически подставит реальный тип данных при создании конкретной версии функции. И хотя для задания

обобщенного типа в template-объявлении по традиции применяется ключевое слово class, можно также использовать ключевое слово typename.

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

//Пример шаблонной функции.

#include <iostream>

using namespace std;

//Определение шаблонной функции.

template <class X> void swapargs(X &a, X &b)

{

X temp;

temp = a;

a = b;

b = temp;

}

int main()

{

int i = 10, j=20;

double x=10.1, y=23.3;

char a='x', b='z';

cout << "Исходные значения i, j: " << i << ' '<< j << ' \ n ';

cout << "Исходные значения x, у: " << x << ' '<< у << '\n';

cout << "Исходные значения a, b: " << a << ' '<< b << ' \n';

swapargs(i, j); // перестановка целых чисел

swapargs(x, у); // перестановка значений с плавающей точкой

swapargs(a, b); // перестановка символов

cout << "После перестановки i, j: " << i << ' '<< j << ' \ n

';

cout << "После перестановки x, у: " << x << ' '<< у << '\n';

cout << "После перестановки a, b: " << a << ' '<< b << ' \n';

return 0;

}

Вот как выглядят результаты выполнения этой программы.

Исходные значения i, j: 10 20

Исходные значения х, у: 10.1 23.3

Исходные значения a, b: х z

После перестановки i, j: 20 10

После перестановки х, у: 23.3 10.1

После перестановки a, b: z х

Итак, рассмотрим внимательно код программы. Строка

template <class Х> void swapargs(X &а, X &b)

сообщает компилятору, во-первых, что создается шаблон, и, во-вторых, что здесь начинается обобщенное определение. Обозначение X представляет собой обобщенный тип, который используется в качестве "заполнителя". За template-заголовком следует объявление функции swapargs(), в котором символ X означает тип данных для значений, которые будут меняться местами. В функции main() демонстрируется вызов функции swapargs() с использованием трех различных типов данных: int, float и char. Поскольку функция swapargs() является обобщенной, компилятор автоматически создает три версии функции swapargs(): одну для обмена целых чисел, вторую для обмена значений с плавающей точкой

и третью для обмена символов.

Здесь необходимо уточнить некоторые важные термины, связанные с шаблонами. Вопервых, обобщенная функция (т.е. функция, объявление которой предваряется template- инструкцией) также называется шаблонной функцией. Оба термина используются в этой книге взаимозаменяемо. Когда компилятор создает конкретную версию этой функции, то говорят, что создается ее специализация (или конкретизация). Специализация также называется порожденной функцией (generated function). Действие порождения функции определяют как ее реализацию (instantiating). Другими словами, порождаемая функция является конкретным экземпляром шаблонной функции.

Поскольку C++ не распознает символ конца строки в качестве признака конца инструкции, template-часть определения обобщенной функции может не находиться в одной строке с именем этой функции. В следующем примере показан еще один (довольно распространенный) способ форматирования функции swapargs().

template <class Х>

void swapargs(X &a, X &b)

{

X temp;

temp = a;

a = b;

b = temp;

}

При использовании этого формата важно понимать, что между template-инструкцией и началом определения обобщенной функции никакие другие инструкции находиться не могут. Например, следующий фрагмент кода не скомпилируется.

// Этот код не скомпилируется.

template <class Х>

int i; // Здесь ошибка!

void swapargs(X &а, X &b)

{

X temp;

temp = a;

a = b;

b = temp;

}

Как отмечено в комментарии, template-спецификация должна стоять непосредственно перед определением функции. Между ними не может находиться ни инструкция объявления переменной, ни какая-либо другая инструкция.

Функция с двумя обобщенными типами

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

#include <iostream>

using namespace std;

template <class type1, class type2>

void myfunc(type1 x, type2 y)

{

cout << x << ' ' << у << '\n';

}

int main()

{

myfunc(10, "Привет");

myfunc(0.23, 10L);

return 0;

}

В этом примере при выполнении функции main(), когда компилятор генерирует конкретные экземпляры функции myfunc(), заполнители типов type1 и type2 заменяются сначала парой типов данных int и char*, а затем парой double и long соответственно.

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

для обработки различных способов, которые использует программа для ее вызова.

Явно заданная перегрузка обобщенной функции

"Вручную" перегруженная версия обобщенной функции называется явной специализацией.

Несмотря на то что обобщенная функция сама перегружается по мере необходимости, это можно делать и явным образом. Формально этот процесс называется явной специализацией. При перегрузке обобщенная функция переопределяется "в пользу" этой конкретной версии. Рассмотрим, например, следующую программу, которая представляет собой переработанную версию первого примера из этой главы.

// Перегрузка шаблонной функции.

#include <iostream>

using namespace std;

template <class X>

void swapargs(X &a, X &b)

{

X temp;

temp = a;

a = b;

b = temp;

cout << "Выполняется шаблонная функция swapargs.\n";

}

// Эта функция переопределяет обобщенную версию функции swapargs() для int-параметров.

void swapargs(int &а, int &b)

{

int temp;

temp = a;

a = b;

b = temp;

cout << "Это int-специализация функции swapargs.\n";

}

int main()

{

int i=10, j =20;

double x=10.1, y=23.3;

char a='x', b='z';

cout << "Исходные значения i, j: " << i << ' '<< j << '\n';

cout << "Исходные значения x, у: " << x << ' '<< у << '\n';

cout << "Исходные значения a, b: " << a << ' '<< b << '\n';

swapargs(i, j); // Вызывается явно перегруженная функция swapargs().

swapargs(x, у); // Вызывается обобщенная функция swapargs().

swapargs(a, b); // Вызывается обобщенная функция swapargs().

cout << "После перестановки i, j: " << i << ' '<< j << '\n';

cout << "После перестановки x, у: " << x << ' '<< у << '\n';

cout << "После перестановки a, b: " << a << ' '<< b << '\n';

return 0;

}

При выполнении эта программа генерирует такие результаты.

Исходные значения i, j: 10 20

Исходные значения х, у: 10.1 23.3

Исходные значения a, b: х z

Это int-специализация функции swapargs.

Выполняется шаблонная функция swapargs.

Выполняется шаблонная функция swapargs.

После перестановки i, j: 20 10

После перестановки х, у: 23.3 10.1

После перестановки а, b: z х

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

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

// Использование нового синтаксиса задания специализации.

template<>

void swapargs<int> (int &a, int &b)

{

int temp;

temp = a;

a = b;

b = temp;

cout << "Это int-специализация функции swapargs.\n";

}

Как видите, в новом синтаксисе для обозначения специализации используется конструкция template<>. Тип данных, для которых создается эта специализация, указывается в угловых скобках после имени функции. Для задания любого типа обобщенной функции используется один и тот же синтаксис. На данный момент ни один из синтаксических способов задания специализации не имеет никаких преимуществ перед другим, но с точки зрения перспективы развития языка, возможно, все же лучше использовать новый стиль.

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

Перегрузка шаблона функции

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

//Объявление перегруженного шаблона функции.

#include <iostream>

using namespace std;

//Первая версия шаблона f().

template <class X>

void f(X a)

{

cout << "Выполняется функция f(X a)\n";

}

// Вторая версия шаблона f().

template <class X, class Y>

void f(X a, Y b)

{

cout << "Выполняется функция f(X a, Y b)\n";

}

int main()

{

f(10); // Вызывается функция f(X).

f(10, 20); // Вызывается функция f(X, Y).

return 0;

}

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

Использование стандартных параметров в шаблонных функциях

В шаблонных функциях можно смешивать стандартные параметры с обобщенными параметрами типа. Эти параметры работают так же, как в любой другой функции. Рассмотрим пример.

//Использование стандартных параметров в шаблонной функции.

#include <iostream>

using namespace std;

//Отображение данных заданное количество раз.

template<class Х>

void repeat(X data, int times)

{

do {

cout << data << "\n";

times--;

}while(times);

}

int main()

{

repeat("Это тест.", 3);

repeat(100, 5);

repeat(99.0/2, 4);

return 0;

}

Вот какие результаты генерирует эта программа.

Это тест.

Это тест.

Это тест.

100

100

100

100

100

49.5

49.5

49.5

49.5

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

функцию repeat() можно использовать для отображения данных любого типа. Параметр times — стандартный, он передается по значению. Смешанное задание обобщенных и необобщенных параметров, как правило, не вызывает никаких проблем и является обычной практикой программирования.

Ограничения при использовании обобщенных функций

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

void outdata(int i)

{

cout << i;

}

void outdata(double d)

{

cout << d * 3.1416;

}

Создание обобщенной функции abs()

Давайте-ка снова обратимся к функции abs(). Вспомните, что в главе 8 стандартные библиотечные функции abs(), labs() и fabs() были сгруппированы в три перегруженные функции с общим именем myabs(). Каждая из перегруженных версий функции myabs() предназначена для возврата абсолютного значения для данных "своего" типа. Несмотря на то что показанную в главе 8 перегрузку функции abs() можно считать шагом вперед по сравнению с использованием трех различных библиотечных функций (с различными именами), все же это не лучший способ создания функции, которая возвращает абсолютное значение заданного аргумента. Поскольку процедура возврата абсолютного значения числа одинакова для всех типов числовых значений, функция abs() может послужить прекрасным поводом для создания шаблонной функции. При наличии обобщенной версии функции abs() компилятор сможет автоматически создавать необходимую ее версию. Программист в этом случае освобождается от написания отдельных версий для каждого типа данных. (Кроме того, исходный код программы не будет загромождаться несколькими "вручную" перегруженными версиями.)

В следующей программе содержится обобщенная версия функции myabs(). Имеет смысл

сравнить ее с перегруженными версиями, приведенными в главе 8. Нетрудно убедиться, что обобщенная версия короче и обладает большей гибкостью.

// Обобщенная версия функции myabs().

#include <iostream>

using namespace std;

template <class X>

X myabs(X val)

{

return val < 0 ? -val : val;

}

int main()

{

cout << myabs(-10) << "\n"; // для типа int

cout << myabs(-10.0) << "\n"; // для типа double

cout << myabs(-10L) << "\n"; // для типа long

cout << myabs(-10.0F) << "\n"; // для типа float

return 0;

}

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

Обобщенные классы

Помимо обобщенных функций, можно также определить обобщенный класс. Для этого создается класс, в котором определяются все используемые им алгоритмы; при этом реальный тип обрабатываемых в нем данных будет задан как параметр при создании объектов этого класса.

Обобщенные классы особенно полезны в случае, когда в них используется логика, которую можно обобщить. Например, алгоритмы, которые поддерживают функционирование очереди целочисленных значений, также подходят и для очереди символов. Механизм, который обеспечивает поддержку связного списка почтовых адресов, также годится для поддержки связного списка, предназначенного для хранения данных о запчастях к автомобилям. После создания обобщенный класс сможет выполнять определенную программистом операцию (например, поддержку очереди или связного списка) для любого типа данных. Компилятор автоматически сгенерирует корректный тип объекта на основе типа, заданного при создании объекта.

Общий формат объявления обобщенного класса имеет следующий вид:

template <class Ttype> class имя_класса {

.

.

.

}

Здесь элемент Ttype представляет собой "заполнитель" для имени типа, который будет задан при реализации класса. При необходимости можно определить несколько обобщенных типов данных, используя список элементов, разделенных запятыми.

Создав обобщенный класс, можно создать его конкретный экземпляр, используя следующий общий формат.

имя_класса <тип> имя_объекта;

Здесь элемент тип означает имя типа данных, которые будут обрабатываться экземпляром обобщенного класса. Функции-члены обобщенного класса автоматически являются обобщенными. Поэтому вам не нужно использовать ключевое слово template для явного определения их таковыми.

В следующей программе класс queue (впервые представленный в главе 11) переделан в обобщенный. Это значит, что его можно использовать для организации очереди объектов любого типа. В данном примере создаются две очереди: для целых чисел и значений с плавающей точкой, но можно использовать данные и любого другого типа.

// Демонстрация использования обобщенного класса очереди.

#include <iostream>

using namespace std;

const int SIZE=100;

// Создание обобщенного класса queue.

template <class QType>

class queue{

QType q[SIZE];

int sloc, rloc;

public:

queue() { sloc = rloc =0; }

void qput(QType i);

QType qget();

};

// Занесение объекта в очередь.

template <class QType>

void queue<QType>::qput(QType i)

{

if(sloc==SIZE) {

cout << "Очередь заполнена.\n";

return;

}

sloc++;

q[sloc] = i;

}

// Извлечение объекта из очереди.

template <class QType>

QType queue<QType>::qget()

{

if(rloc == sloc) {

cout << "Очередь пуста.\n";

return 0;

}

rloc++;

return q[rloc];

}

int main()

{

queue<int> a, b; // Создаем две очереди для целых чисел.

a.qput(10);

a.qput(20);

b.qput(19);

b.qput(1);

cout << a.qget() << " ";

cout << a.qget() << " ";

cout << b.qget() << " ";

cout << b.qget() << "\n";

queue<double> с, d; // Создаем две очереди для double-

значений.

c.qput(10.12);

c.qput(-20.0);

d.qput(19.99);

d.qput(0.986);

cout << с.qget() << " ";

cout << с.qget() << " ";

cout << d.qget()<< " ";

cout << d.qget() << "\n";

return 0;

}

При выполнении этой программы получаем следующие результаты.

10 20 19 1

10.12 -20 19.99 0.986

В этой программе объявление обобщенного класса подобно объявлению обобщенной функции. Тип данных, хранимых в очереди, обобщен в объявлении класса. Он неизвестен до тех пор, пока не будет объявлен объект класса queue, который и определит реальный тип данных. После объявления конкретного экземпляра класса queue компилятор автоматически сгенерирует все функции и переменные, необходимые для обработки реальных данных. В данном примере объявляются два различных типа очереди: две очереди для хранения целых чисел и две очереди для значений типа double. Обратите особое внимание на эти объявления:

queue<int> а, b;

queue<double> с, d;

Заметьте, как указывается нужный тип данных: он заключается в угловые скобки. Изменяя тип данных при создании объектов класса queue, можно изменить тип данных,

хранимых в очереди. Например, используя следующее объявление, можно создать еще одну очередь, которая будет содержать указатели на символы:

queue<char *> chrptrQ;

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

struct addr {

char name[40];

char street[40];

char city[30];

char state[3];

char zip[12];

};

Тогда для того, чтобы с помощью класса queue сгенерировать очередь для хранения объектов типа addr, достаточно использовать такое объявление.

queue<addr> obj;

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

Пример класса с двумя обобщенными типами данных

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

/* Здесь используется два обобщенных типа данных в определении класса.

*/

#include <iostream>

using namespace std;

template <class Type1, class Type2>

class myclass {

Type1 i;

Type2 j;

public:

myclass(Type1 a, Type2 b) { i = a; j = b; }

void show() { cout << i << ' ' << j << '\n'; }

};

int main()

{

myclass<int, double> ob1(10, 0.23);

myclass<char, char *> ob2('x', "Это тест.");

ob1.show(); // отображение int- и double-значений

ob2.show(); // отображение значений типа char и char *

return 0;

}

Эта программа генерирует такие результаты.

10 0.23

X Это тест.

В данной программе объявляется два вида объектов. Объект ob1 использует данные типа int и double, а объект ob2 — символ и указатель на символ. Для этих ситуаций компилятор

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

Создание обобщенного класса безопасного массива

Прежде чем двигаться дальше, рассмотрим еще одно приложение для обобщенного класса. Как было показано в главе 13, можно перегружать оператор "[]", что позволяет создавать собственные реализации массивов, в том числе и "безопасные массивы", которые обеспечивают динамическую проверку нарушения границ. Как вы знаете, в C++ во время выполнения программы возможен выход за границы массива без выдачи сообщения об ошибке. Но если создать класс, который бы содержал массив, и разрешить доступ к этому массиву только через перегруженный оператор индексации ("[]"), то можно перехватить индекс, соответствующий адресу за пределами адресного пространства массива.

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

// Пример создания и использования обобщенного безопасного массива.

#include <iostream>

#include <cstdlib>

using namespace std;

const int SIZE = 10;

template <class AType>

class atype {

AType a[SIZE];

public:

atype() {

register int i;

for(i=0; i<SIZE; i++) a[i] = i;

}

AType &operator[](int i);

};

// Обеспечение контроля границ для класса atype.

template <class АТуре>

АТуре &atype<AType>::operator[](int i)

{

if(i<0 || i> SIZE-1) {

cout << "\n Значение индекса ";

cout << i << " за пределами границ массива.\n";

}

return a [i];

}

int main()

{

atype<int> intob; // массив int-значений

atype<double> doubleob; // массив double-значений

int i;

cout << "Массив int-значений: ";

for(i=0; i<SIZE; i++) intob[i] = i;

for(i=0; i<SIZE; i++) cout << intob[i] << " ";

cout << '\n';

cout << "Массив double-значений: ";

for(i=0; i<SIZE; i++) doubleob[i] = (double) i/3;

for(i=0; i<SIZE; i++) cout << doubleob[i] << " ";

cout << '\n';

intob[12] = 100; // ошибка времени выполнения!

return 0;

}

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

Использование в обобщенных классах аргументов, не являющихся типами

В template-спецификации для обобщенного класса можно также задавать аргументы, не являющиеся типами. Это значит, что в шаблонной спецификации можно указывать то, что обычно принимается в качестве стандартного аргумента, например, аргумент типа int или аргумент-указатель. Синтаксис (он практически такой же, как при задании обычных параметров функции) включает определение типа и имени аргумента. Вот, например, как можно по-другому реализовать класс безопасного массива, представленного в предыдущем разделе.

// Использование в шаблоне аргументов, которые не являются типами.

#include <iostream>

#include <cstdlib>

using namespace std;

// Здесь элемент int size - это аргумент, не являющийся типом.

template <class AType, int size>

class atype {

AType a[size]; // В аргументе size передается длина массива.

public:

atype() {

register int i;

for(i=0; i<size; i++) a[i] = i;

}

AType &operator[](int i);

};

// Обеспечение контроля границ для класса atype.

template <class АТуре, int size>

AType &atype<AType, size>::operator[](int i)

{

if(i<0 || i> size-1) {

cout << "\n Значение индекса ";

cout << i << " за пределами границ массива.\n";

exit(1);

}

return a[i];

}

int main()

{

atype<int, 10> intob; // 10-элементный массив целых чисел

atype<double, 15> doubleob; // 15-элементный массив double-

значений

int i;

cout << "Массив целых чисел: ";

for(i=0; i<10; i++) intob[i] = i;

for(i=0; i<10; i++) cout << intob[i] << " ";

cout << '\n';

cout << "Массив double-значений: ";

for(i=0; i<15; i++) doubleob[i] = (double) i/3;

for(i=0; i<15; i++) cout << doubleob[i] << " ";

cout << '\n';

intob[12] = 100; // ошибка времени выполнения!

return 0;

}

Рассмотрим внимательно template-спецификацию для класса atype. Обратите внимание на то, что аргумент size объявлен с указанием типа int. Этот параметр затем используется в теле класса atype для объявления размера массива a. Несмотря на то что в исходном коде программы член size имеет вид "переменной", его значение известно уже во время компиляции. Поэтому его можно успешно использовать для установки размера массива. Кроме того, значение "переменной" size используется для контроля выхода за границы массива в операторной функции operator[](). Обратите также внимание на то, как в функции main() создается массив целых чисел и массив значений с плавающей точкой. При этом размер каждого из них определяется вторым параметром template-спецификации.

На тип параметров, которые не представляют типы, налагаются ограничения. В этом случае разрешено использовать только целочисленные типы, указатели и ссылки. Другие типы (например, float) не допускаются. Аргументы, которые передаются параметру, не являющемуся типом, должны содержать либо целочисленную константу, либо указатель или ссылку на глобальную функцию или объект. Таким образом, эти "нетиповые" параметры следует рассматривать как константы, поскольку их значения не могут быть изменены. Например, в теле функции operator[]() следующая инструкция недопустима:

size = 10; // ошибка

Поскольку параметры-"нетипы" обрабатываются как константы, их можно использовать для установки размерности массива, что существенно облегчает жизнь программисту.

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

Шаблонный класс queue, представленный выше в этой главе, также выиграл бы от применения к нему "нетипового" параметра, задающего размер очереди. В качестве упражнения попробуйте усовершенствовать класс queue самостоятельно.

Использование в шаблонных классах аргументов по умолчанию

Шаблонный класс может по умолчанию определять аргумент, соответствующий обобщенному типу. Например, в результате такой template-спецификации

template <class X=int>

class myclass { //...

};

будет использован тип int, если при создании объекта класса myclass отсутствует задание какого-то бы то ни было типа.

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

Рассмотрим еще одну версию класса безопасного массива, в котором используются аргументы по умолчанию как для типа данных, так и для размера массива.

// Демонстрация использования шаблонных аргументов по умолчанию.

#include <iostream>

#include <cstdlib>

using namespace std;

/* Здесь параметр AType по умолчанию принимает тип int. а параметр size по умолчанию устанавливается равным 10.

*/

template <class AType=int, int size=10>

class atype{

AType a[size]; // Через параметр size передается размер массива.

public:

atype() {

register int i;

for(i=0; i<size; i++) a[i] = i;

}

AType &operator[](int i);

};

// Обеспечение контроля границ для класса atype.

template <class АТуре, int size>

AType &atype<AType, size>::operator[](int i)

{

if( i<0 || i> size-1) {

cout << "\n Значение индекса ";

cout << i << " за пределами границ массива.\n";

exit(1);

}

return a[i];

}

int main()

{

atype<int, 100> intarray; /* 100-элементный массив целых чисел

*/

atype<double> doublearray; /* 10-элементный массив double-

значений (размер массива установлен по умолчанию) */

atype<> defarray; /* 10-элементный массив int-значений (размер и тип int установлены по умолчанию) */

int i;

cout << "Массив целых чисел: ";

for(i=0; i<100; i++ ) intarray[i] = i;

for(i=0; i<100; i++) cout << intarray[i] << " ";

cout << '\n';

cout << "Массив double-значений: ";

for(i=0; i<10; i++) doublearray[i] = (double) i/3;

for(i=0; i<10; i++) cout << doublearray[i] << " ";

cout << '\n';

cout << "Массив по умолчанию: ";

for(i=0; i<10; i++) defarray[i] = i;

for(i=0; i<10; i++) cout << defarray[i] << " ";

cout << '\n';

return 0;

}

Обратите особое внимание на эту строку:

template <class AType=int, int size=10>

class atype {

Здесь параметр AType по умолчанию заменяется типом int, а параметр size по умолчанию устанавливается равным числу 10. Как показано в этой программе, объекты класса atype можно создать тремя способами:

путем явного задания как типа, так и размера массива;

задав явно лишь тип массива, при этом его размер по умолчанию устанавливается равным 10 элементам;

вообще без задания типа и размера массива, при этом он по умолчанию будет хранить элементы типа int, а его размер по умолчанию устанавливается равным 10.

Использование аргументов по умолчанию (особенно типов) делает шаблонные классы еще более гибкими. Тип, используемый по умолчанию, можно предусмотреть для наиболее употребительного типа данных, позволяя при этом пользователю ваших классов задавать при необходимости нужный тип данных.

Явно задаваемые специализации классов

Подобно шаблонным функциям можно создавать и специализации обобщенных классов. Для этого используется конструкция template<>, которая работает по аналогии с явно задаваемыми специализациями функций. Рассмотрим пример.

// Демонстрация специализации класса.

#include <iostream>

using namespace std;

template <class T>

class myclass {

T x;

public:

myclass(T a) {

cout << "В теле обобщенного класса myclass.\n";

x = a;

}

T getx() { return x; }

};

// Явная специализация для типа int.

template <>

class myclass<int> {

int x;

public:

myclass(int a) {

cout << "В теле специализации myclass<int>.\n";

x = a * a;

}

int getx() { return x; }

};

int main()

{

myclass<double> d(10.1);

cout << "double: " << d.getx() << "\n\n";

myclass<int> i(5);

cout << "int: " << i.getx() << "\n";

return 0;

}

При выполнении данная программа отображает такие результаты.

Втеле обобщенного класса myclass.

double: 10.1

Втеле специализации myclass<int>.

int: 25

В этой программе обратите особое внимание на следующую строку.

template <>

class myclass<int> {

Она уведомляет компилятор о том, что создается явная int-специализация класса myclass. Тот же синтаксис используется и для любого другого типа специализации класса.

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