Вывод аргумента шаблона для параметра указателя переменной функции - обработка неоднозначных случаев
Рассмотрим следующий код:
#include <iostream>
void f(int) { }
void f(int, short) { }
template<typename... Ts> void g(void (*)(Ts...))
{
std::cout << sizeof...(Ts) << '\n';
}
template<typename T, typename... Ts> void h(void (*)(T, Ts...))
{
std::cout << sizeof...(Ts) << '\n';
}
int main()
{
g(f); // #1
g<int>(f); // #2
h(f); // #3
h<int>(f); // #4
}
Намерение состоит в том, чтобы попробовать каждую из строк в теле main()
по отдельности. Я ожидал, что все четыре вызова будут неоднозначными и приведут к ошибкам компилятора.
Я проверил код на:
- Clang 3.6.0 и GCC 4.9.2, оба с использованием
-Wall -Wextra -pedantic -std=c++14
(-std=c++1y
для GCC) - одинаковое поведение во всех этих случаях, за исключением незначительных различий в формулировке сообщений об ошибках; - Visual C++ 2013 Update 4 и Visual C++ 2015 CTP6 - опять то же самое поведение, поэтому я буду называть их "MSVC" в будущем.
Clang и GCC:
#1
: Ошибка компилятора с ошибочным сообщением, в основномno overload of 'f' matching 'void (*)()'
, Какие? Откуда взялась декларация без параметров?#3
: Ошибка компилятора с другим непонятным сообщением:couldn't infer template argument 'T'
, Из всех вещей, которые могут там потерпеть неудачу, вывести аргументT
будет последним, которого я ожидаю...#2
а также#4
: Компилируется без ошибок и предупреждений и выбирает первую перегрузку.
Для всех четырех случаев, если мы устраняем одну из перегрузок (любую), код прекрасно компилируется и выбирает оставшуюся функцию. Это выглядит как несоответствие в Clang и GCC: в конце концов, если вычет будет успешным для обеих перегрузок по отдельности, как один может быть выбран из другого в случаях #2
а также #4
? Разве они не идеальные пары?
Теперь MSVC:
#1
,#3
а также#4
: Ошибка компилятора с хорошим сообщением:cannot deduce template argument as function argument is ambiguous
, Вот о чем я говорю! Но ждать...#2
: Компилируется без ошибок и предупреждений и выбирает первую перегрузку. Пробуя две перегрузки по отдельности, совпадает только первая. Второй генерирует ошибку:cannot convert argument 1 from 'void (*)(int,short)' to 'void (*)(int)'
, Уже не так хорошо.
Чтобы уточнить, что я ищу с делом #2
Вот что говорится в стандарте (N4296, первый черновик после C++14 final) в [14.8.1p9]:
Вывод аргументов шаблона может расширить последовательность аргументов шаблона, соответствующую пакету параметров шаблона, даже если последовательность содержит явно указанные аргументы шаблона.
Похоже, эта часть не совсем работает в MSVC, поэтому она выбирает первую перегрузку для #2
,
Пока что похоже, что MSVC, хотя и не совсем правильно, но, по крайней мере, относительно последовательна. Что происходит с Clang и GCC? Каково правильное поведение в соответствии со стандартом для каждого случая?
1 ответ
Насколько я могу судить, Clang и GCC правы во всех четырех случаях в соответствии со стандартом, хотя их поведение может показаться нелогичным, особенно в случаях #2
а также #4
,
Есть два основных этапа анализа вызовов функций в примере кода. Первый - это вывод и замена аргументов шаблона. Когда это завершается, это приводит к объявлению специализации (либо g
или же h
) где все параметры шаблона были заменены фактическими типами.
Затем второй шаг пытается соответствовать f
перегружает фактический параметр указатель на функцию, который был создан на предыдущем шаге. Наилучшее совпадение выбирается по правилам в [13.4] - Адрес перегруженной функции; в наших случаях это довольно просто, поскольку среди перегрузок нет шаблонов, поэтому у нас либо одно идеальное соответствие, либо его нет вообще.
Ключевым моментом для понимания того, что здесь происходит, является то, что двусмысленность на первом этапе не обязательно означает, что весь процесс провалился.
Приведенные ниже цитаты взяты из N4296, но содержание не изменилось с C++ 11.
[14.8.2.1p6] описывает процесс вывода аргумента шаблона, когда параметр функции является указателем на функцию (выделено мной):
Когда P является типом функции, указатель на тип функции или указатель на тип функции-члена:
- Если аргумент является набором перегрузок, содержащим один или несколько шаблонов функций, параметр обрабатывается как не выводимый контекст.
- Если аргумент является набором перегрузки (не содержащим шаблонов функций), попытка выведения пробного аргумента выполняется с использованием каждого из членов набора. Если вывод выполняется только для одного из членов набора перегрузки, этот элемент используется в качестве значения аргумента для вывода. Если вывод выполняется успешно для более чем одного члена набора перегрузки, параметр обрабатывается как невыгруженный контекст.
Для полноты [14.8.2.5p5] поясняется, что то же правило применяется, даже когда нет совпадений:
Неведуемые контексты: [...]
- Параметр функции, для которого невозможно сделать вывод аргумента, потому что аргумент связанной функции является функцией или набором перегруженных функций (13.4), и применяется одно или несколько из следующего:
- более чем одна функция соответствует типу параметра функции (что приводит к неоднозначному выводу), или
- ни одна функция не соответствует типу параметра функции, или
- набор функций, предоставляемых в качестве аргумента, содержит один или несколько шаблонов функций.
Таким образом, нет серьезных ошибок из-за неоднозначности в этих случаях. Вместо этого все параметры шаблона во всех наших случаях находятся в невыполненных контекстах. Это объединяется с [14.8.1p3]:
[...] Пакет параметров конечного шаблона (14.5.3), не выведенный иначе, будет выведен в пустую последовательность аргументов шаблона. [...]
В то время как использование слова "вывод" здесь сбивает с толку, я понимаю, что это означает, что пакет параметров шаблона установлен в пустую последовательность, если никакие элементы не могут быть выведены для него из какого-либо источника, и для него явно не указаны аргументы шаблона.,
Теперь сообщения об ошибках от Clang и GCC начинают иметь смысл (сообщение об ошибке, которое имеет смысл только после того, как вы поймете, почему ошибка возникает, не совсем является определением полезного сообщения об ошибке, но я думаю, что это лучше, чем ничего):
#1
: ПосколькуTs
пустая последовательность, параметрg
специализация действительноvoid (*)()
в этом случае. Затем компилятор пытается сопоставить одну из перегрузок с типом назначения и завершается неудачно.#3
:T
появляется только в не выводимом контексте и явно не указан (и это не пакет параметров, поэтому он не может быть "пустым"), поэтому объявление специализации не может быть создано дляh
отсюда и сообщение.
Для случаев, которые компилируются:
#2
:Ts
не может быть выведено, но для него явно указан один параметр шаблона, поэтомуTs
являетсяint
, делаяg
параметр специализацииvoid (*)(int)
, Перегрузки затем сопоставляются с этим типом назначения, и выбирается первый.#4
:T
явно указан какint
а такжеTs
пустая последовательность, такh
параметр специализацииvoid (*)(int)
так же, как и выше.
Когда мы устраняем одну из перегрузок, мы устраняем неоднозначность во время вывода аргументов шаблона, поэтому параметры шаблона больше не находятся в недедуцированных контекстах, что позволяет их выводить в соответствии с оставшейся перегрузкой.
Быстрая проверка заключается в том, что добавление третьей перегрузки
void f() { }
позволяет случай #1
компилировать, что согласуется со всем вышеперечисленным.
Я предполагаю, что таким образом были определены вещи, позволяющие получать аргументы шаблона, включенные в параметры указателя на функцию, из других источников, таких как другие аргументы функции или явно заданные аргументы шаблона, даже если вычет аргумента шаблона не может быть сделан на основе сам параметр указатель на функцию. Это позволяет создавать объявление специализации шаблона функции в большем количестве случаев. Поскольку перегрузки затем сопоставляются с параметром синтезированной специализации, это означает, что у нас есть способ выбрать перегрузку, даже если вывод аргумента шаблона является неоднозначным. Весьма полезно, если это то, что вам нужно, ужасно запутывая в некоторых других случаях - на самом деле ничего необычного.
Самое смешное, что сообщение об ошибке MSVC, хотя и, по-видимому, приятное и полезное, на самом деле вводит в заблуждение #1
несколько, но не совсем полезно для #3
и неверно для #4
, Кроме того, его поведение для #2
является побочным эффектом отдельной проблемы в ее реализации, как объяснено в вопросе; если бы не это, он, вероятно, выдает такое же неправильное сообщение об ошибке для #2
также.
Это не значит, что мне нравятся сообщения об ошибках Clang и GCC для #1
а также #3
; Я думаю, что они должны, по крайней мере, включить примечание о невнятном контексте и причине, по которой это происходит.