Аргументы шаблона по умолчанию
Параметру шаблона можно задать аргумент по умолчанию:
template<typename T = int>
void foo();
Если при вызове тип не указан явно, используется аргумент по умолчанию:
foo(); // T = int
foo<double>(); // T = double
Во втором случае явно указанный аргумент заменяет аргумент по умолчанию.
Зависимость от предыдущих параметров
Аргумент по умолчанию может зависеть от предшествующих параметров шаблона:
#include <type_traits>
template<typename T1, typename T2,
typename RT = std::common_type_t<T1, T2>>
RT max_value(T1 a, T2 b)
{
return b < a ? a : b;
}
Типы T1 и T2 выводятся из аргументов функции, а RT определяется через std::common_type_t:
auto a = max_value(4, 7.2);
// T1 = int
// T2 = double
// RT = double
Тип результата можно задать явно. Для этого нужно также указать предшествующие аргументы шаблона:
auto b = max_value<int, double, long double>(4, 7.2);
// RT = long double
Тип результата первым параметром
В шаблонах функций параметр с аргументом по умолчанию может располагаться перед параметрами, которые выводятся из аргументов функции:
template<typename RT = long, typename T1, typename T2>
RT max_value(T1 a, T2 b)
{
return b < a ? a : b;
}
Теперь тип результата удобно задавать первым:
max_value(4, 7.2); // RT = long, результат — 7
max_value<double>(4, 7.2); // RT = double, результат — 7.2
В обоих вызовах компилятор автоматически выводит T1 = int и T2 = double. Различается только тип возвращаемого результата.
Перегрузка шаблонной функции
Шаблон может иметь то же имя, что и обычная функция:
int max_value(int a, int b)
{
return b < a ? a : b;
}
template<typename T>
T max_value(T a, T b)
{
return b < a ? a : b;
}
Рассмотрим несколько вызовов:
max_value(7, 42); // Обычная функция
max_value(7.0, 42.0); // Шаблон с T = double
max_value('a', 'b'); // Шаблон с T = char
Аргументы типа int
max_value(7, 42);
Для этого вызова подходят оба кандидата:
- обычная функция
int max_value(int, int); - специализация шаблона
max_value<int>.
В обоих случаях типы аргументов точно соответствуют типам параметров. При таком выборе преимущество получает обычная функция.
Аргументы типа double
max_value(7.0, 42.0);
Обычная функция требует преобразования каждого аргумента:
double → int
Шаблон с T = double даёт точное совпадение, поэтому выбирается max_value<double>.
Аргументы типа char
max_value('a', 'b');
Шаблон с T = char принимает аргументы без преобразования их типа. Обычной функции требуется преобразование char в int, поэтому выбирается шаблон.
Перегрузка и аргументы разных типов
Для того же набора перегрузок рассмотрим вызов со смешанными типами:
max_value('a', 42.7);
В шаблоне оба параметра функции имеют один тип T:
template<typename T>
T max_value(T, T);
Но аргументы дают разные результаты вывода:
Первый аргумент → T = char
Второй аргумент → T = double
Вывести единый тип T не удаётся, поэтому шаблонный кандидат исключается.
Обычная функция остаётся допустимой:
int max_value(int, int);
Для её вызова оба аргумента преобразуются в int:
char → int
double → int
Таким образом, ошибка вывода аргумента шаблона для одного кандидата не обязательно означает ошибку всего вызова: может подойти другая перегрузка.
Вызовы max_value(), max_value<>() и max_value<T>()
Для набора из обычной функции и шаблона с одним параметром T три формы вызова имеют разный смысл:
max_value(7, 42);
max_value<>(7, 42);
max_value<double>(7, 42);
Обычный вызов
max_value(7, 42);
Рассматривается весь доступный набор перегрузок: обычные функции и шаблоны. В данном случае выбирается обычная функция с параметрами типа int.
Пустой список аргументов шаблона
max_value<>(7, 42);
Выбор выполняется среди шаблонных функций. Параметр T выводится автоматически:
T = int
Явное указание типа
max_value<double>(7, 42);
Тип задан заранее:
T = double
Оба аргумента типа int преобразуются в double.
Несколько шаблонных перегрузок
Шаблоны функций тоже можно перегружать:
template<typename T>
T max_value(T a, T b)
{
return b < a ? a : b;
}
template<typename T>
T* max_value(T* a, T* b)
{
return *b < *a ? a : b;
}
Первая версия сравнивает переданные значения. Вторая принимает указатели, сравнивает значения объектов, на которые они указывают, и возвращает один из указателей.
Рассмотрим вызов:
int a = 10;
int b = 20;
int* p1 = &a;
int* p2 = &b;
auto p = max_value(p1, p2);
Для него подходят обе версии.
Общий шаблон
template<typename T>
T max_value(T, T);
Компилятор выводит T = int*. После подстановки типы параметров и результата соответствуют:
int* max_value(int*, int*);
Шаблон для указателей
template<typename T>
T* max_value(T*, T*);
Компилятор выводит T = int. После подстановки получаются те же типы параметров и результата:
int* max_value(int*, int*);
Чтобы выбрать между кандидатами, компилятор сравнивает шаблоны по степени специализации. Версия с параметрами T* считается более специализированной и получает преимущество.
Этот механизм называется частичным упорядочением шаблонов функций — partial ordering of function templates.
В результате p указывает на b, поскольку значение 20 больше 10.
Перегрузка для C-строк
Сравнение указателей через < не сравнивает содержимое строк. Перегрузка с разыменованием *a и *b тоже не решает задачу: для указателей на символы она сравнивает только первые символы.
Для лексикографического сравнения C-строк можно добавить отдельную перегрузку:
#include <cstring>
char const* max_value(char const* a, char const* b)
{
return std::strcmp(b, a) < 0 ? a : b;
}
Теперь полный набор перегрузок выглядит так:
#include <cstring>
#include <string>
template<typename T>
T max_value(T a, T b)
{
return b < a ? a : b;
}
template<typename T>
T* max_value(T* a, T* b)
{
return *b < *a ? a : b;
}
char const* max_value(char const* a, char const* b)
{
return std::strcmp(b, a) < 0 ? a : b;
}
Выбор перегрузки для разных типов
int a = 7;
int b = 42;
auto m1 = max_value(a, b); // 42
std::string s1 = "hey";
std::string s2 = "you";
auto m2 = max_value(s1, s2); // "you"
int* p1 = &a;
int* p2 = &b;
auto m3 = max_value(p1, p2); // Указатель на b
char const* x = "hello";
char const* y = "world";
auto m4 = max_value(x, y); // Указатель на "world"
Для каждого вызова выбирается соответствующая перегрузка:
- для
intиstd::string— шаблон с параметрамиT; - для
int*— шаблон с параметрамиT*; - для
char const*— обычная функция соstd::strcmp.
Разные способы передачи и возврата значений
Сочетание перегрузок, возвращающих результат по ссылке и по значению, требует внимания к времени жизни объектов.
Рассмотрим двухаргументный шаблон с возвратом ссылки:
template<typename T>
T const& max_value(T const& a, T const& b)
{
return b < a ? a : b;
}
Для C-строк оставим перегрузку с передачей и возвратом указателей по значению:
#include <cstring>
char const* max_value(char const* a, char const* b)
{
return std::strcmp(b, a) < 0 ? a : b;
}
После этих перегрузок определим функцию для трёх значений:
template<typename T>
T const& max_value(T const& a,
T const& b,
T const& c)
{
return max_value(max_value(a, b), c);
}
Вызов с числами
auto m = max_value(7, 42, 68); // m = 68
В этом выражении результат копируется в m, пока временные аргументы ещё существуют.
Вызов с C-строками
char const* s1 = "frederic";
char const* s2 = "anica";
char const* s3 = "lucas";
auto m = max_value(s1, s2, s3); // Ошибка времени жизни
Здесь трёхаргументная функция возвращает висячую ссылку. Причина связана с результатами вложенных вызовов.
Откуда возникает висячая ссылка
Разберём выражение внутри трёхаргументного шаблона:
return max_value(max_value(a, b), c);
Для аргументов типа char const* происходит следующее:
- Внутренний вызов
max_value(a, b)выбирает перегрузку для C-строк и возвращает указатель по значению. - Внешний двухаргументный вызов также выбирает перегрузку для C-строк и возвращает указатель по значению.
- Трёхаргументный шаблон должен вернуть
T const&, поэтому возвращаемая ссылка связывается с временным объектом-указателем.
В данном случае T = char const*, поэтому тип возвращаемой ссылки:
char const* const&
В C++17 время жизни временного объекта не продлевается при возврате такой ссылки. После завершения выражения в return ссылка становится висячей. Её использование для инициализации m приводит к неопределённому поведению.
Возврат результата по значению
В рассмотренном примере проблему можно устранить, возвращая результат по значению во всех перегрузках:
#include <cstring>
template<typename T>
T max_value(T a, T b)
{
return b < a ? a : b;
}
char const* max_value(char const* a, char const* b)
{
return std::strcmp(b, a) < 0 ? a : b;
}
template<typename T>
T max_value(T a, T b, T c)
{
return max_value(max_value(a, b), c);
}
Перегрузка для C-строк объявлена до определения трёхаргументного шаблона, поэтому доступна при выборе вложенных вызовов.
Теперь результат для C-строк — копия выбранного указателя:
char const* s1 = "frederic";
char const* s2 = "anica";
char const* s3 = "lucas";
auto m = max_value(s1, s2, s3); // Указатель на "lucas"
Порядок объявлений
Набор перегрузок, доступных внутри шаблона, зависит от места его определения.
Рассмотрим пример, в котором обычная функция объявлена после трёхаргументного шаблона:
#include <iostream>
template<typename T>
T max_value(T a, T b)
{
std::cout << "template\n";
return b < a ? a : b;
}
template<typename T>
T max_value(T a, T b, T c)
{
return max_value(max_value(a, b), c);
}
int max_value(int a, int b)
{
std::cout << "int overload\n";
return b < a ? a : b;
}
Вызов:
max_value(47, 11, 33);
внутри трёхаргументного шаблона использует двухаргументный шаблон. На экран выводится:
template
template
Почему поздняя перегрузка не выбирается
Причина связана с двухфазным поиском имён.
В момент определения трёхаргументного шаблона компилятор уже видит двухаргументный шаблон:
template<typename T>
T max_value(T, T);
Обычная функция:
int max_value(int, int);
объявлена позже.
При инстанцировании зависимых вызовов набор кандидатов может дополняться через ADL — поиск, зависящий от типов аргументов.
У типа int нет связанных пространств имён, поэтому позднее объявленная глобальная перегрузка в этот набор не попадает.
Как включить перегрузку в набор кандидатов
Чтобы обычная функция участвовала в выборе внутри трёхаргументного шаблона, достаточно объявить её раньше:
#include <iostream>
int max_value(int a, int b);
template<typename T>
T max_value(T a, T b)
{
std::cout << "template\n";
return b < a ? a : b;
}
template<typename T>
T max_value(T a, T b, T c)
{
return max_value(max_value(a, b), c);
}
int max_value(int a, int b)
{
std::cout << "int overload\n";
return b < a ? a : b;
}
Теперь тот же вызов:
max_value(47, 11, 33);
выводит:
int overload
int overload
Предварительное объявление делает обычную функцию доступной при определении шаблона. При последующем выборе перегрузки для аргументов типа int она получает преимущество.