Универсальные классы (Руководство по программированию на C#)
Универсальные классы инкапсулируют операции, которые не относятся к конкретному типу данных. Универсальные классы чаще всего используются для работы с коллекциями, такими как связанные списки, хэш-таблицы, стеки, очереди, деревья и т. д. Такие операции, как добавление и удаление элементов коллекции, по существу выполняются одинаково, независимо от типа хранимых данных.
В большинстве случаев для этого используются классы коллекций. Рекомендуется выбирать те из них, которые представлены в библиотеке классов платформы .NET. Дополнительные сведения об использовании этих классов см. в разделе Универсальные коллекции в .NET.
Как правило, при создании универсального класса сначала определяется конкретный класс, после чего его типы поочередно заменяются параметрами типов до тех пор, пока не будет достигнут необходимый баланс между степенью обобщения и удобством работы. При создании собственных универсальных классов необходимо учитывать следующие важные моменты:
- Типы, которые требуется обобщить с использованием параметров типа. Как правило, чем больше типов параметризовано, тем более гибким и универсальным становится ваш код. Тем не менее слишком высокая степень обобщения может отрицательно сказаться на понятности создаваемого вами кода для других разработчиков.
- Ограничения (если требуются), которые будут применяться к параметрам типа (см. раздел Ограничения параметров типа). Рекомендуется применять максимально возможный объем ограничений, при котором вы по-прежнему сможете работать с необходимыми типами. Например, если универсальный класс будет использоваться только для работы со ссылочными типами, примените ограничение класса. Это позволит исключить случайное использование класса с типами значений и позволит использовать оператор as в отношении T , а также проверять наличие значений null.
- Требуется ли разбивать универсальные функции между базовыми классами и подклассами. Поскольку универсальные классы могут выступать в качестве базовых классов, здесь необходимо учитывать те же принципы разработки, что и для классов, не являющихся универсальными. См. описание правил наследования от универсальных базовых классов далее в этом разделе.
- Требуется ли реализовывать один или несколько универсальных интерфейсов. Например, при разработке класса, который будет использоваться для создания элементов коллекции на основе универсальных шаблонов, может потребоваться реализовать интерфейс IComparable , где T — это тип вашего класса.
Пример простого универсального класса можно найти в разделе Введение в универсальные шаблоны.
Правила в отношении параметров типа и ограничений влияют на поведение универсального класса, особенно в контексте наследования и доступа к элементам. Прежде чем продолжить, необходимо ознакомиться с некоторыми терминами и понятиями. Для универсального класса Node, клиентский код может ссылаться на класс путем указания аргумента типа для создания закрытого сконструированного типа ( Node ) или путем оставления параметра типа неуказанным, например при указании универсального базового класса, чтобы создать открытый сконструированный тип ( Node ). Универсальные классы могут наследоваться от конкретных, а также закрытых или открытых сконструированных базовых классов:
class BaseNode < >class BaseNodeGeneric < >// concrete type class NodeConcrete : BaseNode < >//closed constructed type class NodeClosed : BaseNodeGeneric < >//open constructed type class NodeOpen : BaseNodeGeneric
Классы, не являющиеся универсальными, то есть конкретные классы, могут наследоваться от закрытых сконструированных базовых классов. Наследование от аналогичных открытых классов или от параметров типа невозможно, поскольку во время выполнения клиентский код не может предоставить аргумент типа, необходимый для создания экземпляра базового класса.
//No error class Node1 : BaseNodeGeneric < >//Generates an error //class Node2 : BaseNodeGeneric <> //Generates an error //class Node3 : T <>
Универсальные классы, наследуемые от открытых сконструированных типов, должны предоставлять аргументы типа для любых параметров типа базового класса, которые не используются совместно с наследующим классом. Это продемонстрировано в следующем коде:
class BaseNodeMultiple < >//No error class Node4 : BaseNodeMultiple < >//No error class Node5 : BaseNodeMultiple < >//Generates an error //class Node6 : BaseNodeMultiple <>
Универсальные классы, наследуемые от открытых сконструированных типов, должны задавать множество ограничений, которые явно или косвенно включают в себя все ограничения базового типа:
class NodeItem where T : System.IComparable, new() < >class SpecialNodeItem : NodeItem where T : System.IComparable, new()
Универсальные типы могут использовать несколько параметров типа и ограничений, как показано ниже:
class SuperKeyType where U : System.IComparable where V : new()
Открытые и закрытые сконструированные типы можно использовать в качестве параметров метода:
void Swap(List list1, List list2) < //code to swap items >void Swap(List list1, List list2) < //code to swap items >
Если универсальный класс реализует интерфейс, все экземпляры такого класса можно привести к этому интерфейсу.
Универсальные классы инвариантны. Другими словами, если входной параметр задает List , при попытке предоставить List возникает ошибка времени компиляции.
См. также
- System.Collections.Generic
- Руководство по программированию на C#
- Универсальные шаблоны
- Сохранение состояния перечислителей
- Загадка по наследованию, часть 1
Совместная работа с нами на GitHub
Источник этого содержимого можно найти на GitHub, где также можно создавать и просматривать проблемы и запросы на вытягивание. Дополнительные сведения см. в нашем руководстве для участников.
Generics
Начиная с JDK 1.5, в Java появляются новые возможности для программирования. Одним из таких нововведений являются Generics. Generics являются аналогией с конструкцией «Шаблонов»(template) в С++, но имеет свои нюансы. Generics позволяют абстрагировать множество типов. Наиболее распространенными примерами являются Коллекции.
Вот типичное использование такого рода (без Generics):
1. List myIntList = new LinkedList(); 2. myIntList.add(new Integer(0)); 3. Integer x = (Integer) myIntList.iterator().next();
Как правило, программист знает, какие данные должны быть в List’e. Тем не менее, стоит обратить особое внимание на Приведение типа («Cast») в строчке 3. Компилятор может лишь гарантировать, что метод next() вернёт Object, но чтобы обеспечить присвоение переменной типа Integer правильным и безопасным, требуется Cast. Cast не только создает беспорядки, но дает возможность появление ошибки «Runtime Error» из-за невнимательности программиста.
И появляется такой вопрос: «Как с этим бороться? » В частности: «Как же зарезервировать List для определенного типа данных?»
Как раз такую проблему решают Generics.
1. List myIntList = new LinkedList (); 2. myIntList.add(new Integer(0)); 3. Integer x = myIntList.iterator().next();
Обратите внимание на объявления типа для переменной myIntList. Он указывает на то, что это не просто произвольный List, а List. Мы говорим, что List является generic-интерфейсом, который принимает параметр типа — в этом случае, Integer. Кроме того, необходимо обратить внимание на то, что теперь Cast выполняется в строчке 3 автоматически.
Некоторые могут задуматься, что беспорядок в коде увеличился, но это не так. Вместо приведения к Integer в строчке 3, у нас теперь есть Integer в качестве параметра в строчке 1. Здесь существенное отличие. Теперь компилятор может проверить этот тип на корректность во время компиляции.
И когда мы говорим, что myIntList объявлен как List, это будет справедливо во всем коде и компилятор это гарантирует.
Эффект от Generics особенно проявляется в крупных проектах: он улучшает читаемость и надежность кода в целом.
Свойства
- Строгая типизация
- Единая реализация
- Отсутствие информации о типе
Пример реализации Generic-класса
public interface List < E get(int i); set(int i, E e); add(E e); Iteratoriterator(); … >
Для того чтобы использовать класс как Generics, мы должны прописать после имени класса , куда можно подставить любое имя, wildcard и т.д.
После того как было объявлено имя generic-типа его можно использовать как обычный тип внутри метода. И когда в коде будет объявлен, к примеру, List, то Е станет Integer для переменной list (как показано ниже).
Теперь рассмотрим чем старая реализация кода отличается от новой:
List ─ список элементов E
List list = new List(); list.add(new Integer(1)); Integer i = (Integer) list.get(0);
List list = new List(); list.add(new Integer(1)); Integer i = list.get(0);
Как видите, больше не нужно приводить Integer, так как метод get() возвращает ссылку на объект конкретного типа (в данном случае – Integer).
Несовместимость generic-типов
Это одна из самых важных вещей, которую вы должны узнать о Generics
Как говорится: «В бочке мёда есть ложка дегтя». Для того чтобы сохранить целостности и независимости друг от друга Коллекции, у Generics существует так называемая «Несовместимость generic-типов».
Пусть у нас есть тип Foo, который является подтипом Bar, и еще G - наследник Коллекции. То G не является наследником G.
List li = new ArrayList(); List lo = li;
lo.add(“hello”); // ClassCastException: String -> int Integer li = lo.get(0);
Проблемы реализации Generics
- Решение 1 — Wildcard
Пусть мы захотели написать метод, который берет Collection и выводит на экран. И мы захотели вызвать dump для Integer.
void dump(Collection c) < for (Iteratori = c.iterator(); i.hasNext(); ) < Object o = i.next(); System.out.println(o); >>
List l; dump(l); List l; dump(l); // Ошибка
В этом примере List не может использовать метод dump, так как он не является подтипом List.
Проблема в том что эта реализация кода не эффективна, так как Collection не является полностью родительской коллекцией всех остальных коллекции, грубо говоря Collection имеет ограничения.
Для решения этой проблемы используется Wildcard («?»). Он не имеет ограничения в использовании(то есть имеет соответствие с любым типом) и в этом его плюсы. И теперь, мы можем вызвать dump с любым типом коллекции.
void dump(Collection c) < for (Iteratori = c.iterator(); i.hasNext(); ) < Object o = i.next(); System.out.println(o); >>
- Решение 2 – Bounded Wildcard
Пусть мы захотели написать метод, который рисует List. И у Shape есть наследник Circle. И мы хотим вызвать draw для Circle.
void draw(List c) < for (Iteratori = c.iterator(); i.hasNext(); ) < Shape s = i.next(); s.draw(); >>
List l; draw(l); List l; draw(l); // Ошибка
Проблема в том, что у нас не получится из-за несовместимости типов. Предложенное решение используется, если метод который нужно реализовать использовал бы определенный тип и его подтипов. Так называемое «Ограничение сверху». Для этого нужно вместо прописать .
void draw(List c) < for (Iteratori = c.iterator(); i.hasNext(); ) < Shape s = i.next(); s.draw(); >>
- Решение 3 – Generic-Метод
Пусть вы захотели сделать метод, который берет массив Object и переносить их в коллекцию.
void addAll(Object[] a, Collection c) < for (int i = 0; i < a.length; i++) < c.add(a[i]); >>
addAll(new String[10], new ArrayList()); addAll(new Object[10], new ArrayList()); addAll(new Object[10], new ArrayList()); // Ошибка addAll(new String[10], new ArrayList()); // Ошибка
Напомним, что вы не можете просто засунуть Object в коллекции неизвестного типа. Способ решения этой проблемы является использование «Generic-Метод» Для этого перед методом нужно объявить и использовать его.
void addAll(T[] a, Collection c) < for (int i = 0; i < a.length; i++) < c.add(a[i]); >>
Но все равно после выполнение останется ошибка в третьей строчке :
addAll(new Object[10], new ArrayList()); // Ошибка
- Решение 4 – Bounded type argument
Реализуем метод копирование из одной коллекции в другую
void addAll(Collection c, Collection c2) < for (Iteratori = c.iterator(); i.hasNext(); ) < M o = i.next(); c2.add(o); >>
addAll(new AL(), new AL()); addAll(new AL(), new AL()); //Ошибка
Проблема в том что две Коллекции могут быть разных типов (несовместимость generic-типов). Для таких случаев было придуман Bounded type argument. Он нужен если метод ,который мы пишем использовал бы определенный тип данных. Для этого нужно ввести (N принимает только значения M). Также можно корректно писать . (Принимает значения нескольких переменных)
void addAll(Collection c, Collection c2) < for (Iteratori = c.iterator(); i.hasNext(); ) < N o = i.next(); c2.add(o); >>
- Решение 5 – Lower bounded wcard
Реализуем метод нахождение максимума в коллекции.
> T max(Collection c)
List il; Integer I = max(il); class Test implements Comparable List tl; Test t = max(tl); // Ошибка
- > обозначает что Т обязан реализовывать интерфейс Comparable.
Ошибка возникает из за того что Test реализует интерфейс Comparable. Решение этой проблемы — Lower bounded wcard(«Ограничение снизу»). Суть в том что мы будет реализовывать метод не только для Т, но и для его Супер-типов(Родительских типов). Например: Если мы напишем
List list;
Мы можем заполнить его List, List или List.
> T max(Collection c)
- Решение 6 – Wildcard Capture
Реализуем метод Swap в List
void swap(List list, int i, int j) < list.set(i, list.get(j)); // Ошибка >
Проблема в том, что метод List.set() не может работать с List, так как ему не известно какой он List. Для решение этой проблемы используют «Wildcard Capture» (или «Capture helpers»). Суть заключается в том, чтобы обмануть компилятор. Напишем еще один метод с параметризованной переменной и будем его использовать внутри нашего метода.
void swap(List list, int i, int j) < swapImpl(list, i, j); > void swapImpl(List list, int i, int j)
Ограничения Generic
Также нужно запомнить простые правила для работы с Generics.
- Невозможно создать массив параметра типа
Collection c; T[] ta; new T[10]; // Ошибка !!
- Невозможно создать массив Generic-классов
new ArrayList>(); List[] la = new List[10]; // Ошибка !!
Преобразование типов
В Generics также можно манипулировать с информацией, хранящийся в переменных.
- Уничтожение информации о типе
List l = new ArrayList();
- Добавление информации о типе
List l = (List) new ArrayList(); List l1 = new ArrayList();
Примеры кода
- Первый пример:
List ls; List li; ls.getClass() == li.getClass() // True ls instanceof List // True ls instanceof List // Запрещено
- Второй пример:
Нахождение максимума в Коллекции Integer.
Collection c; Iterator i = c.iterator(); Integer max = (Integer) i.next(); while(i.hasNext()) < Integer next = (Integer) i.next(); if (next.compareTo(max) > 0) < max = next; >>
- С помощью Generics
Collection c; Iterator i = c.iterator(); Integer max = i.next(); while(i.hasNext()) < Integer next = i.next(); if (next.compareTo(max) >0) < max = next; >>
Шаблоны классов в С++
Мы уже ранее рассматривали в С++ такой инструмент, как шаблоны, когда создавали шаблоны функций. Почему стоит пользоваться шаблонами, было написано в статье, с шаблонами функций. Там мы рассмотрели основные положения шаблонов в С++. Давайте их вспомним.
Любой шаблон начинается со слова template , будь то шаблон функции или шаблон класса. После ключевого слова template идут угловые скобки — < >, в которых перечисляется список параметров шаблона. Каждому параметру должно предшествовать зарезервированное слово class или typename . Отсутствие этих ключевых слов будет расцениваться компилятором как синтаксическая ошибка. Некоторые примеры объявления шаблонов:
template
template
template
Ключевое слово typename говорит о том, что в шаблоне будет использоваться встроенный тип данных, такой как: int , double , float , char и т. д. А ключевое слово class сообщает компилятору, что в шаблоне функции в качестве параметра будут использоваться пользовательские типы данных, то есть классы. Но не в коем случае не путайте параметр шаблона и шаблон класса. Если нам надо создать шаблон класса, с одним параметром типа int и char , шаблон класса будет выглядеть так:
template class Name < //тело шаблона класса >;
где T — это параметр шаблона класса, который может принимать любой из встроенных типов данных, то, что нам и нужно.
А если параметр шаблона класса должен пользовательского типа, например типа Array , где Array — это класс, описывающий массив, шаблон класса будет иметь следующий вид:
template class Name < //тело шаблона класса >;
C этим вам лучше разобраться изначально, чтобы потом не возникало никаких ошибок, даже, если шаблон класса написан правильно.
Давайте создадим шаблон класса Стек, где стек — структура данных, в которой хранятся однотипные элементы данных. В стек можно помещать и извлекать данные. Добавляемый элемент в стек, помещается в вершину стека. Удаляются элементы стека, начиная с его вершины. В шаблоне класса Stack необходимо создать основные методы:
- Push — добавить элемент в стек;
- Pop — удалить элемент из стека
- printStack — вывод стека на экран;
Итак реализуем эти три метода, в итоге получим самый простой класс, реализующий работу структуры стек. Не забываем про конструкторы и деструкторы. Смотрим код ниже.
#include «stdafx.h» #include using namespace std; #include template class Stack < private: T *stackPtr; // указатель на стек int size; // размер стека T top; // вершина стека public: Stack(int = 10);// по умолчанию размер стека равен 10 элементам ~Stack(); // деструктор bool push(const T ); // поместить элемент в стек bool pop(); // удалить из стека элемент void printStack(); >; int main() < Stack myStack(5); // заполняем стек cout > temp; myStack.push(temp); > myStack.printStack(); // вывод стека на экран cout // конструктор template Stack::Stack(int s) < size = s >0 ? s: 10; // инициализировать размер стека stackPtr = new T[size]; // выделить память под стек top = -1; // значение -1 говорит о том, что стек пуст > // деструктор template Stack::~Stack() < delete [] stackPtr; // удаляем стек >// элемент функция класса Stack для помещения элемента в стек // возвращаемое значение — true, операция успешно завершена // false, элемент в стек не добавлен template bool Stack::push(const T value) < if (top == size - 1) return false; // стек полон top++; stackPtr[top] = value; // помещаем элемент в стек return true; // успешное выполнение операции >// элемент функция класса Stack для удаления элемента из стек // возвращаемое значение — true, операция успешно завершена // false, стек пуст template bool Stack::pop() < if (top == - 1) return false; // стек пуст stackPtr[top] = 0; // удаляем элемент из стека top--; return true; // успешное выполнение операции >// вывод стека на экран template void Stack::printStack() < for (int ix = size -1; ix >= 0; ix—) cout
#include using namespace std; #include template class Stack < private: T *stackPtr; // указатель на стек int size; // размер стека T top; // вершина стека public: Stack(int = 10);// по умолчанию размер стека равен 10 элементам ~Stack(); // деструктор bool push(const T ); // поместить элемент в стек bool pop(); // удалить из стека элемент void printStack(); >; int main() < Stack myStack(5); // заполняем стек cout > temp; myStack.push(temp); > myStack.printStack(); // вывод стека на экран cout // конструктор template Stack::Stack(int s) < size = s >0 ? s: 10; // инициализировать размер стека stackPtr = new T[size]; // выделить память под стек top = -1; // значение -1 говорит о том, что стек пуст > // деструктор template Stack::~Stack() < delete [] stackPtr; // удаляем стек >// элемент функция класса Stack для помещения элемента в стек // возвращаемое значение — true, операция успешно завершена // false, элемент в стек не добавлен template bool Stack::push(const T value) < if (top == size - 1) return false; // стек полон top++; stackPtr[top] = value; // помещаем элемент в стек return true; // успешное выполнение операции >// элемент функция класса Stack для удаления элемента из стек // возвращаемое значение — true, операция успешно завершена // false, стек пуст template bool Stack::pop() < if (top == - 1) return false; // стек пуст stackPtr[top] = 0; // удаляем элемент из стека top--; return true; // успешное выполнение операции >// вывод стека на экран template void Stack::printStack() < for (int ix = size -1; ix >= 0; ix—) cout
Как видите шаблон класса Stack объявлен и определен в файле с main -функцией. Конечно же такой способ утилизации шаблонов никуда не годится, но для примера сойдет. В строках 7 — 20 объявлен интерфейс шаблона класса. Объявление класса выполняется привычным для нас образом, а перед классом находится объявление шаблона, в строке 7. При объявлении шаблона класса, всегда используйте такой синтаксис.
Строки 47 — 100 содержат элемент-функции шаблона класса Stack, причем перед каждой функцией необходимо объявлять шаблон, точно такой же, как и перед классом — template . То есть получается, элемент-функции шаблона класса, объявляются точно также, как и обычные шаблоны функций. Если бы мы описали реализацию методов внутри класса, то заголовок шаблона — template для каждой функции прописывать не надо.
Чтобы привязать каждую элемент-функцию к шаблону класса, как обычно используем бинарную операцию разрешения области действия — :: с именем шаблона класса — Stack . Что мы и сделали в строках 49, 58, 68, 83, 96.
Обратите внимание на объявление объекта myStack шаблона класса Stack в функции main , строка 24. В угловых скобочка необходимо явно указывать используемый тип данных, в шаблонах функций этого делать не нужно было. Далее в main запускаются некоторые функции, которые демонстрируют работу шаблона класса Stack . Результат работы программы смотрим ниже.
CppStudio.com
Заталкиваем элементы в стек: 12 3456 768 5 4564 |4564 | 5 | 768 |3456 | 12 Удаляем два элемента из стека: | 0 | 0 | 768 |3456 | 12
К сожалению, для данной темы пока нет подходящих задач. Если у вас есть таковые на примете, отправте их по адресу: admin@cppstudio.com. Мы их опубликуем!
Шаблоны в С++
Шаблоны предназначены для быстрой генерации новых классов или функций имеющих одинаковый функционал. Иногда шаблоны классов и функций называют обобщенными или родовыми классами и функциями. В качестве параметров шаблона выступают сами типы, и иногда значения указанных типов. Шаблоны функций генерируются автоматически по типам их аргументов, для классов необходимо явно указать параметры шаблона при создании объекта. Методы классов также могут быть шаблонами. Методы шаблона класса уже являются шаблонами.
определение шаблонов
Шаблоны определяются ключевым словом, за которым в угловых скобках перечисляются параметры шаблона. Далее следует обычное определение объекта шаблона, т.е. функция, метод или класса. Если параметром шаблона является тип, то он определяется через ключевое слово typename. Параметры шаблона функции обязательно должны использоваться в описании аргументов.
#include #include #include using namespace std; template void Swap(T &a, T &b) < T c=a; a=b; b=c; >template class Arrayn < public: T data[n]; Arrayn() < ; >Arrayn(const T& val) < std::fill(data, data+n, val); >int size() < return n; >T& at(int ind); T& operator[](int ind) < if (ind>=n) throw std::out_of_range("incorrect index"); return data[ind]; > void operator =(const Arrayn &a) < std::copy(a.data, a.data+n, data); >// копирование массивов одного типа, но с разным числом эл. template void operator =(const Arrayn &a) < std::copy(a.data, a.data+std::min(n, k), data); >void fill(const T& val) < std::fill(data, data+n, val); >>; template T& Arrayn::at(int ind) < if (ind=n) throw std::out_of_range("incorrect index"); return data[ind]; >
использование шаблонов
Шаблон используется как обычная функция или класс. В последнем случае, после имени в угловых скобках указываются значения параметров шаблона, если они не заданы по умолчанию. Ниже приведен пример использования определенных выше шаблонов.
int main() < Arrayna1(4); Arrayn a3(2); // для второго параметра шаблона используется // значение по умолчанию Arrayn a2(7); a1=a2; for (int i=0; i dbls(3.14); for (int i=0; i catch(exception&e) < coutreturn 0; >
замещение шаблонов
Если для каких-то типов все же внутренний функционал должен изменится, то можно определить другой шаблон с таким же именем, уточнив типы или указав конкретные значения параметров. Например, для приведенного выше класса Arrayn, ниже определены два замещающих шаблона. Для краткости это пустые классы с одним методом, позволяющим определить какой именно шаблон используется.
// частичное замещение, шаблон будет использоваться, // если T является указателем на какой-нибудь тип template class Arrayn < public: void outptr() < >>; // полное замещение template<> class Arrayn < public: void outchr() < >>; int main() < Arraynb; b.outptr(); Arrayn c; c.outchr(); >
Для шаблонов функций вместо замещения можно воспользоваться перегрузкой функций.
шаблон как параметр шаблона
Если в качестве параметра шаблона используется другой шаблон, то вложенные угловые скобки должны быть разделены пробелами от окружающих угловых скобок.
template > .
Шаблоны функций
Начнём с простенького примерчика – напишем свою функцию обмена значениями 2-х переменных целого типа:
void swap(int &a, int &b)
А теперь захотим сделать тоже самое, только для типа float:
void swap(float &a, float &b)
Как мы видим, алгоритм одинаковый, поменялись только типы. Теперь мы можем захотеть написать тоже самое для других типов: char, short, double, string и т.д. Ну не писать же в самом деле для каждого типа свои функции? Всё правильно, потому что разработчики С++ пошли другим путём и придумали шаблоны функция template . Они имеют следующий вид: template < параметры_шаблона >описание_функции Для нашего примера:
template < typename T >void swap(T &a, T &b)
А как вызывать такую функцию? Как-то так:
int i1 = 1, i2 = 2; swap(i1, i2); // В скобках < >указываем конкретный тип int swap(i1, i2); // Компилятор сам подставит тип int float f1 = 1.5, f2 = 2.5; swap(f1, f2); // В скобках < >указываем конкретный тип float swap(f1, f2); // Компилятор сам подставит тип float
Итак, как мы видим, после имени функции в скобках указываем нужным нам тип данных. Скобки можно не указывать, в этом случае, компилятор сделает это за нас. После этого, компилятор где-то у себя сгенерирует несколько функций для каждого используемого типа данных. Например, если мы вызываем такую функцию с тремя разными типами, то сгенерируется три разные функции.
Шаблонные функции-члены класса
Функции, которые являются членами класса также могут быть шаблонными. Рассмотрим сразу на примере:
class Swap < public: template < typename T >void swap(T &a, T &b) < T c = a; a = b; b = c; >>;
Вызвать такую функцию-член также просто:
Swap sw; int i1 = 1, i2 = 2; sw.swap(i1, i2); float f1 = 1.5, f2 = 2.5; sw.swap(f1, f2);
Вот мы и разобрали шаблоны функций C++, теперь обязательно нужно изучить шаблоны классов.
Шаблоны классов
Отличие от шаблонов функций лишь одно – мы не отдельным функциям сообщаем, что хотим использовать обобщённые функции, а целому классу! Синтаксис шаблонов имеет следующий вид: template < параметры_шаблона >описание_класса Лучше рассмотрим на примере самописного стека:
#define STACK_SIZE 10 template class Stack < protected: T *pStack; //указатель на стек int top = -1; //вершина стека public: Stack(); ~Stack(); bool push(T element); bool pop(T& element); >; template Stack::Stack() < pStack = new T[STACK_SIZE]; >template Stack::~Stack() < delete[] pStack; >template bool Stack::push(T element) < if (top == STACK_SIZE - 1) return false; // стек полон top++; pStack[top] = element; return true; >template bool Stack::pop(T& element) < if (top == -1) return false; // стек пуст element = pStack[top]; top--; return true; >
Изучая код, мы видим, что и для описания класса и для описания его функций-членов используется конструкция: template
template bool Stack::push(T element)