C++11 型别推导

Posted by Simon's | Blog on April 21, 2020

C++11 型别推导

成千上万的C++11程序员使用模板编程,大量的人在不知道细节的情况下,往模板函数传递参数时都得到了完全满意的答案,这只能说明C++11类型推导设计的非常成功,但并不意味着了解细节不重要。本文是我在学习C++11该部分内容时的总结笔记。

C++11中最简单的函数模板大概为:

template <typename T>
void f(ParamType param);

该函数的调用则形如:

f(expr);

模板在编译期起作用。在编译期,编译器会根据expr完成对型别TParamType的推导。对于不同的ParamType存在不同的推导规则。主要可以分为三种:

  • ParamType是指针或者引用,但不是万能引用(T&&)。
  • ParamType是万能引用。
  • ParamType既非指针也非引用。

下面我们分别讨论。

1. ParamType是指针或者引用,但不是万能引用

此时推导规则是这样的

  • 如果expr具有引用型别,将其引用部分忽略。
  • 然后,对expr的型别和ParamType的型别进行模式匹配,推导出型别T
ParamType是引用型别
template <typename T>
void f(T& param);

又声明了如下变量:

int x = 3;
const int cx = x;
const int& rx = x;

在各次调用中,编译器的型别推导情况如下:

f(x);		//将T推导为int, ParamType推导为int&
f(cx);		//将T推导为 const int, ParamType推导为 const int&
f(rx);		//将T推导为 const int, ParamType推导为 const int&

注意,对第二、三个调用,我们发现参数中的const在型别推导中被保留了下来,所以我们向持有T&形参类型的模板函数传递const对象是安全的。

对于第三个调用,即便rx具有引用型别,但ParamType依然被推导成了const int&,而不是const int&& ,符合规则二。

ParamType是指针型别

其实没什么不同,考虑如下定义:

template <typename T>
void f(T* param);

int x = 3;
const int* px = &x;

则调用函数f的型别推导如下:

f(&x);		//将T推导为int, ParamType推导为int*
f(px);		//将T推导为const int, ParamType推导为const int*

2. ParamType是万能引用T&&

此时推导规则是:

  • 如果expr是个左值,TParamType都会被推到为左值引用。
  • 如果expr是个右值,则按“常规”情况推导,即按前面说的ParamType是引用而非万能引用推导。

如:

template<typename T>
void f(T&& param);

int x = 3;
const int cx = x;
const int& rx = x;

f(x);		//x是左值,T的型别被推到为int&,ParamType被推导为int&。
f(cx);		//x是左值,T的型别被推到为const int&,ParamType被推导为const int&。
f(rx);		//x是左值,T的型别被推到为const int&,ParamType被推导为const int&。
f(3);		//3是右值,T的型别被推到为int,ParamType被推导为int&&。

3. ParamType既非指针也非引用

此时就是常说的值传递:

template<typename T>
void f(T param);

所谓值传递,就是无论传入的是什么,param都只是它的一个副本,一个全新的对象。其推导规则如下:

  • 如果expr具有引用型别,则忽略其引用部分。
  • 忽略expr的常量性(const)和挥发性(volatile)。

所以:

int x = 3;
const int cx = x;
const int& rx = x;

f(x);		//T的型别被推到为int,ParamType被推导为int。
f(cx);		//T的型别被推到为int,ParamType被推导为int。
f(rx);		//T的型别被推到为int,ParamType被推导为int。

注意,第二次和第三次调用说明,即便rx cx本身是const类型,但param仍然不具有常量性,因为param是一个完全独立于实参的副本。所以对持有T类型形参的模板函数传递常量参数时,参数仍然可以在函数体内被修改。

特别注意:之前讲过,如果形参是const的引用或者指针,expr的常量行会在型别推导中加以保留。我们考虑:expr是个指向const对象的const指针,那么会如何推到呢?

template<typename T>
void f(T param);

const char* const ptr = "hello world"; //ptr是一个指向const对象的const指针

f(ptr);		//此时ParamType的型别被推导为const char*

在上述例子中,星号右侧的const表明:ptr指向的地址不能被改变;星号左侧的const表明:ptr指向地址的内容不能被改变,即该对象(在本例中是一个字符串)本身不能被修改。

按照我们的推导[规则1],ptr本身的常量性被忽略,即在函数体内可以修改ptr指向的内容,包括把ptr置为nullptr,但是ptr指向对象的常量性得到保留。

重点总结

  • 模板性别推导过程中,具有引用型别的实参会被当成非引用型别处理。
  • 对于持有万能引用形参的模板函数,处理左值实参时会做特殊处理。
  • 对值传递形式的形参进行推导时,若实参型别中有constvolatile修饰词时,它们会被忽略。
  • 在模板型别推导过程中,数组货函数型别的实参会退化成对应的指针,除非他们被用来初始化引用。

引用折叠

– 以下内容为2020年5月8日补充

今天看Scott大神的Effective Modern C++ 条款28讲引用折叠,发现跟之前这篇文章内容类似,并且更容易记,遂补充之。

首先,编译器不允许声明一个变量的型别为引用的引用,即形如

int x;
auto& &rx = x; //编译错误!

所以对于上述的型别推导

template <typename T>
void f(ParamType param);

如果TParamType都具有引用语意,编译器会对它们进行所谓的引用折叠

折叠规则如下:

  • 如果TParamType中有任意一个为左值引用,则折叠后推导出的param的型别为左值引用
  • 如果TParamType都为右值引用,则折叠后推导出的param的型别为右值引用
  • 如果param是一个左值但非引用型别,推导出的结果为左值引用
  • 如果param是一个右值,且ParamType右值引用,则推导出的结果不具有引用型别,即param的类型为T
  • 如果param是一个右值,且ParamType左值引用编译错误!不允许将右值传递给一个接收左值引用的模板函数!

至此,终于对C++11型别推导有一个还算清晰的认识了!!