c++11模板类型推导规则

模板类型推导规则

当你准备迫切地想要了解本文的内容时,说明你对c++的了解已经比较深入了!本文将试图介绍c++11之后的模板类型推导机制是怎样的(加入右值引用概念),诚然,模板类型推导规则是了解GP(泛型编程,Generic Programming)的基础和前提,但只会这个还远远不够,泛型编程对我来说还是一个触不可及的范式;哪怕你不准备学习使用GP,模板类型推导规则仍然是一个合格的c++工程师必备的知识,模板在现代c++开发中还是很常用的!

c++的函数/类模板为我们提供了这样一种语义:可以针对多种不定的类型作为参数设计类或者函数,而这些类型在设计类或函数时时暂时不能确定,用 模板参数 来暂为代表:

template<class T>//只有在模板参数声明中,class=typename
class A;

template<typename T>
void fun(T arg);

就像这样,我们可以暂且以一个模板参数T,代替以后将要传入的类型,编译器则负责在调用时推导出T的类型,然后将模板具现化

template<typename T>
void fun(T arg);
int main(){
    int a=2;
    fun(a);//调用时,编译器会生成这样的声明源码:void fun(int arg)
}

可见,模板体系可以正确运作的前提就是必须正确按照用户的调用,推导出模板参数的类型,然后生成具现化版本就容易了。看到这你会问,这。。有啥可讲的呢?模板的推导应该都是编译器级别的东西啊,我们放心交给编译器就好了嘛,学这个有啥用呢?如果你一开始是这样想的,那么可以看看下面这些例子并说出编译器会将模板参数推导为什么?

template<typename T>
void fun_r(T& arg);//模板参数为左值引用(指针也可)

template<typename T>
void fun_rr(T&& arg);//模板参数为万能引用

template<typename T>
void fun(T arg);//模板参数为类型

int main(){
    int a=0;
    //下面这些调用,编译器会将模板推导成什么类型?
    fun_r(a);//答案是:void fun_r(int& arg)
    fun_rr(a);//答案是:void fun_rr(int& arg)
    fun(a);//答案是:void fun(int arg)
}

上面的结果和你预期的一样吗?如果你是初学者的话,很可能会对上面的结论感到怀疑和困惑。因此可以发现,我们学习模板类型推导的目的,主要是为了研究**“当模板函数的形参具有引用属性时,传入具有不同引用属性的实参,编译器会如何推导出其形参类型?”**上面的例子,把这个问题分成了3个子问题,我们只需分别搞清楚:当模板函数形参类型为模板参数的左值引用(指针)、万能引用、值类型 时,模板类型推导的行为。除此以外,还有2种特殊情况需要说明:传递数组实参、传递函数实参。

下文所述的表达式含义如下代码:

template<typename T>
void fun(Type arg);//模板参数为类型

fun(expr);

T为模板参数、Type为上述3种T的引用属性类型、arg为模板函数形参、expr为调用时传入的实参。

1. 模板函数形参类型Type 为模板参数的左值引用或指针

即,模板函数为如下形式:

template<typename T>
void fun(T& arg);

此时的规则如下:

  • 忽略传入的expr实参的任何引用属性作为T的推导类型
  • 将T的推导类型代入Type,即可得到arg的推导类型
  • 不接受右值

举个栗子:

//调用处:
int a=0;
int& b=0;
const int c=0;
const int& d=0;
fun(a);//T->int, arg->int&
fun(b);//T->int,arg->int&
fun(c);//T->const int,arg->const int&
fun(d);//T->const int,arg->const int&
fun(2);//不接受右值!

可见,传入参数的饰词也会被保留下来算作T的推导类型,毕竟T的推导只是去掉引用属性嘛,别的都保留下来。类似,如果Type类型中含有其他饰词则也会被保留下来作为arg的形参推导类型。

2. 模板函数形参类型Type 为模板参数的万能引用

你可能不是很清楚万能引用是个啥?可以这样理解:万能引用的写法和右值引用一样,但如果出现在模板函数形参类型中,这就不叫右值引用,而叫做万能引用。为什么叫“万能引用”呢?因为万能引用比较“全能”,可以推导出你实际传入参数的左值or右值属性类型。

规则如下:

  • expr为左值时,将T推导为T&;当expr为右值时,将T推导为T(值类型)
  • 将T的推导类型代入Type,即可得到arg的推导类型(与“形参类型为左值引用”的第2条规则一样)
  • 如果Type类型表达式中含有任何其他饰词(const等)、其他表达形式,都不算做万能引用,即此规则的第1条不再成立(按照“形参类型为左值引用的第1条规则来推导”)
template<typename T>
void fun(T&& arg);
//调用处:
int a=0;
fun(a);//T->int&,arg->int& && -> 引用折叠 -> int&
fun(2);//T->int,arg->int&&

template<typename T>
void fun1(const T&& arg);//不算万能引用!(按左值引用的规则来推导)
template<typename T>
void fun2(std::vector<T>&& arg);//不算万能引用!(按左值引用的规则来推导)
template<typename T>
class A{
    void fun3(T&& arg);//不算万能引用!(按左值引用的规则来推导)
};

可见以下几点结论:

  1. 万能引用的形成条件很严苛,只有严格按照T&&auto&&(接下来的文章会讲到auto)的形式才能获得万能引用的好处,任何“小聪明”都是不行的。
  2. 万能引用确实很好用:你传入左值实参,他推导出左值形参;你传入右值实参,他也能推导出右值形参。

停等会儿!上面的代码中:a,b作为实参时,推导出来的arg形参是个什么玩意啊啊啊啊?int& && ??? 要解释这是个啥,就要搬出引用折叠的概念了!

  • 引用折叠:

    引用折叠规则也是c++11引用右值引用后,不得不引入的一种规则,使用场景几乎仅在万能引用的类型推导上,引用折叠的具体规则如下:

    1. T& && = T&
    2. T&& & = T&
    3. T& & = T&
    4. T&& && = T&&

    注意,这些引用之间是有空格的,意味着:“左值引用的右值引用”…诸如此类。通过以上4条规则,可以概括成1条通用原则:**发生引用折叠时,只要带有一个左值引用,结果就是左值引用;只有当折叠的所有引用都是右值引用,结果才是右值引用。**这句话有点绕,但结合上面的4条,应该可以很快理解。

补上引用折叠的知识,就可以解释为什么上面代码中奇怪的表述:int& && 等价于 int& 了。

3. 模板函数形参类型Type 为模板参数的值类型

即,模板函数为如下形式:

template<typename T>
void fun(T arg);

这种情况下其实不要把它想复杂了,和引用无关的话反而简单,相当于形参是实参的拷贝(和实参没啥关系了)。规则如下:

  • 忽略传入的expr实参的任何引用、const属性等任何属性后,作为T的推导类型
  • 将T的推导类型代入Type,即可得到arg的推导类型

举个栗子:

template<typename T>
void fun(T arg);
//调用处:
int a=0;
int& b=0;
const int& c=0;
const int* const d=&a;
fun(a);//T->int,arg->int
fun(b);//T->int,arg->int
fun(c);//T->int,arg->int
fun(d);//T->const int*,arg->const int*
fun(2);//T->int,arg->int

template<typename T>
void fun1(const T arg);
//调用处:
int a=0;
fun(a);//T->const int,arg->const int
//诸如此类...

4. 传递数组实参

c语言使用者都知道,数组和指针有着紧密的联系,但应该意识到,即便是c语言,也会把指针和数组看成是两个不同的类型,只不过数组可以隐式转换为指针而已,这称为数组退化,比如下面的经典情况:

void func(int*);
//调用处
int arr[10];
func(arr);

这样数组就退化成了指针,但在c语言中,你也可以让他不退化地传入函数啊:

void func(int arr[10]);
//调用处
int arr[10];
func(arr);

这样就没有发生退化,一个完整的数组被传入函数了,而不是只有首地址的指针。当然了,在现代c++中深究这个问题没有太大意义(std::array),知道有这么回事,能看懂就行了。

我们要讨论的问题是,当数组传入模板函数时,会发生这种退化吗?直接看结论:

值类型的形参,传入数组退化为指针;而引用类型的形参,传入数组不退化。

template<typename T>
void fun(T arg);
template<typename T>
void fun1(T& arg);
//调用处
int arr[10];
fun(arr);//数组退化,T->int*,arg->int*
fun1(arr);//不退化,T->int[10],arg->int(&)[10](数组引用)

5. 传递数组实参

和数组类似,函数即便在c中也是一种类型的,而却可以很容易地退化成函数指针。

这样,道理就与数组几乎一样了,当一个函数传入模板函数时,有着和数组类似的结论:

值类型的形参,传入函数退化为函数指针;而引用类型的形参,传入函数不退化。

template<typename T>
void fun(T arg);
template<typename T>
void fun1(T& arg);
//调用处
void cfunc(int);
fun(cfunc);//函数类型退化,T->void(*)(int),arg->void(*)(int)(函数指针)
fun1(cfunc);//不退化,T->void(int),arg->void(&)(int)(函数类型引用)
  • 最后,我们来综合审视以上5点规则,简单谈谈使用建议:
    1. 当你使用c++编写模板函数时(其实用到模板函数还是很普遍的),1-3条会对你写出正确的代码有很大帮助。
    2. 而4和5。。只能说很冷门,在一般的现代c++工程中不建议使用这种c风格的代码,现代c++对于数组和函数都有很好的设施来为你保驾护航。(除非你是c语言狂热者,但你要做好写出的代码被同事敬佩然后狠狠吐槽的准备哈哈哈。。。)

你可能感兴趣的:(C++学习笔记,c++,开发语言)