Подтвердить что ты не робот

Правило вычитания параметра шаблона типа С++

Следующий код, построенный с помощью

clang -Wall main.cpp -o main.o

генерирует следующую диагностику (после кода):

template <typename F>
void fun(const F& f)
{

}

template <typename F>
void fun(F f)
{

}

double Test(double d) { return d; }

int main(int argc, const char * argv[])
{
    fun(Test);

    return 0;
}

Диагностика:

main.cpp:17:5: error: call to 'fun' is ambiguous
    fun(Test);
    ^~~
main.cpp:2:6: note: candidate function [with F = double (double)]
void fun(const F& f)
     ^
main.cpp:8:6: note: candidate function [with F = double (*)(double)]
void fun(F f)
     ^
1 error generated.

Интересная часть заключается не в самой ошибке двусмысленности (что не является главной проблемой здесь). Интересная часть состоит в том, что параметр шаблона F первого fun разрешен как чистый тип функции double (double), тогда как параметр шаблона F второго fun разрешен как более ожидаемый double (*)(double) тип указателя функции, когда fun вызывается только с именем функции.

Однако, когда мы меняем вызов fun(Test) на fun(&Test), чтобы явно использовать адрес функции (или явный указатель функции), то оба fun разрешают параметр шаблона F равным double (*)(double)!

Такое поведение, по-видимому, является общим для всех Clang и GCC (и Visual Studio 2013).

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

PS: если мы добавим еще один экземпляр fun, чтобы взять F* f, то, похоже, правило перегрузки просто решает выбрать эту версию, и никакой двусмысленности вообще не сообщается (хотя, как я уже сказал, двусмысленность не является самой большой проблемой ранее, но в этом последнем случае я действительно удивляюсь, почему третья версия здесь лучше всего подходит?)

template <typename F>
void fun(F* f)
{
}
4b9b3361

Ответ 1

Вероятно, другие могут объяснить это лучше меня, но я так понимаю (никаких цитат из Стандарта, извините).

Невозможно скопировать переменную типа функции вокруг, поэтому в template <typename F> void fun(F f) F не может иметь тип функции.

Однако переменная типа функции может быть преобразована в указатель на тип функции (это называется "распад", как преобразование между массивами и указателями), поэтому при сопоставлении типа функции с template <typename F> void fun(F f), F должен быть указателем на функцию.

При обращении к типу функции распад функции-к-указателю не может произойти (я не могу найти это в стандарте, но он должен быть описан вместе с правилами ссылки на массив), поэтому при сопоставлении template <typename F> void fun(const F& f), F - это тип функции (и тип параметра - это ссылка на функцию).

Ответ 2

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

Интересная часть состоит в том, что параметр шаблона F первого fun разрешен как чистый тип функции double (double), тогда как параметр шаблона F второго fun разрешен как более ожидаемый тип указателя функции double (*)(double), когда fun вызывается только с именем функции.

Прежде всего, имейте в виду, что массивы и функции странны в том, что массив может неявно распадаться на указатель на свой первый элемент, а функции могут неявно распадаться на указатели на функции. И хотя синтаксически корректно, функциональные параметры фактически не могут иметь тип массива или функции, а указатели, то есть параметры функции могут быть записаны с типом массива или функций, но компиляторы рассматривают такие типы, как указатели. Например, посмотрите на код ниже:

int val [3]; //type of val is 'int [3]'
int * pval = val; //type of pval is 'int *'
                  //the assignment is correct since val can decay into 'int *'

double foo(double); //type of foo is 'double (double)'
double (*pfoo) (double); // type of pfoo is 'double (*)(double)'
pfoo = foo; //correct since functions can decay into function pointers.

void bar(int x []); // syntax is correct 
                    // but compilers see the type of x as 'int *'

void bar(int x(int));// again syntax is correct
                     // but compilers see the type of x as 'int (*)(int)'

Однако все становится еще страннее, когда параметр функции имеет ссылочный тип. Считается, что параметр функции, у которого есть тип ссылки на массив/функцию, имеет тип ссылки на массив/функцию, не тип указателя. Например:

void bar(int (& x)[2]); //type of x is now 'int (&) [2]'
void bar(int (& x)(int)); //type of x is now 'int (&)(int)'

Что касается вашего первого вопроса, так как тип параметра в первой функции вашего (fun(const F& f)) включает ссылку, тип F будет выведен как ссылка на функцию, когда функция передается как аргумент; точнее, выводимый тип F будет double (&) (double). С другой стороны, поскольку второй тип параметра функции не включает ссылку (fun(F f)), компиляторы неявно выводят тип F в качестве указателя функции (выводимый тип F будет double (*)(double)), когда функция передается в качестве аргумента.

Однако, когда мы меняем вызов fun(Test) на fun(&Test), чтобы явно принять адрес функции (или явный указатель функции), тогда оба fun разрешают параметр шаблона F double (*)(double)!

Итак, теперь, поскольку вы явно передаете тип указателя функции в качестве аргумента (выбирая адрес Test), выведенный тип F должен иметь указатель. Однако ссылка и постоянство первого параметра функции не игнорируются. Когда выполняется fun(&Test), выводимый тип F для первой функции будет double (* const &) (double), а выводимый тип F для второй функции будет double (*) (double).

PS: если мы добавим еще один экземпляр fun, чтобы взять F* f, то кажется, что правило перегрузки только решает выбрать эту версию, и никакой двусмысленности не сообщается вообще (хотя, как я уже сказал, двусмысленность не является самой большой проблемой ранее, но в этом последнем случае я действительно удивляюсь, почему третья версия здесь лучше всего подходит?)

(я удалил свой предыдущий ответ для этой части, см. ниже)

ИЗМЕНИТЬ: Я дал очень небрежный ответ на вопрос о том, почему нет никакой двусмысленности, когда добавляется третья функция (fun(F * f)). Ниже ясный ответ, надеюсь.

Правило для решения, какую функцию подбирать в случае шаблонов функций, - это сначала выяснить набор специализированных шаблонов для данного аргумента. Причиной этого является устранение шаблонов функций, которые приводят к сбою замены в качестве кандидата. Затем, основываясь на преобразованиях из аргумента в параметры, худшие совпадения исключаются из пула кандидатов не-шаблонных функций и действительных специализированных шаблонов. Если не-шаблон и функции шаблона одинаково хороши, не-шаблон подбирается. Если более чем одна функция шаблона одинаково хороша, тогда правила частичного заказа используются для устранения менее специализированных шаблонов функций. Если один светит как наиболее специализированный шаблон функции, то он разрешен; с другой стороны, если ни один из них не является более специализированным, то компилятор выдает ошибку неоднозначности. Излишне говорить, что если не найдено действительного кандидата, ошибка выдается снова.

Теперь давайте снова укажем специализированные шаблоны для аргумента Test. Как упоминалось выше, после вычитания типа шаблона специализация шаблона первого шаблона функции void fun(double (&f) (double) ), а шаблон второй функции - void fun(double (*f) (double) ). На основе преобразований, необходимых для типа аргумента double (double), для типов параметров шаблона кандидата-шаблона double (&) (double) и double (*) (double) соответственно, оба они считаются точными, так как требуются только тривиальные преобразования. Поэтому для определения того, какой из них более специализирован, используются правила частичного упорядочения. Оказывается, что ни то, ни другое, поэтому возникает ошибка двусмысленности.

Когда вы добавили третий шаблон функции (void fun(F * f)), вывод типа шаблона задает специализацию шаблона как void fun(double (*f)(double). Как и раньше, все три функции шаблона одинаково хороши (на самом деле они точно совпадают). Из-за этого снова применяются правила частичного упорядочения в качестве последнего средства, и оказывается, что третий шаблон функции более специализирован и, следовательно, он подбирается.

Примечание о тривиальных преобразованиях: хотя и не завершено, следующие преобразования из типа аргумента в тип параметра считаются тривиальным преобразованием (с учетом типа T):

  • от T до const T
  • от T до T &
  • или из массива или типов функций в соответствующие им типы указателей (распады, упомянутые в начале).

РЕДАКТИРОВАТЬ № 2. Кажется, что я не могу использовать правильные формулировки, поэтому просто чтобы понять, что я имею в виду под шаблоном функции, является шаблон, который создает функции и функция шаблона - функция, созданная шаблоном.