Практический курс по ООП C++: https://stepik.org/a/205781
В языке C++ помимо
шаблонов функций можно определять и шаблоны классов (и по аналогии шаблоны структур).
Давайте представим, что в программах предполагается использовать класс Point для
представления точки на плоскости. Пусть для простоты он будет такой:
class Point {
int x {}, y {};
public:
Point(int a, int b) : x(a), y(b)
{ }
};
Типы координат x, y здесь
определены как int. Однако, в общем случае, нам могут понадобиться и
другие типы, например, double. Как тогда быть? Объявлять еще
один подобный класс, который будет отличаться только типом данных для x, y? Например:
class DoublePoint {
double x {}, y {};
public:
DoublePoint(double a, double b) : x(a), y(b)
{ }
};
Как вы
понимаете, это приводит к дублированию кода, которого следует избегать при проектировании
программ. И здесь, как раз, очень хорошо помогают шаблоны, в частности, шаблоны
классов.
Чтобы описать
шаблон какого-либо класса используется следующий синтаксис:
template <параметры шаблона>
class имя_класса {
// тело шаблонного класса
};
Параметр шаблона
может быть один или несколько (можно, конечно, не указывать ни одного, но это обычно
специализация шаблона). Если параметров несколько, то они прописываются через
запятую.
Итак, для класса
Point шаблон можно
записать следующим образом:
template <typename T>
class Point {
T x{}, y{};
public:
Point(T a, T b) : x(a), y(b)
{ }
};
Обратите
внимание, что ключевое слово typename определяет
параметр типа, который, затем, можно использовать внутри класса Point. В частности,
мы его прописываем у переменных x, y, а также в
конструкторе Point. Однако вместо typename допустимо
использовать ключевое слово class:
которое имеет
практически тот же самый смысл, что и typename. Однако на
практике сейчас в основном пользуются ключевым словом typename, т.к. оно
точнее подчеркивает смысл параметра T.
После объявления
шаблона класса Point, мы можем использовать его для создания
объектов, например, в функции main, следующим образом:
int main()
{
Point<int> pt_i(1, 2);
Point<double> pt_d(1, 2);
Point pt_i2(10, 20); // начиная с C++17
Point pt_d2(1.2, 2.5); // начиная с C++17
return 0;
}
В первых двух
вариантах после класса Point в угловых
скобках явно прописан тип для параметра T. И это было
строго обязательно до стандарта C++17. Начиная со стандарта C++17, параметры
шаблона класса могут быть вычислены по аргументам, передаваемыми в конструктор
класса.
Шаблоны с параметрами по умолчанию
Сразу отмечу,
что компилятор не всегда имеет возможность вычислить параметры шаблонов с
использованием конструктора. Например, если из конструктора убрать все
параметры и записать шаблон класса в виде:
template <typename T>
class Point {
T x{}, y{};
public:
Point() {}
};
То, очевидно,
его нельзя будет использовать без явного указания типа для параметра T:
int main()
{
Point<int> pt_i;
Point<double> pt_d;
Point pt_i2; // ошибка
return 0;
}
Однако в
шаблонах можно указывать начальные значения параметров, например, так:
template <typename T=int>
class Point {
T x{}, y{};
public:
Point() {}
};
Тогда при
создании объекта pt_i2 ошибки не будет, так как вместо T подставляется
значение по умолчанию int.
Шаблоны классов с несколькими параметрами
При этом
вычисление типов происходит так же, как и при вызове шаблонных функций. Например,
если передать аргументы разных типов:
Point pt_d2(1.2, 2); // ошибка
то компилятор не
сможет вычислить единый тип для параметра T и выдаст
ошибку. Выйти из подобных ситуаций легко, достаточно в угловых скобках
прописать желаемый тип:
Point<double> pt_d2(1.2, 2); // ok
Именно так было
бы правильно использовать шаблон Point. Однако на этом примере я покажу, как
можно использовать несколько параметров в шаблоне класса. Пусть у нас каждая
координата x, y может иметь
разные типы данных. Тогда шаблон запишется в виде:
template <typename T1, typename T2>
class Point {
public:
T1 x{};
T2 y{};
public:
Point(T1 a, T2 b) : x(a), y(b)
{ }
};
Использовать его
теперь следует так:
int main()
{
Point<int, int> pt_i(1, 2);
Point<double, int> pt_d(1, 2);
Point pt_i2(10, 20); // начиная с C++17
Point pt_d2(1.2, 2); // начиная с C++17
return 0;
}
Теперь, при
вычислении типов T1 и T2 при создании объекта pt_d2 не возникает
никаких ошибок, так как T1 будет соответствовать double, а T2 – int.
Инстанцирование шаблонов классов
Шаблоны классов,
так же, как и шаблоны функций, не описывают конкретный класс – это лишь
инструкция компилятору по созданию различных классов с разными типами данных.
Когда шаблон класса используется непосредственно в программе, как правило, для
создания объектов, компилятор вычисляет параметры шаблона и на их основе
формирует соответствующий класс. Этот процесс называется инстацированием
классов.
Объявление методов шаблона класса
Давайте теперь
посмотрим, как выполняется объявление методов в шаблонах классов. Для класса Point можно прописать
следующие очевидные методы:
template <typename T>
class Point {
T x{}, y{};
public:
Point(T a = 0, T b = 0) : x(a), y(b)
{ }
T get_x() { return x; }
T get_y() { return y; }
void set_xy(T a, T b)
{
x = a;
y = b;
}
};
Как видите, все
достаточно очевидно и просто. Воспользоваться ими можно следующим образом:
int main()
{
Point<int> pt1;
Point<double> pt2(5.6, -3.4);
pt1.set_xy(10, 20);
std::cout << pt1.get_x() << std::endl;
return 0;
}
При
инстацировании класса Point для объекта pt1 шаблонный
параметр T равен типу int и подставляется
при объявлении методов этого класса. Аналогично для объекта pt2, только там
будет сгенерирован класс с типом T=double.
Но, что если мы
бы хотели объявить реализацию метода вне шаблона класса, например, для set_xy? Делается это следующим образом:
template <typename T>
void Point<T>::set_xy(T a, T b)
{
x = a;
y = b;
}
Фактически,
метод описывается, как шаблон функции, только дополнительно вначале указывается
его область видимости, а именно, шаблон класса Point<T>. То же
самое следует делать и при описании конструкторов и деструкторов вне шаблона.
Например:
template <typename T>
Point<T>::Point(T a, T b) : x(a), y(b)
{ }
template <typename T>
Point<T>::~Point()
{ }
А так же для
статических переменных:
template <typename T>
class Point {
static T counter_obj;
T x{}, y{};
...
};
template <typename T>
T Point<T>::counter_obj = 0;
Вот в целом идея
шаблонов классов. На следующем занятии мы продолжим эту тему и рассмотрим
некоторые возможности и особенности работы шаблонов классов.
Практический курс по ООП C++: https://stepik.org/a/205781