Специализация и наследование шаблонов классов

Практический курс по ООП C++: https://stepik.org/a/205781

Смотреть материал на YouTube | RuTube

Продолжаем тему шаблонов классов и давайте представим, что имеется уже знакомый нам шаблон класса Point:

template <typename T>
class Point {
         T x{}, y{};
public:
         Point(T a, T b) : x(a), y(b)
                   { }
 
         T get_x() { return x; }
         T get_y() { return y; }
};

и дополнительно объявим два обычных класса для представления цветов в разных форматах:

class ColorRGB {
         unsigned char r{}, g{}, b{};
};
 
class ColorUVB {
         unsigned char u{}, v{}, b{};
};

А следом объявим еще один шаблонный класс Rectangle для представления прямоугольников на плоскости, заданных двумя координатами: верхнего левого и нижнего правого углов:

template <typename PT, typename CL>
class Rectangle {
         Point<PT> sp, ep; // координаты прямоугольника
         CL color; // цвет прямоугольника
public:
         Rectangle(Point<PT> pt1, Point<PT> pt2) : sp{pt1}, ep{pt2}
                   { puts("Rectangle"); }
 
         void set_color(CL cl) { color = cl; }
};

Здесь первый шаблонный параметр PT определяет тип координат точек прямоугольника, а второй параметр CL – цвет при отображении прямоугольника.

Теперь мы можем воспользоваться этим шаблоном следующим образом:

int main()
{
         Point<int> start(0, 0), end(10, 20);
 
         Rectangle<int, ColorRGB> rect1(start, end);
 
         return 0;
}

Обратите внимание, что в качестве второго типа указан класс ColorRGB. Очевидно, так тоже можно делать, т.к. имя класса (или структуры) – это полноценный пользовательский тип данных.

Однако при использовании шаблонов у нас может возникнуть ситуация, когда логика работы класса меняется для некоторых типов и эту логику следовало бы описывать в отдельном шаблоне. Например, мы так спроектировали программу, что для класса ColorUVB обработка цвета в классе Rectangle происходит несколько иначе, чем в стандартной палитре RGB. Поэтому для типа ColorUVB было бы правильно объявить свой шаблон класса Rectangle. Сделать это можно следующим образом:

template <typename PT>
class Rectangle<PT, ColorUVB> {
         Point<PT> sp, ep; // координаты прямоугольника
         ColorUVB color; // цвет прямоугольника
public:
         Rectangle(Point<PT> pt1, Point<PT> pt2) : sp{pt1}, ep{pt2}
                   { puts("Rectangle<PT, ColorUVB>"); }
 
         void set_color(ColorUVB cl) { color = cl; }
};

Теперь, при создании объекта с типом ColorUVB будет использован именно этот шаблон класса:

Rectangle<int, ColorUVB> rect2(start, end);

В языке C++ такой подход называется специализацией шаблонов классов. Шаблон специализируется под строго определенные типы. В нашем примере под тип ColorUVB, который уже не нужно прописывать в списке параметров. Причем специализация шаблонов обязательно должна идти после общего шаблона класса.

Если в шаблоне заменяются (специализируются) все типы. Например:

template <>
class Rectangle<double, ColorUVB> {
         Point<double> sp, ep; // координаты прямоугольника
         ColorUVB color; // цвет прямоугольника
public:
         Rectangle(Point<double> pt1, Point<double> pt2) : sp{pt1}, ep{pt2}
                   { puts("Rectangle<double, ColorUVB>"); }
 
         void set_color(ColorUVB cl) { color = cl; }
};

То это называется полной специализацией шаблона. В противовес частичной специализации предыдущего примера.

Соответственно, использование всех этих шаблонов выглядит следующим образом:

int main()
{
         Point<int> start(0, 0), end(10, 20);
         Point<double> sd(0, 0), ed(10, 20);
 
         Rectangle<int, ColorRGB> rect1(start, end);
         Rectangle<int, ColorUVB> rect2(start, end);
         Rectangle<double, ColorUVB> rect3(sd, ed);
 
         return 0;
}

Увидим в консоли строчки:

Rectangle
Rectangle<PT, ColorUVB>
Rectangle<double, ColorUVB>

Вот принцип специализации шаблонов классов и их назначение.

Наследование шаблонных классов

Давайте теперь предположим, что у нас есть базовый класс GeomBase, представленный в виде следующего шаблона:

template <typename T>
class GeomBase {
protected:
    T x0{0}, y0{0}, x1{0}, y1{0};
public:
         GeomBase(T x0, T y0, T x1, T y1) : x0{x0}, y0{y0}, x1{x1}, y1{y1}
                   { }
};

От этого шаблонного класса наследуется шаблонный класс Rectangle. Записывается такое наследование следующим образом:

template <typename PT>
class Rectangle : public GeomBase<PT> {
public:
         Rectangle(PT x0, PT y0, PT x1, PT y1) : GeomBase<PT>(x0, y0, x1, y1)
                   { puts("Rectangle"); }
};

Как видите, все достаточно очевидно. Главное помнить, что при использовании шаблонного класса (при наследовании или при инициализации) всегда нужно указывать параметры этого шаблона в угловых скобках. Во всем остальном наследование работает так же, как и с обычными классами.

Если же нам нужно прописать наследование для обычного не шаблонного класса, то это делается так:

class Rectangle : public GeomBase<int> {
public:
         Rectangle(int x0, int y0, int x1, int y1) : GeomBase<int>(x0, y0, x1, y1)
                   { puts("Rectangle"); }
};

То есть, вместо параметра шаблона нужно указать конкретный тип данных, тем самым инстанцировать шаблон GeomBase и сформировать на его основе конкретный класс, от которого, затем, и происходит наследование.

Заключение

Это было краткое введение в шаблоны функций и классов. Но его достаточно для первых шагов в их применении и понимании текста программ, которые их используют. В качестве небольшого практического задания предлагаю переписать класс DArray для работы с динамическим массивом в виде шаблонного класса, в котором тип элементов массива определялся бы параметром шаблона T. Сам класс DArray, я напомню, мы ранее создавали в этом курсе.

Практический курс по ООП C++: https://stepik.org/a/205781

Видео по теме