C++函数类型模板参数推导规则

C++ function type template parameter deduction rule

本文关键字:规则 参数 函数 类型 C++      更新时间:2023-10-16

以下代码构建时

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.

有趣的部分不是关于歧义错误本身(这不是这里的主要关注点)。有趣的是,当仅使用函数名称调用 fun 时,第一个fun的模板参数F被解析为纯函数类型double (double),而第二个fun的模板参数F被解析为更期望的double (*)(double)函数指针类型。

但是,当我们将fun(Test)的调用更改为fun(&Test)以显式获取函数的地址(或显式函数指针)时,则两者都fun解析模板参数Fdouble (*)(double)

这种行为似乎是所有Clang和GCC(以及Visual Studio 2013)的常见行为。

那么问题来了:在我的示例代码中给出的形式中,模板函数的函数类型模板参数推导规则是什么?

PS:如果我们添加另一个fun实例来获取F* f,那么似乎重载规则只是决定选择这个版本,并且根本没有报告歧义(即使,正如我已经说过的,歧义不是前面最大的问题,但在最后一种情况下,我确实想知道为什么第三个版本是这里的最佳匹配?

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

可能你已经想通了,因为你发布这个问题已经快 3 年了。但如果你还没有,我会给出我的答案。

有趣的是,当仅使用函数名称调用fun时,第一个fun的模板参数F被解析为纯函数类型double (double),而第二个fun的模板参数F被解析为更预期的double (*)(double)函数指针类型。

首先,请记住,数组和函数很奇怪,因为数组可以隐式衰减为指向其第一个元素的指针,而函数可以隐式衰减为函数指针。尽管语法上有效,但函数参数实际上不能是数组或函数类型,而是指针,这意味着函数参数可以使用数组或函数的类型编写,但编译器将此类类型视为指针。例如,请看下面的代码:

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)以显式获取函数的地址(或显式函数指针)时,两者都很有趣地将模板参数 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):

  1. Tconst T
  2. TT &
  3. 从数组或函数类型到其相应的指针类型(开头提到的衰减)。

编辑#2:似乎我没有使用正确的措辞,所以要明确我的意思是函数模板是创建函数的模板,模板函数是由模板创建的函数

可能其他人可以比我更好地解释这一点,但这就是我的理解方式(没有引用标准,抱歉)。

不能复制函数类型的变量,所以在template <typename F> void fun(F f)中,F不能有函数类型。

但是,函数类型的变量可以转换为指向函数类型的指针(这称为"衰减",就像数组到指针的转换一样),因此当将函数类型与template <typename F> void fun(F f)匹配时,F必须是指向函数的指针。

在处理对函数类型的引用时,函数到指针的衰减不会发生(我在标准中找不到这个,但它应该与对数组的引用规则一起描述),所以当匹配模板<typename F> void fun(const F& f)时,F是函数类型(参数的类型是对函数的引用)。