Параметрический полиморфизм

Язык C++ / Параметрический полиморфизм
Шаблоны функций

Шаблоны, которые называют иногда родовыми или параметризованными типами, позволяют создавать (конструировать) семейства родственных функций и классов.

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

В определении шаблона семейства функций используется служебное слово template. Для параметризации используется список формальных параметров шаблона, который заключает­ся в угловые скобки <>. Каждый формальный параметр шаблона обозначается служебным словом class, за которым следует имя параметра (идентификатор). Пример определения шаблона функций, вычисляющих абсолютные значения числовых величин разных типов:

template <class type>
type abs (type x) { if(x>0) return(x); else return(-x);}

Шаблон семейства функций состоит из двух частей – заголовка шаблона:

template <СписокПараметровШаблона>

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

В качестве другого примера рассмотрим шаблон семейства функций для обмена значений двух передаваемых им параметров:

template <class T>
void swap (T* x, T* y) {
  T z = *x;
  *x = *y;
  *y = z;
}

Здесь параметр T шаблона функций используется не только в заголовке для спецификации формальных параметров, но и в теле определения функции, где он задает тип вспомогатель­ной переменной z.

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

cout << abs(-10.3);

то на основе приведенного ранее шаблона компилятор сформирует следующее определение функции:

double abs (double x) { if(x>0) return(x); else return(-x);}

Далее будет организовано выполнение именно этой функции и в точку вызова в качестве результата вернется вещественное значение 10.3.

Если в программе присутствует приведенный ранее шаблон семейства функций swap() и имеет место, скажем, следующий вызов этой функции:

long int k = 4, d = 8;
swap (&k, &d);

то компилятор сформирует определение функции:

void swap (long int *x, long int *y) {
  long int z = *x;
  *x = *y;
  *y = z;
}

Затем будет выполнено обращение именно к этой функции и значения переменных k, d  поменяются местами. Если в той же программе присутствуют операторы:

double a = 2.44, b = 66.3;
swap (&a, &b);

то сформируется и выполнится функция

void swap (double *x, double *y) {
  double z = *x;
  *x = *y;
  *y = z;
}

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

#include <iostream>
using namespace std;
//Функция возвращает ссылку на элемент с максимальным значением
template <class type>
type& rmax (int n, type *d) {
  int im = 0;
  for (int i = 1; i < n; i++)
    if(d[im] < d[i])
      im = i;
  return d[im];
}
int main() {
//Массив целых чисел
  int n = 4;
  int x[] = { 10, 20, 30, 14};
  cout << "rmax(n,x) = " << rmax (n,x) << endl; // rmax(n,x) = 30
  for (int i = 0; i < n; i++)
  cout << "   x[" << i << "] =" << x[i];
  rmax(n,x) = 0;                    // обнулить максимальный элемент
  cout << endl;
  for (int i = 0; i < n; i++)
  cout << "   x[" << i << "] =" << x[i];
//Массив вещественных чисел
  int m = 3;
  float arx[] = { 10.3, 20.4, 10.5};
  cout << endl <<"rmax(m,arx) = " << rmax (m,arx) << endl;
  for (int i = 0; i < m; i++)
  cout << "   arx[" << i << "] =" << arx[i];
  rmax(m, arx) = 0;                // обнулить максимальный элемент
  cout << endl;
  for (int i = 0; i < m; i++)
  cout << "   arx[" << i << "] =" << arx[i];
  cin.get();
  return 0;
}

Результат выполнения Шаблон класса
В примере используются два разных обращения к функции rmax(). В одном случае параметр – целочисленный массив и возвращаемое значение – ссылка типа int. Во втором случае фактический параметр – имя массива типа float и возвращаемое значение имеет тип ссылки на float.

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

Параметры шаблонов

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

  • Имена параметров шаблона должны быть уникальными во всем определении шаблона.
  •  Список параметров шаблона функции не может быть пустым, так как при этом теряется возможность параметризации, и шаблон функций становится обычным определением конкретной функции.
  •  В списке параметров шаблона функций может быть несколько параметров. Каждый из них должен начинаться со служебного слова class. Например, допустим такой заголо-вок шаблона:
    template <class type1, class type2>

    Соответственно, неверен заголовок:

    template <class type1, type2>
  • Недопустимо использовать в заголовке шаблона параметры с одинаковыми именами, то есть ошибочен такой заголовок:
    template <class type, class type>
  • Имя параметра шаблона (в примерах – type1, type2) имеет в определяемой шаблоном функции все права имени типа, то есть с его помощью могут специализироваться формальные параметры, определяться тип возвращаемого функцией значения и типы любых объектов, локализованных в теле функции. Имя параметра шаблона видно во всем определении и скрывает другие использования того же идентификатора в области, глобальной по отношению к данному шаблону функций. Если внутри тела определяемой функции необходим доступ к внешним объектам с тем же именем, нужно применять операцию изменения области видимости (оператор разрешения контекста).

Пример

#include <iostream>
using namespace std;
int N = 0; // глобальная
template <class N>
N maxim (N x, N y) {
  N a = x;
  cout << endl <<"N = " << ++::N;
  if (a < y) a = y;
  return a;
}
int main()
{
  int a = 12, b = 42;
  cout << endl << maxim (a,b);
  float z = 66.3, f = 222.4;
  cout << endl << maxim (z,f);
  cin.get();
  return 0;
}

Результат выполнения
Параметрический шаблон

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

template <class A, class B, class C>
B func (A n, C m) {B value; ... }

В данном случае остался неиспользованным параметр шаблона с именем B. Его применение в качестве типа возвращаемого функцией значения и для определения объекта value в теле функции недостаточно.
Определяемая с помощью шаблона функция может иметь любое количество не параметризованных формальных параметров. Может быть не параметризовано и возвращаемое функцией значение.

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

#include <iostream>
using namespace std;
template <class D>
long int count0 (int, D *); //Прототип шаблона
int main() {
  int A[] = {1,0,6,0,4,10};
  int n = sizeof(A)/sizeof(A[0]);
  cout << "count0(n,A) = " << count0(n,A) << endl;
  double X[] = {10.0, 0.0, 3.3, 0.0, 2.1, 0.0};
  n = sizeof(X)/sizeof X[0];
  cout << "count0(n,X) = " << count0(n,X) << endl;
  cin.get();
  return 0;
}
template <class T>
long int count0 (int size, T* array) {
  long int k = 0;
  for (int i = 0; i < size; i++)
  if (int(array[i]) == 0) k++;
  return k;
}

Результат выполнения
Шаблон функции

В шаблоне функций count0 параметр T используется только в спецификации одного формального параметра array. Параметр size и возвращаемое функцией значение имеют явно заданные не параметризованные типы.
Как и при работе с обычными функциями, для шаблонов функций существуют определения и описания. В качестве описания шаблона функций используется прототип шаблона:

template < CписокПараметровШаблона > ЗаголовокФункции;

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

template < class E >
void swap (E,E);

недопустимо использовать такое обращение к функции:

int n = 4; double d = 4.3;
swap (n,d); // Ошибка в типах параметров

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

swap (double (n) , d); // Правильные типы параметров

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

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

Аналогично шаблонам функций определяется шаблон семейства классов:
template <CписокПараметровШаблона> ОпределениеКласса
Шаблон семейства классов определяет способ построения отдельных классов подобно тому, как класс определяет правила построения и формат отдельных объектов. В определении класса, входящего в шаблон, особую роль играет имя класса. Оно является не именем отдельного класса, а параметризованным именем семейства классов.
Как и для шаблонов функций, определение шаблона класса может быть только глобальным.

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

ИмяПараметризованногоКласса <ФактическиеПараметрыШаблона>
ИмяОбъекта (ПараметрыКонструктора);

В нашем случае определить вектор, имеющий восемь вещественных координат типа double, можно следующим образом:

Vector <double> z (8);

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

#include <iostream>
using namespace std;
template <class T> // T -  параметр шаблона
class Vector {
  T *data;              // Указатель на одномерный массив
  int size;              // Количество элементов в массиве
public:
  Vector (int);    // Конструктор
  ~Vector () { delete [] data; }         // Деструктор
  // Расширение действия (перегрузка) операции []:
  T& operator [] (int i) { return data[i];}
};
// Внешнее определение конструктора класса:
template <class T>
Vector <T>:: Vector (int n=1) {
  data = new T [n];
  size = n;
}
int main() {
  Vector <int> X(5); //Создаем вектор типа int
  Vector <char> C(5);//Создаем вектор типа char
  for (int i = 0; i < 5; i++)
    { X [i] = i; C[i] = 'A' + i;}
  for (int i = 0; i < 5; i++)
    cout << " " << X[i];
  cout << endl;
  for (int i = 0; i < 5; i++)
    cout << " " << C[i];
  cin.get();
  return 0;
}

Результат выполнения
Шаблон"

В программе шаблон семейства классов с общим именем Vector используется для форми­рования двух классов с массивами целого и символьного типов. В соответствии с требовани­ем синтаксиса имя параметризованного класса, определенное в шаблоне (в примере Vector), используется в программе только с последующим конкретным фактическим пара­метром (аргументом), заключенным в угловые скобки. Параметром может быть имя базового или определенного пользователем типа (класса). В данном примере использованы стан­дартные типы int и char. Использовать имя Vector без указания фактического параметра шаблона нельзя – никакое умалчиваемое значение при этом не предусматривается.

В списке параметров шаблона могут присутствовать формальные параметры, не определяющие тип, точнее – это параметры, для которых тип фиксирован:

#include <iostream>
using namespace std;
template <class T, int n=1> // n -  параметр фиксированного типа
class Vector {
  T *data;
  int size;
public:
  Vector (void)  {
    data = new T [n];
    size = n;
  }
  ~Vector () { delete [] data; }
  T& operator [] (int i) { return data[i];}
};
int main() {
  Vector <int, 5> X;
  Vector <char, 5> C;
  for (int i = 0; i < 5; i++)
    { X [i] = i; C[i] = 'A' + i;}
  for (int i = 0; i < 5; i++)
    cout << " " << X[i];
  cout << endl;
  for (int i = 0; i < 5; i++)
    cout << " " << C[i];
  cin.get();
  return 0;
}

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

Использовать имя шаблона класса без указания фактического параметра шаблона нельзя, но если программист считает, что такая конструкция класса затрудняет читабельность кода, то можно воспользоваться оператором typedef, например, следующим образом

typedef Vector <int> iVector;

и в дальнейшем работать с типом iVector.

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

Назад


Назад: Язык C++

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *