8

考虑以下代码:

#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()。我的期望是所有四个调用都是模棱两可的,并且会导致编译器错误。

我在以下位置测试了代码:

  • Clang 3.6.0 和 GCC 4.9.2,都使用-Wall -Wextra -pedantic -std=c++14( -std=c++1yfor 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'。在所有可能失败的事情中,推论 forT将是我所期望的最后一个......
  • #2and #4:编译时没有错误和警告,并选择第一个重载。

对于所有四种情况,如果我们消除其中一个重载(任何一个),代码编译良好并选择剩余的函数。这看起来像是 Clang 和 GCC 中的不一致:毕竟,如果两个重载分别推导成功,那么在 case#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)'. 不再那么好了。

为了澄清我在寻找什么 case #2,这就是标准(N4296,C++14 final 之后的初稿)在 [14.8.1p9] 中所说的:

模板实参推导可以扩展与模板形参包对应的模板实参序列,即使该序列包含显式指定的模板实参。

看起来这部分在 MSVC 中不太适用,使其选择第一个重载为#2.

到目前为止,看起来 MSVC 虽然不太正确,但至少是相对一致的。Clang 和 GCC 是怎么回事?根据每种情况的标准,正确的行为是什么?

4

1 回答 1

7

据我所知,根据标准,Clang 和 GCC 在所有四种情况下都是正确的,尽管它们的行为可能看起来违反直觉,尤其是在 case#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不能推导出来,但为它显式指定了一个模板参数,因此Tsintmakeg的专业化参数也是如此void (*)(int)。然后将重载与此目标类型进行匹配,并选择第一个。
  • #4:T显式指定为intandTs为空序列,所以h' 特化的参数为void (*)(int),同上。

当我们消除其中一个重载时,我们消除了模板实参推导过程中的歧义,因此模板参数不再处于非推导上下文中,允许根据剩余的重载推导它们。

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

void f() { }

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

我想以这种方式指定的东西是允许从其他来源获得指向函数参数的模板参数,例如其他函数参数或显式指定的模板参数,即使模板参数推导不能基于指向函数参数本身的指针。这允许在更多情况下构造函数模板特化声明。由于重载随后与综合特化的参数匹配,这意味着即使模板参数推导不明确,我们也有办法选择重载。如果这是您所追求的,那将非常有用,在其他一些情况下会非常混乱-真的没有什么不寻常的。

有趣的是,MSVC 的错误消息虽然表面上很好而且很有帮助,但实际上对 有误导性,对#1. 有一定帮助,但#3#4. 此外,它的行为#2是其实现中一个单独问题的副作用,如问题中所述;如果不是这样,它可能也会发出同样的错误消息#2

这并不是说我喜欢 Clang 和 GCC 的 and 错误#1消息#3;我认为他们至少应该包括关于非推断上下文及其发生原因的注释。

于 2015-04-02T16:09:58.330 回答