Что такое шаблон класса

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

Тип элементов задаётся параметром шаблона 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 аргументы шаблона класса иногда можно вывести из аргументов конструктора.