Лекция 15. Шаблоны

1. Введение

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

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

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

Объявление шаблона имеет следующий синтаксис:
template <список параметров шаблона> объявление

Здесь угловые скобки являются элементом синтаксиса.

Объявление в объявлении шаблона должно описывать функцию или класс. Объявление шаблона может быть только глобальным.

Список параметров шаблона не может быть пуст. Шаблон с пустым списком параметров может быть просто описан как обычный класс либо функции, и потому не имеет смысла.

Процесс генерации объявления класса по шаблону класса и параметру шаблона часто называют инстанцированием шаблона. Аналогично, функция генерируется («инстанцируется») из шаблона функции и параметра шаблона. Версия шаблона для конкретного параметра называется специализацией. Сгенерированные классы являются совершенно обычными классами, которые подчиняются всем стандартным правилам для классов (например, у них могут быть дружественные функции). Аналогично, сгенерированные функции являются обычными функциями, которые подчиняются всем стандартным правилам для функций.

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

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

Шаблон функции задаёт способ построения отдельных функций. Синтаксис шаблона функции:
template <список параметров шаблона функции> тип_функции имя_функции (список формальных параметров) { ... }

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

template <typename TYPE> TYPE abs(TYPE x) { return x >= 0 ? x : -x; } void main() { int i = 567; double d = -123.45; printf("%7d\n", abs<int>(i)); printf("%7.2lf\n", abs<double>(d)); printf("%7d\n", abs(i)); printf("%7.2lf\n", abs(d)); } // Параметр шаблона определяется по типу фактического параметра функции // Параметр шаблона определяется по типу фактического параметра функции

Тип, используемый в качестве параметра шаблона, должен обеспечивать интерфейс, ожидаемый шаблоном. Если мы попытаемся подставить в шаблон функции abs имя типа, для которого не определены операции >= и , это приведет к ошибке.

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

template <typename TYPE> TYPE max(TYPE a, TYPE b) { return a > b ? a : b; } void main() { int i = 567; float f = 7.5; double d = -123.45; printf("%7d\n", max(i, 0)); printf("%7.2lf\n", max(d, 0)); printf("%7.2lf\n", max(d, 0.0)); printf("%7.2lf\n", max(d, f)); printf("%7.2lf\n", max(d, (double)f)); } // Всё правильно // Ошибка – 0 является константой целого типа // Всё правильно // Ошибка – f и d имеют разные типы // Всё правильно

Шаблонная функция может совместно использоваться с другими (обычными) функциями и с другими шаблонными функциями.
template <typename TYPE> TYPE max(TYPE a, TYPE b) { return a > b ? a : b; } template <typename TYPE> TYPE max(TYPE a, TYPE b, TYPE c) { TYPE d; d = a; if (b > d) d = b; if (c > d) d = c; return d; }

3. Шаблоны класса

Шаблон класса задаёт способ построения отдельных классов. Синтаксис шаблона класса:
template <список параметров шаблона класса> class имя_шаблона_класса { ... };

Здесь угловые скобки являются элементом синтаксиса.

Имя шаблона класса должно быть уникально в программе. Список параметров шаблона класса может содержать ти́повые параметры, а также обычные параметры.

Класс, генерируемый по шаблону, является совершенно нормальным классом. Поэтому использование шаблона не подразумевает каких-либо дополнительных механизмов времени выполнения сверх тех, что использовались бы для эквивалентного написанного «вручную» класса. С другой стороны, использование шаблонов не подразумевает обязательного уменьшения объёма сгенерированного кода.

После объявления шаблона можно объявить некоторый класс по этому шаблону:
имя_шаблона_класса <список фактических параметров шаблона>

Это объявление задаёт так называемое имя класса по шаблону. Каждый фактический параметр шаблона может быть именем пользовательского или стандартного типа (ти́повый параметр), шаблоном или выражением, а именно: константным выражением или адресом глобального объекта или функции или статического члена класса.
template <class TYPE> class Vector { private: int size; TYPE *v; public: Vector(int n = 0); ~Vector(); ... }; template <class TYPE, int NMAX = 100> class Vector { private: int size; TYPE v[NMAX]; public: Vector(); ~Vector(); ... }; template <class CLASS, void (*err_fun)()> class List { ... };

Каждый генерируемый по шаблону класс получает копию каждого статического члена шаблона класса. Определение статических членов класса, которое пишется вне определения класса, также должно быть шаблоном.
template <class T> class X { private: static T common; static int count; ... }; template <class T> T X<T>::common; template <class T> int X<T>::count = 0;

3.1. Шаблоны функций-членов класса

Функция-член шаблонного класса неявно оказывается шаблонной функцией, параметрами шаблона которой являются параметры шаблона класса.
template <class TYPE, int NMAX> class Vector { private: int size; TYPE v[NMAX]; public: Vector(); Vector(TYPE x); };

Данное объявление класса объявляет два шаблона функций. Соответственно их описание должно выглядеть следующим образом:
template <class TYPE, int NMAX> Vector<TYPE, NMAX>::Vector() { size = NMAX; for (int i = 0; i < size; i++) v[i] = 0; } template <class TYPE, int NMAX> Vector<TYPE, NMAX>::Vector(TYPE x) { size = NMAX; for (int i = 0; i < size; i++) v[i] = x; }

3.2. Дружественные функции

Функция-друг шаблона не становится неявно шаблонной функцией. Однако если параметры функции или возвращаемое значение принадлежат шаблонному классу, такая функция становится шаблонной.
template <class T> class X { public: friend void f1(); friend X<T>* f2(); friend int f3(X<T> *p); friend void f4(X *p); // Ошибка, т.к. не существует класса X }; void f1() { ... } template <class T> X<T>* f2 () { ... } template <class T> int f3(X<T> *p) { ... }

4. Инстанцирование

Процесс генерации объявления класса по шаблону класса и параметру шаблона часто называют инстанцированием шаблона. Аналогично, функция генерируется («инстанцируется») из шаблона функции и параметра шаблона. Версия шаблона для конкретного параметра называется специализацией.

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

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

5. Параметры шаблонов по умолчанию

Шаблоны классов позволяют задавать значения по умолчанию для параметров шаблона.
template <class TYPE = int, int NMAX = 10> class Vector { private: int size; TYPE v[NMAX]; ... }; Vector<> v;

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

6. Специализация

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

Например, можно определить отдельную функцию abs для параметра шаблона типа char, задав явное описание функции для этого типа.
template <> char abs<char>(char x) { return x; }

Рассмотрим другой пример. Мы написали шаблон вектора, но для параметров-указателей требуются другие алгоритмы обработки. В этом случае можно также определить отдельную специализацию. Для начала определим специализацию вектора для указателей на void.
template <> class Vector<void*> { ... };

Теперь эту специализацию можно использовать в качестве общей реализации для всех векторов указателей.

Vector<void*> является полной специализацией. То есть, здесь нет параметра шаблона, который следовало бы задавать или который бы выводился при инстанцировании. Данная специализация используется с векторами, объявленными следующим образом:
Vector<void*> vpv;

Для определения специализации, которая используется для любого вектора указателей и только для векторов указателей, нам потребуется частичная специализация.
template <class T> class Vector<T*> { ... };

Образец специализации <T*> после имени означает, что эта специализация должна использоваться для каждого типа указателя, то есть, это определение используется для каждого Vector, параметр шаблона которого можно выразить в виде T*.

Vector<Shape*> spv; Vector<int**> ippv; // <T*> – это <Shape*>, поэтому T – это Shape // <T*> – это <int**>, поэтому T – это int*

Общий шаблон должен быть объявлен прежде любой специализации.

Все специализации шаблона должны быть объявлены в том же самом пространстве имён, что и сам шаблон.

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

template <class T> class Vector; template <class T> class Vector<T*>; template <> class Vector<void*>; // Общий шаблон // Специализация для любого указателя // Специализация только для void*

Более специализированной версии будет отдано предпочтение в объявлениях объектов, указателей и т.д., а также при разрешении перегрузки.

7. Использование параметров шаблона для выбора алгоритма

При разработке общих алгоритмов, например, сортировки или сравнения сложных объектов, таких как строка или вектор, критерий сравнения элементов объекта должен указывается при выполнении конкретной операции. Например, для сравнения строк, содержащих шведские буквы, используются две различные сортирующие последовательности (способы нумерации символов). Поэтому любое общее решение нуждается в том, чтобы алгоритмы были выражены в общих терминах, которые могут быть определены не только для какого-то конкретного типа.
template <class T> class String { ... }; template <class T, class C> int Compare(const String<T>& s1, const String<T>& s2) { for (int i = 0; i < s1.Lenght() && i < s2.Lenght(); i++) if (!C::eq(s1[i], s2[i])) return C::lt(s1[i], s2[i]) ? -1 : 1; return s1.Lenght() - s2.Lenght(); } template <class T> class UsualCompare { public: static int eq(T x, T y) { return x == y; } static int lt(T x, T y) { return x < y; } }; template <class T> class CompareNoCase { public: static int eq(T x, T y) { return 0; } static int lt(T x, T y) { return 0; } }; template <> class CompareNoCase<char> { public: static int eq(char x, char y) { if ('A' <= x && x <= 'Z') x += 'a' - 'A'; if ('A' <= y && y <= 'Z') y += 'a' - 'A'; return x == y; } static int lt(char x, char y) { if ('A' <= x && x <= 'Z') x += 'a' - 'A'; if ('A' <= y && y <= 'Z') y += 'a' - 'A'; return x < y; } }; String<char> s1, s2; cin >> s1 >> s2; cout << Compare<char, UsualCompare<char>>(s1, s2) << endl; cout << Compare<char, CompareNoCase<char>>(s1, s2) << endl;

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

8. Организация исходного кода

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

  1. включать определения шаблонов до их использования в единице компиляции;
  2. включать только объявления шаблонов до их использования в единице компиляции, и компилировать их определения отдельно.

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

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

8. Пример

В данном примере разрабатывается шаблон для класса Vector. Параметрами шаблона являются тип элементов вектора и количество элементов вектора.

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

Файл Vector.h

template <class TYPE, int NMAX> class Vector { private: int size; TYPE v[NMAX]; public: Vector(); Vector(TYPE x); ~Vector() { } int GetSize() const { return size; } TYPE& operator [] (int n); const TYPE& operator [] (int n) const; int operator == (const Vector& vector2); Vector operator + (const Vector& vector2); Vector operator += (const Vector& vector2); Vector operator - (const Vector& vector2); Vector operator -= (const Vector& vector2); TYPE operator * (const Vector& vector2); }; template <class TYPE, int NMAX> ostream& operator<< (ostream &f, const Vector<TYPE, NMAX> &v); template <class TYPE, int NMAX> istream& operator>> (istream &f, Vector<TYPE, NMAX> &v);

Файл Vector.cpp

#include <iostream> using namespace std; #include "Vector.h" template <class TYPE, int NMAX> Vector<TYPE, NMAX>::Vector() { size = NMAX; for (int i = 0; i < size; i++) v[i] = 0; } template <class TYPE, int NMAX> Vector<TYPE, NMAX>::Vector(TYPE x) { size = NMAX; for (int i = 0; i < size; i++) v[i] = x; } template <class TYPE, int NMAX> inline TYPE& Vector<TYPE, NMAX>::operator [] (int n) { if (n < 0) n = 0; else if (n >= size) n = size - 1; return v[n]; } template <class TYPE, int NMAX> inline const TYPE& Vector<TYPE, NMAX>::operator [] (int n) const { if (n < 0) n = 0; else if (n >= size) n = size - 1; return v[n]; } template <class TYPE, int NMAX> int Vector<TYPE, NMAX>::operator == (const Vector& vector2) { for (int i = 0; i < size; i++) if (v[i] != vector2.v[i]) return 0; return 1; } template <class TYPE, int NMAX> Vector<TYPE, NMAX> Vector<TYPE, NMAX>::operator + (const Vector& vector2) { Vector res; for (int i = 0; i < size; i++) res.v[i] = v[i] + vector2.v[i]; return res; } template <class TYPE, int NMAX> Vector<TYPE, NMAX> Vector<TYPE, NMAX>::operator += (const Vector& vector2) { for (int i = 0; i < size; i++) v[i] += vector2.v[i]; return *this; } template <class TYPE, int NMAX> Vector<TYPE, NMAX> Vector<TYPE, NMAX>::operator - (const Vector& vector2) { Vector res; for (int i = 0; i < size; i++) res.v[i] = v[i] - vector2.v[i]; return res; } template <class TYPE, int NMAX> Vector<TYPE, NMAX> Vector<TYPE, NMAX>::operator -= (const Vector& vector2) { for (int i = 0; i < size; i++) v[i] -= vector2.v[i]; return *this; } template <class TYPE, int NMAX> TYPE Vector<TYPE, NMAX>::operator * (const Vector& vector2) { TYPE res = 0; for (int i = 0; i < size; i++) res += v[i] * vector2.v[i]; return res; } template <class TYPE, int NMAX> ostream& operator<< (ostream &f, const Vector<TYPE, NMAX> &v) { streamsize s = f.width(); for (int i = 0; i < v.GetSize(); i++) f << setw(0) << " " << setw(s) << v[i]; f << endl; return f; } template <class TYPE, int NMAX> istream& operator>> (istream &f, Vector<TYPE, NMAX> &v) { for (int i = 0; i < v.GetSize(); i++) f >> v[i]; return f; }

Файл Complex.h**

class Complex { private: double r, m; public: Complex(double nr = 0, double nm = 0) : r(nr), m(nm) {} Complex operator ++(); Complex operator ++(int); Complex operator --(); Complex operator --(int); Complex operator +(const Complex& c) const; Complex operator -(const Complex& c) const; Complex operator +=(const Complex& c); Complex operator -=(const Complex& c); bool operator ==(const Complex& c) const; bool operator !=(const Complex& c) const; friend ostream& operator<< (ostream &f, const Complex &c); friend istream& operator>> (istream &f, Complex &c); };

Файл main.cpp

#include <iostream> #include <iomanip> #include "Vector.cpp" // См. «Организация исходного кода»! #include "Complex.h" using namespace std; void main() { // Вещественный вектор Vector<double, 10> v1, v2, v3; const Vector<double, 10> v5; for (int i = 0; i < v1.GetSize(); i++) { v1[i] = i; v2[i] = i + 50; } cout << fixed << setprecision(1); cout << "v1: " << setw(5) << v1; cout << "v2: " << setw(5) << v2; v3 = v1 + v2; cout << "v1 + v2: " << setw(5) << v3; v3 = v1 - v2; cout << "v1 - v2: " << setw(5) << v3; double r = v1 * v2; cout << "r = " << r << endl; cout << "v5: " << setw(5) << v5; // Целочисленная матрица Vector<Vector<int, 3>, 3> m1, m2(7), m3; cout << "m1" << endl << setw(2) << m1; cout << "m2" << endl << setw(2) << m2; for (int i = 0; i < m1.GetSize(); i++) for (int j = 0; j < m1[i].GetSize(); j++) { m1[i][j] = (i + 1) * (j + 1); m2[i][j] = (i + 1) * j + 5; } cout << "m1" << endl << setw(2) << m1; cout << "m2" << endl << setw(2) << m2; m3 = m1 + m2; cout << "m3" << endl << setw(2) << m3; m3 = m1 - m2; cout << "m3" << endl << m3; Vector<int, 3> v = m1 * m2; cout << "v = " << v << endl; // Вектор комплексных чисел Vector<Complex, 3> c1, c2, c3; for (int i = 0; i < c1.GetSize(); i++) c1[i] = Complex(i, -i); for (int i = 0; i < c2.GetSize(); i++) c2[i] = Complex(i * 2, -i * 3); cout << "c1 = " << setw(4) << c1; cout << "c2 = " << setw(4) << c2; c3 = c1 + c2; cout << "c3 = " << setw(4) << c3; c3 = c1 - c2; cout << "c3 = " << setw(4) << c3; Complex x = c1 * c2; // Для класса Complex операция * не определена. Однако, пока эта операция не используется, // соответствующая функция для класса Vector<Complex, 3> по шаблону не генерируется и проблем не возникает }