为什么除非添加括号,否则构造函数上的模板替换会失败?

Why does template substitution fail on a constructor unless I add brackets?

本文关键字:替换 失败 构造函数 添加 加括号 为什么      更新时间:2023-10-16

我试图理解为什么除非添加括号,否则替换在以下代码片段上失败:

template<typename T>
struct A {};
template<typename T>
struct B {
B(A<T>);
};
template<typename T>
void example(A<T>, B<T>);
struct C {};
struct D {
D(C);
};
void example2(C, D);
int main(int argc, char *argv[]) {
example(A<int>{}, A<int>{}); // error
example(A<int>{}, {A<int>{}}); // ok
example2(C{}, C{}); // ok
example2(C{}, {C{}}); // ok
return 0;
}

请参阅此示例:https://godbolt.org/z/XPqHww

对于example2,我能够隐式地将C{}传递给D的构造函数,而不会出现任何错误。对于example,在我添加括号之前,我不允许隐式传递A<int>{}

什么定义了这种行为?

example是一个函数模板,函数参数取决于模板参数。因此,由于您没有在调用example(A<int>{}, A<int>{});中显式指定任何模板参数,因此当您调用此函数时,将执行模板参数推导以确定调用T应该是什么类型。

除了少数例外,模板参数推导要求可以找到一个T,以便函数调用中的参数类型与函数参数中的类型完全匹配。

您的调用的问题在于,A<int>显然与任何TB<T>完全匹配(并且也没有例外适用),因此调用将失败。

此行为是必需的,因为编译器将需要测试所有可能的类型T以检查是否可以调用该函数。这在计算上是不可行的或不可能的。


在调用example2(C{}, C{});中不涉及模板,因此不执行模板参数推导。由于编译器不再需要确定参数中的目标类型,因此可以考虑从已知参数类型到已知参数类型的隐式转换。一种这样的隐式转换是通过非显式构造函数D(C);C构造D。因此,调用通过该转换成功。

example2(C{}, {C{}});实际上也是如此。


那么问题来了,example(A<int>{}, {A<int>{}});为什么有效。这是因为可以在 C++17 标准(草案 N4659)的 [temp.deduct.type]/5.6 中找到特定规则。它表示一个函数参数/参数对,其参数是初始值设定项列表(即{A<int>{}}),并且参数不是std::initializer_list或数组类型的专用化(这里都不是),函数参数是非推导上下文

非推导上下文意味着在模板参数推导过程中不会使用函数参数/参数对来计算T的类型。这意味着其类型不需要完全匹配。相反,如果模板参数推导成功,则生成的T将简单地替换到非推导上下文中,并且从那里将像以前一样考虑隐式转换。B<T>可以从{A<int>}构造T = int,因为非显式构造函数B(A<T>);

现在的问题是模板参数推演是否会成功并推导出T = int。只有当可以从另一个函数参数推导出T时,它才能成功。事实上,仍然有第一个参数,其类型完全匹配:A<int>/A<T>匹配T = int,并且由于此函数参数不使用初始值设定项列表,因此它是从中推断T的上下文。

因此,确实对于example(A<int>{}, {A<int>{}});从第一个参数中推导出将产生T = int并替换到第二个参数B<T>这使得初始化/转换B<T>{A<int>{}}成功,因此调用是可行的。


如果你像example({A<int>{}}, {A<int>{}});那样对两个参数使用初始值设定项列表,两个参数/参数对都将成为非推导上下文,并且不会有任何内容可以从中推断T,因此调用将因无法推导T而失败。


您可以通过显式指定T来使所有调用正常工作,这样模板参数推断就变得不必要了,例如:

example<int>(A<int>{}, A<int>{});