【问题标题】:C++ function type template parameter deduction ruleC++函数类型模板参数推导规则
【发布时间】:2014-04-17 01:56:31
【问题描述】:

以下代码构建时使用

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 的模板参数F 被解析为double (double) 的纯函数类型,而第二个fun 的模板参数F 被解析为更预期的double (*)(double) 函数指针类型,当 fun 仅使用函数名调用时。

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

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

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

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

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

【问题讨论】:

  • 关于我最后一个“奖励”问题,我认为它是完全匹配的,所以如果存在,它总是被选中。

标签: c++ templates function-pointers


【解决方案1】:

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

有趣的是第一个fun的模板参数F被解析为double (double)的纯函数类型,而第二个fun的模板参数F被解析为当 fun 仅使用函数名调用时,更期望的 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&amp; f))中的参数类型包含引用,因此在传递函数时,f 的类型将被推断为对函数的引用作为论据;更准确地说,f 的推断类型将是double (&amp;) (double)。另一方面,由于第二个函数的参数类型不包含引用(fun(F f)),编译器隐含地将f 的类型推断为函数指针(f 的推断类型将是double (*)(double)),当函数作为参数传递时。

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

好吧,既然您明确地将函数指针类型作为参数传递(通过获取Test 的地址),f 的推导类型必须有一个指针。 但是,第一个函数的参数的引用和恒定性并没有被忽略。运行fun(&amp;Test)时,第一个函数f的推导类型为double (* const &amp;) (double),第二个函数f的推导类型为double (*) (double)

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

(我删除了我之前对该部分的回答,请参考下文)

编辑: 对于添加第三个函数(fun(F * f))时如何不再有歧义的问题,我给出了一个非常草率的答案。我希望下面是一个明确的答案。

在函数模板的情况下,决定选择哪个函数的规则是首先找出给定参数的模板特化集。这样做的原因是为了消除导致替换失败的函数模板作为候选。然后基于从参数到参数的转换,从非模板函数的候选池和有效的模板特化中消除更差的匹配。如果一个非模板和一个模板函数同样匹配,那么非模板就会被选中。如果多个模板函数同样匹配,则使用partial ordering rules 来消除不太专业的函数模板。如果一个作为最专业的功能模板闪耀,那么它就解决了;另一方面,如果两者都不是更专业的,则编译器会发出歧义错误。不用说,如果没有找到有效的候选人,就会再次发出错误。

现在让我们再次指出参数Test 的模板特化。如上所述,在模板类型推导后,第一个函数模板的模板特化为void fun(double (&amp;f) (double) ),第二个函数模板的模板特化为void fun(double (*f) (double) )。根据从参数类型double (double) 到候选模板函数的参数类型double (&amp;) (double)double (*) (double) 所需的转换,它们都被认为是完全匹配的,因为只需要微不足道的转换。因此,部分排序规则被用来区分哪一个更专业。事实证明两者都不是,所以发出了歧义错误。

当您添加第三个函数模板 (void fun(F * f)) 时,模板类型推导会生成模板特化为 void fun(double (*f)(double)。和之前一样,所有三个模板函数都是同样好的匹配(实际上它们是完全匹配的)。再次因为这个原因,部分排序规则被用作最后的手段,结果证明第三个函数模板更专业,因此被选中。

关于琐碎转换的注意事项:虽然不完整,但以下从参数类型到参数类型的转换被认为是琐碎转换(给定类型T):

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

EDIT #2:看来我可能没有使用正确的措辞,所以为了清楚我的意思是 function template 是一个创建函数的模板模板函数是由模板创建的函数。

【讨论】:

  • 很好的答案,除了最后一部分:您并没有真正解释为什么同时拥有按值模板和按非常量指针模板不会产生歧义,尽管两者都提供了指向函数特化的指针。
  • @celtschk,最后部分详述
  • 谢谢,现在答案很完美。我开始了赏金;一旦强制等待时间过去(不知道为什么他们出于赏金原因要求这样做,清楚地表明这是为了现有答案),我会将其奖励给您的答案。
  • 虽然我不会挑剔措辞,但有一些重要的事情需要注意:函数模板的部分排序是重载解决方案的最后一步,而不是第一步。在某些情况下,首先这样做实际上会选择不同的重载。同样重要的是要澄清部分排序在函数模板本身上起作用(它与模板参数推导和替换后生成的特化声明无关),而生成的特化对于重载决策过程的其余部分至关重要。
  • 好吧,也许我会吹毛求疵:trivial conversions 不是一个标准术语,也许值得注意的是它只是你用来解释事物的东西这里。此外,exact match 是标准中比您所描述的更大的类别。而且由于我已经开始吹毛求疵并且无法停止,因此没有模板函数作为标准术语(函数模板不是函数;它们是模板,用于生成函数的蓝图)。有一个术语非模板函数来指定不是函数模板特化的函数。
【解决方案2】:

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

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

但是,函数类型的变量可以转换为指向函数类型的指针(这被称为“衰减”,就像数组到指针的转换一样),所以当用template &lt;typename F&gt; void fun(F f)匹配函数类型时,@ 987654324@ 必须是指向函数的指针。

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

【讨论】:

  • 我认为指针的衰减仍然可能发生在引用参数上,只是它没有。我也没有引用标准,请注意:-)
  • 有道理,但我想等待更多见解。 :-)
猜你喜欢
  • 2022-01-05
  • 2020-07-09
  • 1970-01-01
  • 1970-01-01
  • 2011-07-15
  • 1970-01-01
  • 2011-10-07
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多