可变参数函数指针参数的模板参数推导 - 处理模糊情况

bog*_*dan 8 c++ templates variadic-templates argument-deduction c++14

请考虑以下代码:

#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
}
Run Code Online (Sandbox Code Playgroud)

目的是分别尝试身体中的每条线main().我的期望是所有四个调用都不明确并且会导致编译器错误.

我测试了代码:

  • Clang 3.6.0和GCC 4.9.2,两者都使用-Wall -Wextra -pedantic -std=c++14(-std=c++1y对于GCC) - 在所有这些情况下都是相同的行为,除了错误信息措辞的细微差别;
  • Visual C++ 2013 Update 4和Visual C++ 2015 CTP6 - 再次,相同的行为,所以我将其称为"MSVC".

Clang和GCC:

  • #1:编译器错误,基本上是一个令人困惑的消息no overload of 'f' matching 'void (*)()'.什么?无参数声明来自何处?
  • #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]中所说的:

模板参数推导可以扩展与模板参数包对应的模板参数序列,即使序列包含显式指定的模板参数也是如此.

看起来这部分在MSVC中不起作用,使它选择第一个重载#2.

到目前为止,它看起来像MSVC虽然不太正确,但至少相对一致.Clang和GCC发生了什么?根据每个案例的标准,正确的行为是什么?

bog*_*dan 7

据我所知,Clang和GCC根据标准在所有四种情况下都是正确的,即使他们的行为看似违反直觉,特别是在案件#2和案件中#4.

在代码示例中分析函数调用有两个主要步骤.第一个是模板参数推导和替换.当完成时,它产生(任一个专业化的声明gh所有模板参数已替换为实际类型).

然后,第二步尝试将f过载与上一步中构造的实际指针 - 函数参数进行匹配.根据[13.4]中的规则选择最佳匹配 - 重载函数的地址; 在我们的例子中,这很简单,因为重载中没有模板,所以我们有一个完美匹配或根本没有.

理解这里发生的事情的关键点在于,第一步中的歧义并不一定意味着整个过程失败.

以下引用来自N4296,但内容自C++ 11以来没有变化.

[14.8.2.1p6]描述了当函数参数是函数指针(强调我的)时模板参数推导的过程:

当P是函数类型时,指向函数类型的指针或指向成员函数类型的指针:
- 如果参数是包含一个或多个函数模板的重载集,则该参数将被视为非推导上下文.
- 如果参数是重载集(不包含函数模板),则尝试使用集合中的每个成员进行试验参数推导.如果仅对其中一个重载集成员进行推导成功,则该成员将用作推导的参数值.如果对重载集多个成员进行推导成功,则将该参数视为非推导上下文.

为了完整起见,[14.8.2.5p5]澄清了即使没有匹配也适用相同的规则:

非推导的上下文是:[...]
- 一个函数参数,由于关联的函数参数是一个函数或一组重载函数(13.4),因此无法进行参数推导,并且以下一个或多个适用:
- 多个函数匹配函数参数类型(导致模糊推理),或者
- 没有函数匹配函数参数类型,或者
- 作为参数提供的函数集包含一个或多个函数模板.

因此,由于这些情况下的模糊性,没有硬错误.相反,在我们所有情况下,所有模板参数都在非推导的上下文中.这与[14.8.1p3]结合:

[...]未以其他方式推导出的尾随模板参数包(14.5.3)将被推导为空的模板参数序列.[...]

虽然在这里使用"推断"这个词令人困惑,但我认为这意味着如果没有任何元素可以从任何源推导出来,并且没有明确指定模板参数,则将模板参数包设置为空序列. .

现在,来自Clang和GCC的错误消息开始有意义(只有您理解错误发生的原因之后才有意义的错误消息并不完全是有用的错误消息的定义,但我想它总比没有好):

  • #1:由于Ts是空序列,在这种情况下g确实是专业化的参数void (*)().然后,编译器尝试将其中一个重载与目标类型匹配并失败.
  • #3:T仅出现在非推导的上下文中,并且未明确指定(并且它不是参数包,因此它不能为"空"),因此无法为h消息构造专门化声明.

对于编译的情况:

  • #2:Ts不能推导出,但是一个模板参数被明确为它指定的,那么Ts就是int,做g的专业化的参数void (*)(int).然后将重载与此目标类型进行匹配,并选择第一个.
  • #4:T明确指定为intTs是空序列,因此h专门化的参数void (*)(int)与上面相同.

当我们消除其中一个重载时,我们消除了模板参数推导期间的模糊性,因此模板参数不再在非推导的上下文中,允许根据剩余的重载推导出它们.

快速验证是添加第三个过载

void f() { }
Run Code Online (Sandbox Code Playgroud)

允许#1编译案例,这与上述所有内容一致.

我认为事情是以这种方式指定的,以允许从其他源获取涉及指针到函数参数的模板参数,例如其他函数参数或显式指定的模板参数,即使模板参数推断不能基于指向函数的参数本身.这允许在更多情况下构造函数模板特化声明.由于重载然后与合成特化的参数匹配,这意味着即使模板参数推断不明确,我们也有办法选择重载.非常有用,如果这是你所追求的,在其他一些情况下非常混乱 - 没什么不寻常的,真的.

有趣的是,MSVC的错误信息,虽然显然很好,很有帮助,实际上是误导#1,有些但不太有用#3,而且不正确#4.此外,它的行为#2是其实施中的单独问题的副作用,如问题中所解释的; 如果不是这样,它可能也会发出相同的错误错误消息#2.

这并不是说我喜欢锵的和海湾合作委员会的错误信息#1#3; 我认为他们至少应该包含一个关于非演绎语境及其发生原因的说明.