Вывод аргумента шаблона для параметра указателя переменной функции — обработка неоднозначных случаев

Рассмотрим следующий код:

#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? Каково правильное поведение в соответствии со стандартом для каждого случая?

8

Решение

Насколько я могу судить, 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; Я думаю, что они должны, по крайней мере, включить примечание о невнятном контексте и причине, по которой это происходит.

7

Другие решения


По вопросам рекламы [email protected]