函数模板类型推导笔记

有时候,函数模板类型推导是个让人困惑的玩意儿~

引言

顾名思义,这篇文章的主题是函数模板类型推导,所以不会涉及过多的理论,主要研究的内容是编译器的推导行为。那么如何研究呢?实际上就是引入一些实验代码,来观察编译器的行为,所以这个过程略微有点无聊。
好了,废话不多说,直接进入主题了。

我们以函数模板中的参数传递方式为差异点来进行讨论,对应的我们可以得到四种传递方式。需要注意的是,这只是我们的分类方法,这与普通函数中的四种参数传递方式是不同的。尽管可能存在相似的地方,但本质还是不一样的。

❗ 注意:本文所使用的编译器是 MSVC2019 自带的编译器,不同编译器的推导结果可能会存在差异。

值传递

第一种方式就是值传递,对应的模板形式:

1
2
template <typename T>
void f1(T t) { }

然后我们针对常见的参数类型,进行测试:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
#include <iostream>

void f(int) { }

template <typename T>
void f1(T t) { }

int main() {
int i = 0;
const int ci = i;
int& ri = i;
const int& cri = i;
int&& rri = 20;
int* pi = &i;
int* const cpi = &i;
const int* ptci = &i;
int a[5] = { 0 };

f1(i); // T 被推导为 int
f1(ci); // T 被推导为 int
f1(ri); // T 被推导为 int
f1(cri); // T 被推导为 int
f1(rri); // T 被推导为 int
f1(pi); // T 被推导为 int*
f1(cpi); // T 被推导为 int*
f1(ptci); // T 被推导为 const int*
f1(a); // T 被推导为 int*
f1(f); // T 被推导为 void(*)(int)
f1(50); // T 被推导为 int

return 0;
}

观察上面的结果,可以发现在值传递方式推导模板实参的一些特点:

  1. 可以传递左值,也可以传递右值,但无法推导出引用,只能推导出原始类型。因为模板实例化生成的函数是值传递的,函数形参是函数实参的拷贝。
  2. 对于指针和引用,顶层 const 会被忽略掉,底层 const 不会被忽略掉。原因同上,模板实例化后的函数是值传递的,函数形参是函数实参的拷贝,在函数内直接修改函数形参,不会对函数实参有任何改变;对应的,底层 const 不能忽略,不能在函数内通过函数形参修改其所指对象的值。
  3. 如果数组和函数名被传递给模板,则推导出来的模板实参是对应的指针。

左值引用传递

第二种方式是左值引用传递,对应的模板形式:

1
2
template <typename T>
void f2(T& t) { }

同样可以进行测试:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
#include <iostream>

template <typename T>
void f2(T& t) { }

int main() {
int i = 0;
const int ci = i;
int& ri = i;
const int& cri = i;
int&& rri = 20;
int* pi = &i;
int* const cpi = &i;
const int* ptci = &i;
int a[5] = { 0 };

f2(i); // T 被推导为 int
f2(ci); // T 被推导为 const int
f2(ri); // T 被推导为 int
f2(cri); // T 被推导为 const int
f2(pi); // T 被推导为 int*
f2(cpi); // T 被推导为 int* const
f2(ptci); // T 被推导为 const int*
f2(a); // T 被推导为 int [5]
f2(f); // T 被推导为 void (int)
f2(50); // 错误,无法传递右值

return 0;
}

观察上面的结果,我们也可以得到左值引用传递方式推导模板实参的一些特点:

  1. 可以传递左值,无法传递右值,也无法推导出引用,但本身就是T&,最终t的类型可能是左值引用。
  2. 顶层 const 和底层 const 都不会被忽略,且与实参基本一致,如果实参是 const 的,那么T就是 const 的。
  3. 传递数组和函数名时,推导出来的是对应的类型,不是指针。

常量左值引用传递

第三种方式是常量左值引用传递,对应的模板形式:

1
2
template <typename T>
void f3(const T& t) { }

同样进行测试:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
#include <iostream>

void f(int) { }

template <typename T>
void f3(const T& t) { }

template <typename T>
void f4(T&& t) { }

int main() {
int i = 0;
const int ci = i;
int& ri = i;
const int& cri = i;
int&& rri = 20;
int* pi = &i;
int* const cpi = &i;
const int* ptci = &i;
int a[5] = { 0 };

f3(i); // T 被推导为 int
f3(ci); // T 被推导为 int
f3(ri); // T 被推导为 int
f3(cri); // T 被推导为 int
f3(pi); // T 被推导为 int*
f3(cpi); // T 被推导为 int*
f3(ptci); // T 被推导为 const int*
f3(a); // T 被推导为 int [5]
f3(f); // T 被推导为 void (int)
f3(50); // T 被推导为 int

return 0;
}

观察上面的结果,我们也可以得到常量左值引用传递方式推导模板实参的一些特点:

  1. 可以传递左值,也可以传递右值,也无法推导出引用,但本身就是T&,最终t的类型可能是左值引用。从语义上讲,这与常量左值引用的语义也是符合的。
  2. 顶层 const 会被忽略掉,因为本身就是const T&形式,所以即便推导T时,忽略了顶层 const,但最终又加上了顶层 const;对应的,底层 const 会被保留。
  3. 传递数组和函数名时,推导出来的是对应的类型,不再是指针。

右值引用传递

第四种方式是右值引用传递,对应的模板形式:

1
2
template <typename T>
void f4(T&& t) { }

同样进行测试:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
#include <iostream>

void f(int) { }

template <typename T>
void f4(T&& t) { }

int main() {
int i = 0;
const int ci = i;
int& ri = i;
const int& cri = i;
int&& rri = 20;
int* pi = &i;
int* const cpi = &i;
const int* ptci = &i;
int a[5] = { 0 };

f4(i); // T 被推导为 int&
f4(ci); // T 被推导为 const int&
f4(ri); // T 被推导为 int&
f4(cri); // T 被推导为 const int&
f4(pi); // T 被推导为 int*&
f4(cpi); // T 被推导为 int* const&
f4(ptci); // T 被推导为 const int*&
f4(a); // T 被推导为 int(&)[5]
f4(f); // T 被推导为 void (&)(int)
f4(50); // T 被推导为 int

return 0;
}

观察上面的结果,我们也可以得到右值引用传递方式推导模板实参的一些特点:

  1. 可以传递左值,也可以传递右值,且会被推导为原始类型的引用。
  2. 顶层 const 和底层 const 都会被保留,且会被推导为原始类型的引用。
  3. 传递数组和函数名时,推导出来的是对应的类型,不再是指针,且会被推导为原始类型的引用。
  4. 为了避免出现引用的引用,对应t的类型会发生引用折叠。

总结

我们根据函数模板的参数传递方式,讨论了四种不同传递方式的特点。可以发现:

  1. 右值引用可以最大限度的保留所传递参数的类型和值类别,且会发生引用折叠现象。
  2. 值传递方式下,顶层 const 会被忽略,底层 const 不会被忽略;数组和函数名会被传递为指针。
  3. 模板根据实参推导类型时,不会对实参进行隐式类型转换,只会按照模板实例化出不同函数。

还需要额外说明的是,本文的内容不需要刻意记忆,遇到了查一下就可以了。
另外,本文也可以作为理解完美转发这篇文章的补充内容。


Buy me a coffee ? :)
0%