Ещё о перегрузке шаблонных функций (Partial ordering)

[1] 16.2.2 Partial Ordering of Overloaded Function Templates

Рассмотрим пример:

#include <iostream>
template<typename T>
int f(T)
{
    return 1;
}

template<typename T>
int f(T*)
{
    return 2;
}

int main()
{
    std::cout << f<int*>((int*)nullptr); // calls f<T>(T)
    std::cout << f<int>((int*)nullptr); // calls f<T>(T*)
}

Программа имеет вывод : 12.

Чтобы понять почему, давайте рассмотрим вызов f<int*>((int*)nullptr) поподробнее. Синтаксис f<int*> указывает, что   мы хотим подставить в первый шаблонный параметр шаблона f() указатель int* не полагаясь на вывод шаблонного аргумента. В этом случае имеется более одного шаблона f(), и поэтому создаётся набор перегрузки, состоящий из двух функций, генерированных из шаблонов: f<int*>(int*) (генерированный из первого шаблона) и f<int*>(int**) (генерированный из второго шаблона). Аргумент вызова (int*)nullptr имеет тип int*. Это соответствует только функции генерированной из первого шаблона,  и значит, эта функция будет вызвана.
 Для второго вызова создаваемый набор перегрузки будет содержать f<int>(int) (генерированный из первого шаблона) и f<int>(int*) (генерированный из второго шаблона), поэтому второму вызову соответствует второй шаблон.

Однако, функция будет выбрана даже без явного предоставления шаблонных аргументов. В этом случае в игру вступает вывод шаблонных аргументов. Давайте немного изменим функцию main()

int main()
{

   std::cout << f(0); // calls f<T>(T)
   std::cout << f(nullptr); // calls f<T>(T)
   std::cout << f(
(int*)nullptr); 
// calls f<T>(T*)


}  

Рассмотрим первый вызов, f(0): Тип вызываемого аргумента - int, что соответствует типу параметра в первом шаблоне, если мы подставим int вместо T. Однако, тип параметра во втором шаблоне всегда указатель и поэтому, после выведения, только  функция, генерированная из первого шаблона окажется кандидатом на вызов. Этот случай перегрузки тривиален. 
То же приложимо ко второму вызову f(nullptr): Тип аргумента -  std::nullptr_t, который соответствует только первому шаблону.
Третий вызов f((int*)nullptr) более интересен : Вывод аргументов успешен для обоих шаблонов, получая функции f<int*>(int*) и f<int>(int*). С традиционной перспективы разрешения перегрузки, обе функции одинаково хороши для вызова с аргументом  int* , что позволяет предположить неоднозначность. Однако, в этом случае дополнительный критерий разрешения перегрузки вступает в игру: Выбирается функция, генерированная из "более специализированного" шаблона. Здесь (как мы скоро увидим)  второй шаблон рассматривается как "более специализированный" и поэтому вывод нашего примера будет :112

Глава 16.2.3 Formal Ordering Rules выглядит излишне формализованной, поэтому далее  выдержки из перевода статьи [2]

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

template<class T> void f(T); //(1)

template<class T> void f(T const*); //(2) 

.....
int const* p = nullptr;f(p); 

Мы ожидаем, что f(p) вызовет (2), поскольку p имеет тип int const* (и правильно ожидаем). В порядке принятия решения, что (2) более специализирована чем (1), компилятор должен следовать правилам частичного упорядочивания шаблонных функций. Давайте посмотрим, что говорит стандарт по этому поводу.

[3]
[temp.func.order]/2:

Partial ordering selects which of two function templates is more specialized than the other by
transforming each template in turn (see next paragraph) and performing template argument
deduction using the function type.


Вроде бы не сложно, но вот только "next paragraph" достаточно сложен для понимания, поэтому посмотрим по шагам, что компилятор делает в нашем примере.
  1. Трансформация (1),
  2. Выполнение выведения параметров шаблона в (2) с трансформированным шаблоном из шага 1,
  3. Трансформирование (2),
  4. Выполнение выведения параметров шаблона в (1) с трансформированным шаблоном из шага 3.
Если одно и только одно из выведений успешно, то шаблон, с которым выведение было выполнено, является "более специализированным", чем другой.


Шаг 1. Правила устанавливают, что для каждого шаблонного параметра мы создаем некоторый уникальный тип для использования вместо него (шаблонные шаблонные и не-типовые параметры пропущены в изложении без потери общего смысла). Давайте назовем этот уникальный тип type_0. Мы может притвориться, что этот воображаемый тип определен где-то как-то так class type_0{}. Теперь мы берем нашу шаблонную функцию template<class Tvoid f(T) и подставляем type_0 вместо T. Это дает нам 
void f(type_0). Трансформация завершена.

Шаг 2. Теперь после трансформации template <class T> void f(T) в void f(type_0) , мы будем выполнять дедукцию (выведение типа шаблонного параметра) в (2) используя трансформированный тип функции. Чтобы сделать это, мы вообразим вызов (2), где аргументы имеют тип параметров для (1). А именно, это будет выглядеть так.

template <class T> void func_2(T const*);

func_2(type_0{}); //derived from void f(type_0) 
 


Будет ли этот вызов успешен? Спросим у компилятора (GCC 8.2.0).

prog.cc: In function 'int main()':
prog.cc:27:20: error: no matching function for call to 'func_2(type_0)'
     func_2(type_0{}); //derived from void f(type_0)
                    ^
prog.cc:19:25: note: candidate: 'template<class T> void func_2(const T*)'
 template <class T> void func_2(T const*) {};
                         ^~~~~~
prog.cc:19:25: note:   template argument deduction/substitution failed:
prog.cc:27:20: note:   mismatched types 'const T*' and 'type_0'
     func_2(type_0{}); //derived from void f(type_0)
Итак, выведение из (1) во (2) потерпело провал, поскольку воображаемый тип type_0 не может быть использован для выведения  const T*.

Шаг 3. Давайте попробуем из (2) в (1). Ещё раз мы будем трансформировать (2) из template <class T> void f(T const*) в void f(type_0 const*)

Шаг 4. Попытаемся вывести

template <class T> void func_1(T);

type_0 const* arg = nullptr;


func_1(arg); 
 


Успешно, поскольку type_0 const* может быть использован для выведения T. Поскольку выведение из (1) во (2) неудачно, а из (2) в (1) успешно, (2) "более специализирована" чем (1) и будет выбрана в разрешении перегрузки.

Далее tartanllama приводит ещё 2 примера перегрузки

template<class T> void g(T); //(1)

template<class T> void g(T&); //(2)

int i = 0;

g(i); 

и

template<class T>struct identity { using type = T; };

template<class T>struct A{};

template<class T, class U> void h(typename identity<T>::type, U); //(1)

template
<class T, class U> void h(T, A<U>);
h
<int>(0,A<void>{});
//(2)

 В первом случае неоднозначность заключается в том, что подходят обе функции g(), а во втором - что не подходит ни одна из функций h().


[1] https://drive.google.com/open?id=16jdO4oPren7Lfj7c3XMyJsUrD7MJYaov
[2] https://blog.tartanllama.xyz/function-template-partial-ordering/
[3] http://eel.is/c++draft/temp.func.order

Комментарии

Популярные сообщения из этого блога

Полезные новации С++17. Выражения свертки (Fold expressions)

Перегрузка и специализация шаблонных функций.

Поддержка декомпозиции при объявлении (structural bindings) для классов