请考虑以下代码:
#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()
正文中的每一行。我的期望是所有四个调用都是模糊的,会导致编译器错误。
我测试了代码:
-Wall -Wextra -pedantic -std=c++14
(-std=c++1y
用于GCC) - 在所有这些情况下都是相同的行为,除了错误消息措辞的细微差别; Clang和GCC:
#1
:编译器错误,带有令人困惑的消息,基本上是no overload of 'f' matching 'void (*)()'
。什么? no-param声明来自何处?#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决赛后的初稿)在[14.8.1p9]中所说的:
模板参数推导可以扩展模板的顺序 对应于模板参数包的参数,即使是 sequence包含显式指定的模板参数。
看起来这部分在MSVC中不起作用,使其选择#2
的第一个重载。
到目前为止,看起来MSVC虽然不太对,但至少相对一致。 Clang和GCC发生了什么?根据每个案例的标准,正确的行为是什么?
答案 0 :(得分:7)
据我所知,根据标准,Clang和GCC在所有四种情况下都是正确的,即使他们的行为看似违反直觉,特别是在#2
和#4
的情况下。 / p>
分析代码示例中的函数调用有两个主要步骤。第一个是模板参数推导和替换。完成后,它会生成一个特化声明(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),否则没有 推导出的推断将被推导出一个空的模板参数序列。 [...]
虽然使用了&#34;推断出&#34;这里令人困惑,我认为这意味着如果没有任何元素可以从任何来源推导出来,那么模板参数包就会被设置为空序列,并且没有为它明确指定的模板参数。
现在,来自Clang和GCC的错误消息开始有意义(只有 之后才能理解错误发生的错误信息并不完全是有用的错误消息的定义,但是我想它总比没有好:
#1
:由于Ts
是空序列,因此g
的专业化参数在这种情况下确实是void (*)()
。然后编译器尝试将其中一个重载与目标类型匹配并失败。#3
:T
仅出现在非推断的上下文中,未明确指定(并且它不是参数包,因此它不能是&#34;为空&#34 ;),因此无法为h
构建专门化声明,因此消息。对于编译的案例:
#2
:Ts
无法推断,但是为其明确指定了一个模板参数,因此Ts
为int
,正在制作g
&#39 ; s专业化参数void (*)(int)
。然后将重载与此目标类型进行匹配,并选择第一个。#4
:T
被明确指定为int
,而Ts
是空序列,因此h
的专业化了参数为void (*)(int)
,与上述相同。当我们消除其中一个重载时,我们消除了模板参数推导期间的模糊性,因此模板参数不再在非推导的上下文中,允许根据剩余的重载推断它们。
快速验证是添加第三个重载
void f() { }
允许编译案例#1
,这与上述所有内容一致。
我认为事情是以这种方式指定的,以允许从其他源获取涉及指针到函数参数的模板参数,例如其他函数参数或显式指定的模板参数,即使模板参数推断不能可以根据指针到函数参数本身来完成。这允许在更多情况下构造函数模板特化声明。由于重载然后与合成特化的参数匹配,这意味着即使模板参数推断不明确,我们也有办法选择重载。非常有用,如果这是你所追求的,在其他一些情况下非常混乱 - 没什么不寻常的,真的。
有趣的是,MSVC的错误消息虽然显然很好且很有帮助,但实际上对#1
有误导性,对#3
有些但不太有帮助,{{1}不正确}}。此外,#4
的行为是其实施中单独问题的副作用,正如问题中所解释的那样;如果不是这样,它也可能会为#2
发出相同的错误错误消息。
这并不是说我喜欢Clang和GCC的#2
和#1
的错误消息;我认为他们至少应该包括一个关于非演绎语境及其发生原因的说明。