Введение в шаблоны функций

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

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

Начиная с этого занятия, затронем еще одну важную тему – обобщенное программирование на C++ или просто шаблоны (templates). Если рассматривать их во всех деталях, то этому можно посвятить целый, отдельный курс. Однако реальное практическое применение шаблонов часто сводится к использованию их основных конструкций. Именно с ними мы с вами и познакомимся. Если же впоследствии потребуется углубиться в эту тему, то базовый задел у вас уже будет.

Итак, что же такое шаблоны, для чего они были придуманы и как реализуются в языке C++? Начнем с самого простого – шаблона функций, а затем перейдем к шаблонам классов. Как мы уже знаем, язык C/C++ имеет строгую типизацию, то есть, при объявлении переменных, параметров функций и методов нужно явно прописывать те или иные типы. Например, нам понадобилась функция вычисления площади прямоугольника. Сразу возникает вопрос, какие типы переменных использовать при ее объявлении? Особенно он актуален, если дальнейшее применение функции предполагается в самых разных проектах и при разных исходных данных. В этом случае, зачастую, объявляют перегруженные функции следующим образом:

int sq_rect(int a, int b)
{
    return a * b;
}
 
double sq_rect(double a, double b)
{
    return a * b;
}

Но при таком подходе получаем один существенный недостаток – дублирование текста программы (реализации таких функций, как правило, очень схожи). Конечно, функция sq_rect очень проста и особых проблем с ней не возникло бы. Но, представьте, что мы написали свой непростой алгоритм сортировки данных и его нужно распространить на произвольные типы. Тогда отладка и изменение алгоритма в нескольких перегруженных функциях превратилась бы в сущий мрак для программиста. Поэтому и был предложен инструмент, который бы решал эту задачу, а именно, позволял бы объявлять функции и классы на более высоком, общем уровне, не привязываясь к конкретным типам данных.

Как же в языке C++ возможно объявление функций без привязки к определенным типам? На самом деле используется один очевидный и хитрый ход: на уровне текста программы функция описывается с типами, заданными, как параметры. Применительно к функции sq_rect, это можно записать следующим образом:

template <typename T>
T sq_rect(T a, T b)
{
    return a * b;
}

Здесь template – ключевое слово, говорящее компилятору, что дальше будет идти объявление шаблона функции (или класса) с некоторым типом T. Ключевое слово typename в угловых скобках определяет параметр типа. Буква T – идентификатор параметра. В общем случае, вместо T можно прописать любое другое допустимое имя, так же, как и имя переменной. Но принято использовать заглавные буквы и дополнительно еще цифры, если это необходимо.

Итак, T – это параметр типа, который, на момент описания шаблона функции, не связан ни с каким конкретным типом данных. Далее, мы используем этот идентификатор, объявляя через него параметры функции и возвращаемый тип. В теле функции прописывается операция умножения переменных a и b этого параметаризованного типа данных.

Инстанцирование шаблона. Вызов шаблонных функций

Так как тип T на момент описания шаблона функции не определен, то компилятор не может перевести ее в машинный код. Чтобы это стало возможным, функцию sq_rect необходимо вызвать, например, так:

int main()
{
    int res_1 = sq_rect(1, 2);          // T = int
    double res_2 = sq_rect(3.5, 8.9);   // T = double
    short res_3 = sq_rect(5, 4);        // T = int
    double res_4 = sq_rect(4.2f, 3.2f); // T = float
 
    int x {5}, int y{7};
    int res_5 = *sq_rect(&x, &y);     // T = int *
 
    return 0;
}

В зависимости от типа переданных аргументов, параметр T в шаблоне функции sq_rect принимает соответствующий тип. При этом компилятор на основе этого шаблона формирует код функции с этим определенным типом. В результате получаем реализации перегруженных функций с типами int, double и float. Этот процесс получил название инстанцирование шаблона.

То есть, пока шаблон не используется, он не преобразуется в машинный код. Это некая абстракция на уровне текста программы языка C++ и не более того. Благодаря этому появляется возможность описывать типы на уровне параметров и таким образом объявлять шаблоны.

При необходимости, мы можем явно прописывать тип для параметра T в момент вызова функции:

    short res_3 = sq_rect<short>(5, 4);    // T = short
    double res_4 = sq_rect<float>(4, 3);   // T = float

В этом случае автоматическое вычисление типа компилятором отключается и функция формируется с указанным в угловых скобках типом (или типами, если их несколько).

Преимущество такого подхода может быть в том, что если через аргументы в шаблонную функцию передаются разные типы:

    int res_1 = sq_rect(5, 2.5);

то возникает неопределенность, какого типа должен быть параметр T. Компилятор в этом случае генерирует ошибку. Но, если в угловых скобках будет явно прописан желаемый тип:

    int res_1 = sq_rect<double>(5, 2.5);

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

Прототип шаблонной функции

Шаблон любой функции по-прежнему можно разделить на ее прототип и полное описание. Делается это следующим очевидным образом:

template <typename T> T sq_rect(T a, T b); // прототип шаблонной функции
 
int main()
{
...
    return 0;
}
 
template <typename T>
T sq_rect(T a, T b)  // шаблон с телом функции
{
    return a * b;
}

Также следует отметить, что в ранних стандартах языка C++ (до C++11) вместо ключевого слова typename нужно было прописывать ключевое слово class:

template <class T>
T sq_rect(T a, T b)
{
    return a * b;
}

Для шаблонов функций – это одно и то же. И, вообще, в большинстве случаев typename и class работают совершенно одинаково. Но, все же, на мой взгляд, слово typename более информативно, чем слово class, поэтому в программах чаще всего применяют именно его.

Вариации использования параметра типа

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

template <typename T>
T sq_rect(const T& a, const T& b)
{
    return a * b;
}
 
template <typename T>
void swap(T* x, T* y)
{
    T temp = *x;
    *x = *y;
    *y = temp;
}

Обратите внимание, что возвращаемый тип функции swap указан явно void, т.е. мы можем совершенно свободно комбинировать явные типы с параметрами типов при описании шаблона. Также в теле функции swap объявляется дополнительная переменная temp типа T. Так тоже можно делать, то есть, идентификатор типа допустимо использовать в любом месте описания функции.

Использовать эти шаблоны можно очевидным образом:

int main()
{
    double a {7.8}, b {-5.6};
 
    swap(&a, &b);       // T = double; void swap(double*, double*);
    std::cout << a << " " << b << std::endl;
 
    int res_1 = sq_rect(5, 2);  // T = int; int sq_rect(const int& a, const int& b);
    std::cout << res_1 << std::endl;
 
    return 0;
}

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

Видео по теме