Шаблоны функций. Продолжение

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

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

Продолжаем тему шаблонов функций и давайте обобщим описание шаблона для функции sq_rect следующим образом:

template <typename T1, typename T2>
auto sq_rect(T1 a, T2 b)
{
    return a * b;
}

Смотрите, здесь у нас два параметра типа T1 и T2, а в качестве возвращаемого типа записано ключевое слово auto, указывающее компилятору вычислить его самому на основе значения, которое возвращается оператором return.

Следует отметить, что ключевое слово auto в качестве возвращаемого типа допустимо прописывать, начиная со стандарта C++14. В более ранних стандартах приходилось использовать те или иные костыли, о которых теперь уже нет смысла даже вспоминать.

Давайте посмотрим на варианты вызова этой функции:

int main()
{
    int res_1 = sq_rect(5, 6.5); // double sq_rect<int, double>(int a, double b)
    double res_2 = sq_rect(5, 6); // int sq_rect<int, int>(int a, int b)
    short res_3 = sq_rect<short, short>(2, 3); // int sq_rect<short, short>(short a, short b)
    short res_4 = sq_rect<double>(2, 3); // double sq_rect<double, int>(double a, int b)
 
    return 0;
}

Как видите, теперь в функцию можно передавать аргументы разных типов благодаря использованию двух параметров T1 и T2 в шаблоне функции. Результаты формирования перегруженных функций везде ожидаемы, кроме, может быть функции с двумя типами short. Если вы ожидали, что возвращаемый тип так же будет иметь тип short, то, как видите, он был вычислен, как int. И это логично, т.к. возвращается целочисленное значение, тип которого по умолчанию начинается с int.

Параметры шаблонов с явным указанием типа

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

template <typename RT = double, typename T1, typename T2> 
RT sq_rect(T1 a, T2 b)
{
    return a * b;
}

А, затем, использовать этот шаблон в функции main:

int main()
{
    int res_1 = sq_rect(5, 6.5); // double sq_rect(int a, double b)
    double res_2 = sq_rect(5, 6); // double sq_rect(int a, int b)
    short res_3 = sq_rect<short>(2, 3); // short sq_rect(short a, short b)
    int res_4 = sq_rect<int>(2.3, 3.5); // int sq_rect(double a, double b)
 
    return 0;
}

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

Аргументы параметров шаблонов по умолчанию

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

template <int calc_t = 1, typename T1, typename T2>
auto get_rect(T1 width, T2 height)
{
    if(calc_t == 1)
        return width * height;  // площадь
    else
        return 2 * (width + height); // периметр
}

А, затем, воспользоваться им следующим образом:

int main()
{
    double res_1 = get_rect(5, 6.5); // площадь
    int res_2 = get_rect<2>(5, 6); // периметр
 
    std::cout << res_1 << " " << res_2 << std::endl;
 
    return 0;
}

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

template <typename T1, typename T2>
auto get_rect(T1 width, T2 height, int calc_t = 1)
{
    if(calc_t == 1)
        return width * height;  // площадь
    else
        return 2 * (width + height); // периметр
}

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

template <typename T, size_t N>
T ar_sum(const T (&ar)[N])
{
    std::cout << N << std::endl;
    
    T res = 0;
    for(size_t i = 0; i < N; ++i)
        res += ar[i];
    return res;
}

Когда массив передается по ссылке, то размер N является частью его типа и компилятор имеет возможность вычислить значение этого параметра при инстанцировании шаблона. В результате, этот параметр появляется как целочисленная переменная N в теле функции ar_sum.

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

int main()
{
    double data[] = {0.5, 3.2, 7.8, 3, 10.4, 5.6};
    int marks[] = {2, 2, 3, 2, 3};
 
    auto s = ar_sum(data); // double ar_sum<double, 6>(const double (&ar)[6])
    auto s2 = ar_sum(marks); // int ar_sum<int, 5>(const int (&ar)[5])
 
    std::cout << s << std::endl;
 
    return 0;
}

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

Перегрузка шаблонов функций

В заключение этого занятия отмечу, что шаблоны функций можно также перегружать между собой и даже с другими отдельными функциями. Например:

template <typename T> 
T add(T a, T b) { puts("add: 1"); return a + b; }
 
template <typename T> 
T add(T* a, T* b) { puts("add: 2"); return *a + *b; }
 
template <typename T1, typename T2> 
auto add(T1 a, T2 b) { puts("add: 3"); return a + b; }
 
void add(std::string& dest, const std::string& src)
{
    puts("add: 4");
    dest.append(src);
}

Воспользуемся ими в функции main следующим образом:

int main()
{
    std::string str_1 {"Hello"}, str_2 {"World"};
    int a {0}, b{3};
 
    add(&a, &b);        // add: 2
    add(str_1, str_2);  // add: 4
    add(1, 2);          // add: 1
    add(1.3, 2.7);      // add: 1
    add(1, 2.5);        // add: 3
    
    return 0;
}

Правило выбора того или иного шаблона или функции следующее. Если под типы аргументы подходит явно объявленная (не шаблонная) функция, то компилятор выбирает именно ее. Поэтому для строк типа std::string была вызвана последняя функция с выводом «add: 4». Остальные шаблоны выбираются между собой в соответствии с типами. Если типы обоих аргументов функции add совпадают, то вызывается шаблон «add: 1» или «add: 2». Если же типы различаются, то шаблон «add: 3». Тип возвращаемого значения здесь не играет никакой роли.

Обратите внимание, если вместо функции «add: 4» требуется явно вызвать именно шаблонный вариант, то это можно сделать так:

    add<>(str_1, str_2);  // add: 1

Здесь угловые скобки указывают компилятору генерировать (инстанцировать) функцию из подходящего шаблона.

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

Видео по теме