使用
构建时的以下代码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时,1}}被解析为更期望的fun(Test)
函数指针类型。
但是,当我们将fun(&Test)
的调用更改为fun
以显式获取函数的地址(或显式函数指针)时,两个F
都会解析模板参数double (*)(double)
为fun
!
这种行为似乎是所有Clang和GCC(以及Visual Studio 2013)的常见行为。
那么问题是:在我的示例代码中给出的表单中模板函数的函数类型模板参数推导规则是什么?
PS:如果我们添加F* f
的另一个实例来获取template <typename F>
void fun(F* f)
{
}
,那么似乎重载规则决定选择此版本,并且根本没有报告歧义(即使,因为我已经说过,歧义并不是最早的问题,但在最后一种情况下,我确实想知道为什么第三个版本是最匹配的?)
{{1}}
答案 0 :(得分:5)
可能你已经弄清楚了,因为你发布这个问题差不多已经3年了。但是如果你没有,我会给出答案。
有趣的是,第一个
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)
以显式获取函数的地址(或显式函数指针)时,两者都将模板参数F解析为fun(&Test)
!
好了,既然你明确地将函数指针类型作为参数传递(通过取double (*)(double)
的地址),推导出的Test
类型必须有一个指针。 但是,第一个函数参数的引用和常量不会被忽略。运行f
时,第一个函数的推导类型fun(&Test)
将为f
,第二个函数的推导类型double (* const &) (double)
将为f
PS:如果我们添加
double (*) (double)
的另一个实例来获取fun
,那么似乎重载规则决定选择此版本,并且根本没有报告歧义(即使,因为我已经说过,歧义并不是最早的问题,但在最后一种情况下,我确实想知道为什么第三个版本是最匹配的?)
(我删除了该部分的先前答案,请参阅下文)
修改强>:
在添加第三个函数(F* f
)时,我对如何不再存在歧义的问题给出了一个非常草率的答案。我希望下面是一个明确的答案。
在函数模板的情况下解析哪个函数的规则首先要找出给定参数的模板特化集。其原因是消除导致替换失败的功能模板作为候选。然后,基于从参数到参数的转换,从非模板函数的候选池和有效模板特化中消除了更差的匹配。如果非模板和模板函数同样匹配,则拾取非模板。如果多个模板函数同样匹配良好,则使用partial ordering rules来消除不太专业的函数模板。如果一个作为最专业的功能模板闪耀,那么它就解决了;另一方面,如果两者都不是更专业,那么编译器会发出歧义错误。不用说,如果找不到有效的候选人,则会再次发出错误。
现在让我们再次指出参数fun(F * f)
的模板特化。如上所述,在模板类型推导之后,第一个函数模板的模板特化为Test
,第二个函数模板的模板特化为void fun(double (&f) (double) )
。基于参数类型void fun(double (*f) (double) )
所需的转换到候选模板函数&#39;参数类型double (double)
和double (&) (double)
分别被视为完全匹配,因为只需要进行简单的转换。因此,采用偏序规则来区分哪一个更专业。事实证明,两者都没有,因此产生歧义错误。
当您添加第三个功能模板(double (*) (double)
)时,模板类型推导会将模板专精化生成为void fun(F * f)
。与之前相同,所有三个模板函数都具有相同的匹配性(实际上它们完全匹配)。正因为如此,部分排序规则被用作最后的手段,事实证明第三个功能模板更专业,因此它被拾取。
关于普通转换的注意事项:虽然不完整,但从参数类型到参数类型的以下转换被视为简单转换(给定类型void fun(double (*f)(double)
):
T
到T
const T
到T
编辑#2 :似乎我可能没有使用正确的措辞,所以要明确我的意思功能模板是一个创建功能的模板以及模板功能由模板创建的功能。
答案 1 :(得分: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
是一种函数类型(参数的类型是函数引用)。