Что такое шаблон класса
Шаблон класса позволяет описать один класс для нескольких типов. Например, стек может хранить числа, строки или другие объекты, а его операции при этом остаются одинаковыми.
Тип элементов задаётся параметром шаблона T:
#include <cassert>
#include <vector>
template<typename T>
class Stack
{
private:
std::vector<T> elems;
public:
void push(const T& elem);
void pop();
const T& top() const;
bool empty() const
{
return elems.empty();
}
};
Здесь std::vector<T> хранит элементы типа T. Метод push() принимает такой элемент, а top() возвращает ссылку на верхний элемент стека.
Для параметра типа записи template<typename T> и template<class T> равнозначны. Ниже используется первая форма.
Как определить методы
Метод можно определить внутри класса. Так уже сделано с empty(). Если определять метод за пределами класса, перед ним нужно повторить объявление параметра шаблона и указать Stack<T>:::
template<typename T>
void Stack<T>::push(const T& elem)
{
elems.push_back(elem);
}
template<typename T>
void Stack<T>::pop()
{
assert(!elems.empty());
elems.pop_back();
}
template<typename T>
const T& Stack<T>::top() const
{
assert(!elems.empty());
return elems.back();
}
Определения методов вместе с определением класса образуют полную реализацию Stack<T>. Компилятору нужны определения шаблонных методов там, где он создаёт их для конкретного типа. Поэтому такой код обычно размещают в заголовочном файле.
Как использовать шаблон
При создании объекта вместо T указывают конкретный тип:
Stack<int> intStack;
Stack<std::string> stringStack;
В Stack<int> поле elems имеет тип std::vector<int>, а push() принимает const int&. В Stack<std::string> используются std::vector<std::string> и const std::string&.
Пример использования
После определения Stack<T> можно создать два стека с разными типами элементов:
#include <iostream>
#include <string>
int main()
{
Stack<int> intStack;
intStack.push(7);
intStack.push(42);
std::cout << intStack.top() << '\n'; // 42
Stack<std::string> stringStack;
stringStack.push("hello");
stringStack.push("world");
std::cout << stringStack.top() << '\n'; // world
}
Stack<int> и Stack<std::string> — разные типы. Для каждого набора аргументов шаблона получается своя специализация класса.
Stack и Stack<T> внутри класса
Внутри определения шаблонного класса имя Stack без <T> обозначает текущую специализацию. Это удобно, например, при объявлении копирования:
template<typename T>
class Stack
{
public:
Stack(const Stack& other);
Stack& operator=(const Stack& other);
};
Для Stack<int> параметр конструктора означает const Stack<int>&. Внутри класса можно написать и полную форму Stack<T>, но короткая запись обычно проще.
За пределами класса аргумент шаблона указывают явно:
template<typename T>
bool operator==(const Stack<T>& lhs,
const Stack<T>& rhs);
У этой возможности есть название — injected-class-name: имя класса без аргументов шаблона внутри его собственного определения.
Когда создаётся код для конкретного типа
При использовании Stack<int> компилятор подставляет int вместо T. Поэтому стек хранит std::vector<int>, а его методы работают с int.
Код отдельных методов обычно инстанцируется тогда, когда их определения требуются программе. В следующем фрагменте используются push() и top():
Stack<int> stack;
stack.push(42);
std::cout << stack.top();
Если pop() для Stack<int> нигде не требуется, его тело обычно не инстанцируется. Благодаря этому отдельный метод может предъявлять к T требования, которые не нужны остальным методам.
Требование отдельного метода
#include <iostream>
template<typename T>
class Data
{
public:
void print(const T& value)
{
std::cout << value;
}
};
Когда используется print(), тип T должен поддерживать вывод в поток через operator<<. Другим методам класса эта операция может не понадобиться.
Что можно передать в аргумент шаблона
Полученную специализацию можно использовать как любой другой тип: создавать её объекты, массивы, ссылки и псевдонимы.
using IntStack = Stack<int>;
IntStack stack;
Stack<int> stacks[10];
Stack<int>& ref = stack;
const Stack<int>& constRef = stack;
Сам аргумент шаблона тоже может быть сложным типом. Например, Stack<float*> хранит указатели, а Stack<Stack<int>> хранит стеки целых чисел.
Стек стеков
Stack<int> first;
first.push(10);
first.push(20);
Stack<Stack<int>> stacks;
stacks.push(first);
std::cout << stacks.top().top() << '\n'; // 20
Для внешнего Stack параметр T равен Stack<int>. Поэтому внутри него находится std::vector<Stack<int>>.
Начиная с C++11 закрывающие скобки вложенных шаблонов можно писать подряд: Stack<Stack<int>>. В более ранних стандартах между ними требовался пробел: Stack<Stack<int> >.
Где можно объявить шаблон класса
Обычный шаблон класса объявляют в пространстве имён. Его нельзя объявить в локальной области функции:
template<typename T>
class Data
{
};
Шаблон класса также может быть членом другого класса. Но если перенести показанное объявление внутрь тела функции, код не скомпилируется.
Вывод аргументов шаблона класса
В примерах со стеком тип элементов указан явно: Stack<int> stack;. Начиная с C++17 компилятор в некоторых случаях может вывести аргументы шаблона класса из аргументов конструктора. Этот механизм называется Class Template Argument Deduction, или CTAD.
Вывод типа в C++17
template<typename T>
class Value
{
public:
Value(T value)
: value_(value)
{
}
private:
T value_;
};
Value value(42);
Из аргумента 42 компилятор выводит T = int. Тип объекта value — Value<int>.
У показанного выше Stack нет конструктора, из аргументов которого можно было бы вывести тип элементов. Поэтому запись Stack stack; для него не подходит: нужно указать, например, Stack<int> stack;.
Что нужно запомнить
- Шаблон класса описывает общую реализацию, а аргументы шаблона задают конкретный тип.
- При определении метода вне класса повторяют
template<typename T>и пишут имяStack<T>::. - Определения шаблонных методов должны быть доступны компилятору при их инстанцировании.
Stack<int>иStack<std::string>— разные типы.- Начиная с C++17 аргументы шаблона класса иногда можно вывести из аргументов конструктора.