Шаблон класса (class template) позволяет задать внутри класса объекты, тип которых на этапе написания кода неизвестен. Но прежде чем перейти к определению шаблона класса, рассмотрим проблему, с которой мы можем столкнуться и которую позволяют решить шаблоны.
Допустим, нам надо описать класс пользователя, которые хранит два имя и id (идентификатор), который отличает одного пользователя от другого. С именем все относителньо просто - это строка. А какой тип данных выбрать для хранения id? Мы можем хранить id как число, как строку, как данные какого-то другого типа данных. И каждый тип в разных ситуациях может иметь свои преимущества. Как правило, для id применяются числа и строки, и, на первый взгляд, мы можем просто определить два класса для разных типов:
#include <iostream> // класс Person, где id - целое число class UintPerson { public: UintPerson(unsigned id, std::string name) : id{id}, name{name} { } void print() const { std::cout << "Id: " << id << "\tName: " << name << std::endl; } private: unsigned id; std::string name; }; // класс Person, где id - строка class StringPerson { public: StringPerson(std::string id, std::string name) : id{id}, name{name} { } void print() const { std::cout << "Id: " << id << "\tName: " << name << std::endl; } private: std::string id; std::string name; }; int main() { UintPerson tom{123456, "Tom"}; tom.print(); // Id: 123456 Name: Tom StringPerson bob{"tvi4xhcfhr", "Bob"}; bob.print(); // Id: tvi4xhcfhr Name: Bob }
Здесь класс UintPerson представляет класс пользователя, где id представляет целое число типа unsinged, а тип StringPerson - класс пользователя, где id - строка. В функции main мы можем создавать объекты этих типов и успешно их использовать. Хотя данный пример работает, но по сути мы получаем два идентичных класса, которые отличаются только типом переменной id. А что, если для id потребуется использовать какой-то еще тип? Чтобы упростить код в C++ можно использовать шаблоны классов.
Шаблоны классов позволяют уменьшить повторяемость кода. Для определения шаблона класса применяется следующий синтаксис:
template <список_параметров> class имя класса { // содержимое шаблона класса };
Для применения шаблонов перед классом указывается ключевое слово template, после которого идут угловые скобки. В угловых скобках указываются параметры шаблона. Если несколько параметров шаблона, то они указываются через запятую.
Сам шаблон класса, как и обычный класс, всегда начинается с ключевого слова class (или struct, если речь о структуре), за которым следует имя шаблона класса и тело определения в фигурных скобках. Как и в случае с обычным классом, все шаблон класса заканчивается точкой с запятой. Содержимое шаблона класса фактически аналогично определению стандартного класса за тем исключением, что внутри шаблона вместо конкретных типов мы можем использовать параметры шаблона, которые указаны в угловых скобках. Во всем остальном шаблон класса подобен обычному классу, который может наследоваться, определять функции, переменные, конструкторы, переопределять виртуальные функции и т.д.
Параметр в угловых скобках представляет произвольный идентификатор, перед которым указывается слово typename или class:
template <typename T> // или так template <class T>
Здесь определен один параметр, который называется T
. Какое слово перед ним использовать - class
или typename
, не столь важно.
Перепишем пример с классами UintPerson и StringPerson, применив шаблоны:
#include <iostream> template <typename T> class Person { public: Person(T id, std::string name) : id{id}, name{name} { } void print() const { std::cout << "Id: " << id << "\tName: " << name << std::endl; } private: T id; std::string name; }; int main() { Person tom{123456, "Tom"}; // T - число tom.print(); // Id: 123456 Name: Tom Person bob{"tvi4xhcfhr", "Bob"}; // T - строка bob.print(); // Id: tvi4xhcfhr Name: Bob }
В данном случае шаблон класса применяет один параметр - T. То есть это будет какой-то тип, но какой именно, на этапе написания кода неизвестно.
template <typename T> class Person {
Данный параметр T будет представлять тип переменной id:
T id;
При создании объектов шаблона класса Person, компилятор на основании первого параметра конструктора будет выводить тип id. Например, в первом случае:
Person tom{123456, "Tom"};
полю id передается число 123456. Поскольку это числовой литерал типа int, то и id будет представлять тип int
.
Во втором случае
Person bob{"tvi4xhcfhr", "Bob"};
переменной id передается строка "tvi4xhcfhr" - это литерал типа const char*
, соответственно id будет представлять этот тип.
В этом случае компилятор будет создавать два определения класса - для каждого набора типов - для int и для const char*
и будет использовать эти определения классов
для создания его объектов, которые применяют определенный тип данных для id.
В примере выше тип id определялся автоматически. Но мы также можем явным образом указать тип в угловых скобках после названия класса:
int main() { Person<unsigned> tom{123456, "Tom"}; tom.print(); // Id: 123456 Name: Tom Person<std::string> bob{"tvi4xhcfhr", "Bob"}; bob.print(); // Id: tvi4xhcfhr Name: Bob }
Также можно применять сразу несколько параметров. Например, необходимо определить класс банковского перевода:
#include <iostream> template <typename T, typename V> class Transaction { public: Transaction(T fromAcc, T toAcc, V code, unsigned sum): fromAccount{fromAcc}, toAccount{toAcc}, code{code}, sum{sum} { } void print() const { std::cout << "From: " << fromAccount << "\tTo: " << toAccount << "\tSum: " << sum << "\tCode: " << code << std::endl; } private: T fromAccount; // с какого счета T toAccount; // на какой счет V code; // код операции unsigned sum; // сумма перевода }; int main() { // явная типизация Transaction<std::string, int> transaction1{"id1234", "id5678", 2804, 5000}; transaction1.print(); // From: id1234 To: id5678 Sum: 5000 Code: 2804 // неявная типизация Transaction transaction2{"id6789", "id9018", 3000, 6000}; transaction2.print(); // From: id6789 To: id9018 Sum: 6000 Code: 3000 }
Класс Transaction использует два параметра типа T и V. Параметр T определяет тип для счетов, которые участвуют в процессе перевода. Здесь в качестве номеров счетов можно использовать и числовые и строковые значения и значения других типов. А параметр V задает тип для кода операции - опять же это может быть любой тип.
При использовании шаблона в этом случае надо указать два типа:
Transaction<std::string, int> transaction1("id1234", "id5678", 2804, 5000);
Типы передаются параметрам по позиции. Так, тип string будет использоваться вместо параметра T, а тип int - вместо параметра V.
В случае с переменной transaction2 типы T и V выводятся исходя из параметров конструктора.
Синтаксис определения функций вне шаблона класса может немного отличаться от их определения внутри шаблона. В частности, определения функций вне шаблона класса должны определяться как шаблон, даже если они не используют параметры шаблона.
При определении конструктора вне шаблона класса, его имя должно уточняться именем шаблона класса:
#include <iostream> template <typename T> class Person { public: Person(T, std::string); // обычный конструктор Person(const Person&); // конструктор копирования ~Person(); // деструктор Person& operator=(const Person&); // оператор присваивания void print() const; // функция класса private: T id; std::string name; }; // определение конструктора вне шаблона класса template <typename T> Person<T>::Person(T id, std::string name) : id{id}, name{name} { } // определение конструктора копирования вне шаблона класса template <typename T> Person<T>::Person(const Person& person) : id{person.id}, name{person.name} { } // определение деструктора копирования вне шаблона класса template <typename T> Person<T>::~Person(){ std::cout << "Person deleted" << std::endl; } // определение оператора присвоения вне шаблона класса template <typename T> Person<T>& Person<T>::operator=(const Person& person) { if (&person != this) { name = person.name; id = person.id; } return *this; } // определение функции вне шаблона класса template <typename T> void Person<T>::print() const { std::cout << "Id: " << id << "\tName: " << name << std::endl; } int main() { Person tom{123456, "Tom"}; tom.print(); Person tomas{tom}; // конструктор копирования tomas.print(); Person tommy = tom; // оператор присваивания tommy.print(); }
В данном случае все функции, в том числе конструкторы, деструктор, функция оператора присваивания, определяются как функции шаблона класса
Person<T>
. Причем в данном случае конструктор копирования или функция print никак не используют параметр T, но все равно они определяются как шаблоны.
То же самое касается и деструктора.
Как и параметры функций, параметры шаблонов могут иметь значения по умолчанию - тип по умолчанию, который будет использоваться. Например:
#include <iostream> template <typename T=int> class Person { public: Person(std::string name) : name{name} { } void setId(T value) { id = value;} void print() const { std::cout << "Id: " << id << "\tName: " << name << std::endl; } private: T id; std::string name; }; int main() { Person<std::string> bob{"Bob"}; // T - std::string bob.setId("id1345"); bob.print(); // Id: id1345 Name: Bob Person tom{"Tom"}; // T - int tom.setId(23456); tom.print(); // Id: 23456 Name: Tom }
Здесь для параметра шаблона в качестве типа по умолчанию используется тип int. Параметр шаблона определяет тип переменной id, которую можно установить через функцию setId.
Мы можем указать тип в угловых скобках явным образом:
Person<std::string> bob{"Bob"}; // T - std::string bob.setId("id1345");
В данном случае в качестве типа параметра шаблона применяется тип std::string
, соответственно id будет представлять строку.
Во втором случае тип явным образом не указывается, поэтому применяется тип по умолчанию - int:
Person tom{"Tom"}; // T - int tom.setId(23456);
Поэтому здесь id будет представлять число.