Шаблон класса (class template) позволяет задать внутри класса объекты, тип которых на этапе написания кода неизвестен. Но прежде чем перейти к определению шаблона класса, рассмотрим проблему, с которой мы можем столкнуться и которую позволяют решить шаблоны.
Допустим, нам надо описать класс пользователя, которые хранит два имя и id (идентификатор), который отличает одного пользователя от другого. С именем все относителньо просто - это строка. А какой тип данных выбрать для хранения id? Мы можем хранить id как число, как строку, как данные какого-то другого типа данных. И каждый тип в разных ситуациях может иметь свои преимущества. Как правило, для id применяются числа и строки, и, на первый взгляд, мы можем просто определить два класса для разных типов:
#include <iostream>
#include <string>
// класс 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>
#include <string>
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>
#include <string>
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>
#include <string>
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>
#include <string>
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 будет представлять число.