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

Параметру шаблона можно задать аргумент по умолчанию:

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* происходит следующее:

  1. Внутренний вызов max_value(a, b) выбирает перегрузку для C-строк и возвращает указатель по значению.
  2. Внешний двухаргументный вызов также выбирает перегрузку для C-строк и возвращает указатель по значению.
  3. Трёхаргументный шаблон должен вернуть 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 она получает преимущество.