当函数模板参数是具有默认参数的类模板时,函数模板参数的推导如何执行

How does the template argument deduction perform for function template parameter when it is a class template with default argument

本文关键字:参数 函数模板 何执行 执行 默认      更新时间:2023-10-16
template<typename T, typename U = T>
struct Test{};
template<typename T>
void func(Test<T>){  //#1
}
int main(){
func(Test<int>{});  //#2
}

考虑上面的代码,在调用函数模板func时,参数的类型Test<int,int>,调用函数模板时会执行模板参数推导。

函数调用的模板参数推导规则为:
temp.deduct#call-1

模板参数推导

是通过将包含参与模板参数推导的模板参数的每个函数模板参数类型(称为 P)与调用的相应参数的类型(称为 A)进行比较来完成的,如下所述。

我很确定A的类型是Test<int,int>,但是我不确定这里的P类型是什么。 是Test<T>还是Test<T,T>,按照规律,这里P的类型似乎Test<T>,然后进行演绎过程,确定参与模板参数推演的T的值。 然后根据这些规则描述如下:

温度扣除#呼叫-4

通常,演绎过程会尝试查找模板参数值,这些值将使推导的 A 与 A 相同(在如上所述转换类型 A 之后)。

温度扣除#5

当从默认模板参数

中推导或获取所有模板参数后,模板的模板参数列表中模板参数的所有使用和函数类型都将替换为相应的推导或默认参数值。

由于类模板Test具有默认参数,因此推导的T将替换为默认参数。这意味着推导的ATest<int,int>的,并且与参数类型Test<int,int>相同。

但是,这只是我的理解。我不确定这里的P是什么类型。 如果将函数参数的类型更改为Test<int,double>,则结果将报告:

candidate template ignored: deduced conflicting types for parameter 'T' ('int' vs. 'double')

结果看起来好像PTest<T,T>T的拳头值与T的第二个值冲突。

所以,我的问题是:

这里的PTest<T>还是Test<T,T>,为什么?

不是语言律师的答案

没有类型Test<T>实际上是Test<T, T>的"速记"。

就像默认函数参数一样,如果你有int foo(int a, int b = 24)函数的类型是int (int, int),任何像foo(11)这样的调用实际上是foo(11, 24)

P必须是类型而不是模板。test <T>是模板 ID,但在标准中没有明确说明模板 idtest <T>等同于test<T,T>。唯一说的是:

模板 ID 有效,如果

  • [...]
  • 每个没有默认模板参数的不可推导非包参数都有一个参数,[...]

之后,标准中的漏洞被我们的直觉所填补,这种直觉以使用术语默认值为导向。

我认为这里的关键点是模板指定一个家庭,而模板 ID 不能指定一个家庭。

这里的PTest<T>还是Test<T,T>?为什么?

PTest<T,T>.


我认为我们可以同意[temp.deduct]的规则也适用于类模板;例如[temp.class.order],涵盖了类模板专用化的部分排序,完全基于将类模板重写为(发明的)函数模板的概念,并将函数模板的规则应用于与原始类模板对应的发明函数模板的规则在偏序分析下。结合类模板与函数模板相比非常简短的事实,我将以下参考解释为也适用于类模板。

现在,从[温度扣除]/1[强调我的]:

引用函数模板专用化时,所有模板参数都应具有值。这些值可以显式指定,或者在某些情况下,可以从使用中推断出来,或者从默认模板参数中获取。[...]

并且,从[温度扣除]/2 [强调我的]:

指定显式模板参数列表时,模板参数必须

与模板参数列表兼容,并且必须生成有效的函数类型,如下所述;否则类型推断失败。具体而言,在计算与给定函数模板相关的显式指定的模板参数列表时,将执行以下步骤:

  • (2.1)指定的模板参数必须与实物模板参数匹配(即类型、非类型、模板)。参数不能多于参数,除非 [...]

特别强调">被引用"和"指定的模板参数";没有要求我们为给定的匹配函数(/class)模板指定所有参数,只是那些指定的参数遵循显式指定的模板参数的[temp.deduct]/2的要求。

这导致我们回到给定候选函数/类模板的其余模板参数的 [temp.deduct]/1:这些参数可以推导(函数模板)或从默认模板参数获得。因此,调用:

根据上面的论点,

func(Test<int>{});
在语义上等同于

func(Test<int, int>{});

主要区别在于,前者的模板参数由显式指定的模板参数和默认模板参数决定,而后者的模板参数均由显式指定的模板参数决定。由此,很明显ATest<int, int>,但我们将使用类似的参数来P


来自 [temp.deduct.type]/3 [强调我的]:

给定的类型P可以由许多其他类型、模板和非类型值组成:

  • [...]
  • (
  • 3.3)类模板的专用化类型(例如,A<int>)包括专用化的模板参数列表引用的类型,模板和非类型值。

请注意,[temp.deduct.type]/3.3 中的描述现在返回到模板类型P的模板参数列表P并不重要,因为在重载分辨率中检查此特定候选函数时,通过部分显式指定模板参数列表并部分依赖于默认模板参数来引用类模板,其中后者依赖于实例化。这一步的过载解决并不意味着任何类型的实例化,只意味着对候选人的检查。因此,我们刚刚应用于模板参数的相同规则A上面适用于P,在这种情况下,并且当引用Test<int, int>(通过Test<int>),PTest<int, int>,并且我们有PA的完美匹配(对于此示例的单个参数参数对PA)


编译器错误消息?

基于上面的论点,可以说OP的失败示例会出现类似的错误消息:

// (Ex1)
template<typename T, typename U = T>
struct Test{};
template<typename T>
void func(Test<T>) {}
int main() {
func(Test<int, double>{});
}

至于以下简单的一个:

// (Ex2)
struct Foo {};
template<typename T> struct Test {};
template<typename T> void f(T) {}
int main() {
f<Test<int>>(Test<Foo>{});
}

然而,情况并非如此,因为前者分别为GCC和Clang生成以下错误消息:

// (Ex1)
// GCC
error: no matching function for call to 'func(Test<int, double>)'
note:   template argument deduction/substitution failed:
deduced conflicting types for parameter 'T' ('int' and 'double')
// Clang
error: no matching function for call to 'func'
note: candidate template ignored: deduced 
conflicting types for parameter 'T' ('int' vs. 'double')

而后者分别为 GCC 和 Clang 生成以下错误消息:

// (Ex2)
// GCC
error: could not convert 'Test<Foo>{}' from 'Test<Foo>' to 'Test<int>'
// Clang
error: no matching function for call to 'f'
note: candidate function template not viable: 
no known conversion from 'Test<Foo>' to 'Test<int>' for 1st argument

我们最终可以注意到,如果我们调整(Ex1)明确指定f单一模板参数,GCC 和 Clang 都会产生与(Ex2)类似的错误消息,暗示参数推导已从等式中完全删除。

template<typename T, typename U = T>
struct Test{};
template<typename T>
void func(Test<T>) {}
int main() {
func<int>(Test<int, double>{});
}

这种差异的关键可能如[温度扣除]/6[强调我的]中所述:

在模板参数

推导过程中的某些点,有必要采用使用模板参数的函数类型,并将这些模板参数替换为相应的模板参数。当任何显式指定的模板参数被替换到函数类型中时,在模板参数推导开始时完成此操作,当从默认参数推导或获取的任何模板参数被替换时,在模板参数推导结束时再次执行此操作。

即模板参数推导过程分为明确的开始结束,分类:

  • 显式指定模板参数作为流程的开始,并且,
  • 推导或默认参数获得的模板参数作为过程的结束,

这将解释上述示例的错误消息的差异; 如果在演绎过程开始时明确指定了所有模板参数,则该过程的其余部分将没有任何剩余的模板参数可用于 w.r.t. 演绎或默认模板参数。

我试图想出一个代码,只强制类推导而不推导函数.
在这里,没有函数实例化,但编译器无论如何都会发出错误:

template<typename T, typename U = T>
struct Test{};
template<typename T> 
void func(Test<T, T>){
}
template<typename T>
void func(Test<T>){  
}

redefinition of 'template<class T> void func(Test<T, T>)'

GCC: https://godbolt.org/z/7c981E
Clang: https://godbolt.org/z/G1eKTx

以前的错误答案:

P 是指模板参数,而不是模板本身。在声明中Test<typename T, typename U = T>P指的是T,而不是测试。所以在实例化中Test<int>T 是 int,就像调用中的 A 也是 int 一样。