C++之类型推导

C++类型推导

  • 1 模板类别推导
  • 2 数组和函数作为模板函数参数
  • 2 auto类型推导
  • 3 auto推导与模板推导的不同
  • 4 C++14的更多类型推导特性
  • 5 decltype关键字


1 模板类别推导

函数模板和类模板是C++泛型编程的基础,它使得我们的程序不再受数据类型的限制。因此,要写出高效的现代C++程序,理解模板类型推导是很有必要的。

下面是一个典型的函数模板:

template<typename T>
void func(ParamType param); // ParamType可以是T、const T、const T&等
							// ParamType可以说是用const、&等包装之后的T

其中ParamType可以是Tconst Tconst T&等。
调用如下:

func(expr); // expr =  expression

在编译的时候,编译器通过expr来对ParamTypeT进行推导(通常来说ParamType和T不同,如ParamType为const T、const T&等)。

例如:

template
void func(const T& param);
...
int x = 0;
f(x);

此时,T被推导为intParamType被推导成const int&

实际上,C++的模板类型推导远不止上述这么简单,T的类型不仅与expr的类型有关,还与ParamType的形式有关。

下面分三种情况讨论模板类型推导:

1. ParamType是一个指针或者是一个非通用引用
(其中通用指针的概念后面会进行解释)

此时,T的推导步骤如下:

  • 1.如果expr的类型是引用,则忽略引用符号。
  • 2.将exprParamType进行匹配来判断T的类型。

例如:

template<typename T>
void func(T& param);
...
int x = 27;
const int cx = x;
const int& rx = x;
...
f(x);		// x为int,因此param的类型为int&而T为int
f(cx);		// cx为const int,因此param的类型为const int&而T为const int
f(rx);		// rx为const int&,忽略引用后为const int,因此param的类型为const int&而T为const int

上述代码有如下几个需要注意的地方:

  1. f(rx)中忽视了rx的引用特性,这个在步骤1中已经说明了,因此没有什么需要额外解释的。
  2. 步骤2中exprParamType是如何匹配来判断T的类型还没有解释清楚。看下面例子:
template<typenameT>
void func1(T& param);

template<typeanmeT>
void func2(const T& param);

int x = 27;
const int cx = x;

func1(cx); // cx的类型为const int,对比T&得到T为const int
func2(cx); // cx的类型为const int,对比const T&得到T为int

上述代码中,func1func2的不同在于ParamType,分别为T&const T&

  • const intT&对比得到Tconst int
  • const intconst T&对比得到Tint

同样的,当ParamType为指针时也遵循上述规则:

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

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

func(px); // px为const int *对比T*得到T为const int,从而param的类型为const int *

上述代码中,ParamTypeT*,而exprconst int*,因此T被推导为const int,从而得到param的类型为const int *

2. ParamType是一个通用引用(Universal Reference)

一个通用引用参数的申明类型是T&&,此时需要根据expr是左值还是右值分情况讨论:

  • 1.如果expr是一个左值,TParamType都被推导成左值引用。
  • 2.如果expr是一个右值,则执行和情况1相同的法则。

例如:

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

int x  = 27;
const int& rx = x;

f(x);	// 由于x为左值,因此T和param都被推导为int& -- ①

f(rx); 	// 由于rx为左值,因此T和param都被推导为const int& -- ①

f(27);	// 由于27是右值,其类型为int,与T&&进行对比得到T类型为int而param类型为int&& -- ②

上述代码中,f(rx)rx的类型为const int&,首先忽略引用,然后推导为左值引用。f(27)27为右值,因此推导为int&&
3. ParamType不是指针或引用

ParamType既不是指针也不是引用时,将其处理为传值的形式。这意味着param是参数的一份拷贝,是一个完全新的对象。

同样的,根据expr不同情况给出推导的法则:

  • 如果expr是一个引用,则忽略引用部分。
  • 如果expr是个const或者volatile,也要忽略掉constvolatile

例如:

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

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

f(x);	// x类型为int,因此T和param的类型也为int
f(cx);	// cx类型为const int,忽略掉const,因此T和param的类型仍为int
f(rx);	// rx类型为const int&,忽略掉const和&,因此T和param的类型还是int

考虑expr是一个const指针指向一个const对象,而expr是按值传递的方式传递给func

template
void f(T param);

const char* const ptr = "Hello World!";

f(ptr);	// ptr是一个指向const对象的const指针,忽略其本身的const特性后为const char*
		// 因此可以推导出T和param的类型为const char*

上述代码中,const char*表示ptr指向一个常字符数组,而const ptr表示ptr是一个常指针,自初始化之后便不能变更。因此,在推导的过程中忽略了ptrconst属性而保留了其指向对象的const属性,最终推导出T和param的类型为const char*

2 数组和函数作为模板函数参数

此外,以数组或者函数作为参数进行模板的规则如下:

1. 数组参数

在大多数人眼中,数组类型和指针类型几乎是一样的,如下:

const char str[] = "Hello World!"; // str的类型为const char[13]

const char *ptr = str; // ptr的类型为const char*

实际上,str的类型为const char[13],而ptr的类型为const char*。二者的区别在于str是一个数组对象,而ptr是一个指针对象,用sizeof关键字作用于二者时,sizeof(str)返回13,而sizeof(ptr)返回4(32位).

因此,代码const char * ptr = str过程实际上是数组退化为指针的过程。

对于下面两个函数声明:

void func1(int param[]);

void func2(int* param);

实际上是等价的,因为C++会把数组声明退化为指针声明。这就导致了数组和指针等价的错觉。

尽管函数参数不能真正定义为数组,但是可以声明参数为数组的引用:

templaete<typename T>
void func(T& param)const char str[] = "Hello World!";

func(str);

推导规则对应了上面的情况1,因此T的类型为const char [13],而param的类型为const char(&)[13]

因此,利用上述模板函数可以推导出数组的长度:

template<typename T>
int func(T& param){
	return sizeof(param); // sizeof关键字的返回值类型为usnsigned_int64
}						  // 因此,用int作为返回值可能会溢出。

const char str[] = "Hello World!";
cout << func(str) << endl; // 13

下面给出一个高级严谨的写法:

template<typename T, std::size_t N>
constexpr std::size_t func(T (&)[N]) noexcept{
	return N;
}

2. 函数参数

数组可以退化为指针,函数也可以退化为函数指针:

void someFunc(int, double);

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

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

func1(someFunc);	// param被推导为函数指针,类型为void(*)(int, double)
func2(someFunc);	// param被推导为函数指针,类型为void(&)(int, double)

上述两种方式在实践中几乎没有区别。

综上,模板推导主要有以下几个关键点:

  • 1.模板类型推导时,有引用特征的参数的引用特性被忽略。
  • 2.推导通用引用时,左值引用会被特殊处理。
  • 3.在推导按值传递参数时,const/volatile类型参数的const/volatile属性被忽略。
  • 4.模板类型推导时,数组和函数参数会退化为指针。

2 auto类型推导

auto类型推导几乎和模板类型推导的过程相同,其中唯一不同的一点会在后面提及。

例如:

auto x = 27;

和下面模板推导的规则相同:

template<typename T>
void func(T param);
const auto cx = x;

和下面模板推导的规则相同:

template<typename T>
void func(const T param);
const auto& rx = x;

和下面模板推导的规则相同:

template<typename T>
void func(const T& param);

可以看出,auto实际上就等价于模板推导中的T,因此const auto cx = x推导出cx的类型为const int,而const auto& rx = x推导出rx的类型为const int&

同样的,对于情况2:

auto&& uref1 = x;	// x(int)为左值,因此推导出uref1的类型为int&

auto&& uref2 = cx;	// cx(const int)为左值,因此推导出uref2的类型为const int&

auto&& uref3 = 27;	// 27为右值,因此推导出uref3的类型为int&&

对于数组和函数对象,auto同样推导为指针:

const char str[] = "Hello World!";

auto arr1 = str;	// arr1的类型为const char*

auto& arr2 = name;	// arr2的类型为const char(&)[13]

void someFunc(int, double);

auto& func2 = someFunc; // func2的类型为void(&)(int, double)

3 auto推导与模板推导的不同

auto唯一和模板推导不同的地方在于花括号初始化:当auto声明变量被使用一对花括号初始化,推导的类型是std::initializer_list的一个实例,但如果将相同的初始化传递给相同的模板,则类型推导失败,代码不能通过编译。

例如:

auto x = {1, 2, 3}	// x的类型为std::initializer_list

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

f({1, 2, 3});	// 错误!无法推导出T的类型

但是,当明确模板的param的类型是一个未知类型Tstd::initializer_list

template<typename T>
void func(std::initializer_list<T> initList);

f({1, 2, 3});	// T被推导为int,initList的类型为std::initializer_list

因此,auto和模板类型推导的本质区别是auto假设花括号代表的是std::initailizer_list,而模板推导则不是。

此外,当使用auto进行花括号的类型推导时,如果花括号中的数据类型不同,则类型推导也会失败:

auto x = {1, 2, 3.0};	// 无法推导"auto"类型

4 C++14的更多类型推导特性

C++14允许auto表示函数的返回值,并且允许在lambda中的函数声明中使用auto

令人蛋疼的是,当使用auto声明函数的返回值时,利用的却是模板的类型推导规则,而不是auto的类型推导,例如下例无法编译:

auto createInitList(){
	return {1, 2 ,3.0};
}

在VS中提示的错误为:以括号起的列表不提供返回类型。

同样的,在lambda中使用auto进行参数类型声明时也是利用的模板类型推导规则,下例同样无法编译:

std::vector<int> v;

auto restV = [&v](const auto& newValue){v = newValue;};

restV({1, 2, 3});	// 编译错误,无法推导出{1, 2, 3}的类型。

5 decltype关键字

decltype关键字返回给定变量或表达式的类型,并且可以用这个返回的类型来声明变量。

auto和模板推导不同的是,decltype一般只是复述一遍所给的变量名或者表达式的类型,如下:

const int i = 0;			// decltype(i) is const int

bool f(const Widget & w);	// decltype(w) is const Widget&
								// decltype(f) is bool(const Widget&)

struct Point {
	int x, y;				// decltype(Point::x) is int
};

Widget w;					// decltype(w) is Widget

if(f(w))...					// decltype(f(w)) is bool

template<typename T>
class vector {
public:
	...
		T& operator[](std::size_t index);
	...
};

vector<int> v;				// decltype(v) is vector
...
if(v[0] == 0)				// decltype(v[0])is int&

C++11中,decltype最主要的用处之一就是用来声明一个函数模板,在这个函数模板中返回值的类型取决于参数的类型,如下例:

template<typename Container, typename Index>
auto authAndAccess(Container& c, Index i)
->decltype(c[i]) {
	return c[i];
}

这里使用了C++11的尾置返回类型技术,即函数的返回值类型在函数参数之后声明,其优势是在定义返回值类型的时候使用函数参数。在这个时候,auto的类型推导没有起作用,而是由尾置返回指定了返回值的类型。

当利用auto进行类型推导时:

template<typename Contianer, typename Index>
auto authAndAccess(Container& c, Index i) {
	return c[i];
}

前面说到,当使用auto来推导函数的返回类型时,使用的是模板推导的规则,那么对于返回值为&T时会忽略引用,得到的仅为返回值的一份拷贝,如下例:

vector<int> v = {1, 2, 3, 4, 5, 6};
authAndAccess(v, 3) = 10;	// 报错,因为返回值为右值

auto修改为auto&则可以达到目的:

template<typename Container, typename Index>
auto& authAndAccess(Container& c, Index i){
return c[i];
}
vector<int> v = {1, 2, 3, 4, 5, 6};
authAndAccess(v, 3) = 10;	// v = {1, 2, 3, 10, 5, 6}

C++14中,通过decltype(auto)可以更好的实现上述功能:auto指定需要推导的类型,decltype表明在推导的过程中使用decltype推导规则,因此可以重写如下:

template<typename Container, typename Index>
decltype(auto)
authAndAccess(Container& c, Index i) {
	return c[i];
}

decltype(auto)并不仅限于使用在函数返回值类型上,它也可以很方便的来声明一个变量:

Widget w;
const Widget& cw = w;
auto myWidget1 = cw;			// auto type deduction
								// myWidget1's type is Widget 
decltype(auto) myWidget2 = cw;	// decltype type deduction
								// myWidget2's type is const Widget&

综上,本文总结了C++的部分模板推导规则,其中大多数规则自C++11之后才支持,少量规则仅C++14支持。这些模板推导规则虽然很复杂,但却是C++泛型变成的基础,只有理解了这些模板推导规则才能写出高效的代码。

参考:《Effective Modern C++》

你可能感兴趣的:(C++)