Шаблон класса

Последнее обновление: 20.03.2023

Шаблон класса (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 будет представлять число.

Помощь сайту
Юмани:
410011174743222
Перевод на карту
Номер карты:
4048415020898850