一 理解模板型别推导
函数模板类型如下:
template <typename T>
void f(ParamType param); //ParamType是一个T相关的类型
f(expr); //推导T和ParamType的型别
ParamType有三种类型:
- ParamType是个指针或引用,但不是万能引用
- ParamType是万能引用
- ParamType不是指针也不是引用
对应代码如下:
template <typename T>
void func1(T& param) //指针或引用,但不是万能引用
{
cout << param << endl;
}
template <typename T>
void func2(T&& param) //万能引用
{
cout << param << endl;
}
template <typename T>
void func3(T param) //不是指针也不是引用
{
cout << param << endl;
}
int main()
{
int x = 10;
const int cx = x;
const int &rx = x;
func1(x); //T:int; param:int &
func1(cx); //T:const int; param:const int &
func1(rx); //T:const int; param:const int &
func2(x); //T:int &; param:int &
func2(cx); //T:const int &; param:const int &
func2(rx); //T:const int &; param:const int &
func2(10); //T:int; param:int &&
func3(x); //都是int
func3(cx); //都是int
func3(rx); //都是int
return 0;
}
对于func1()来说,传入参数的引用类型是被忽略的,并通过param的类型和ParamType的类型执行模式匹配,来决定T的类型。const类型成为型别推导的组成部分。即传入的是int或者是int &,T类型都是int,而param类型为int &。
对于func2()来说,既可以传入左值又可以传入右值,传入左值的时候,T和param类型是一致的,传入右值的时候,T的类型是int,param的类型就变为int &&,这是一个右值引用,延长了右值的生存时间。
对于func3()来说,传入的param是实参的一个副本,所以无论实参是什么类型,param都只传入最基本的类型,忽略const,volatile等组成部分。但是如果传入的实参指向的对象也是const的话,传入param后,这个特性是不会变的。例如const int * const p传入,指针和指针指向的对象都是不可以修改的,但是传入func3()后,指针的副本可以修改,但是指针的副本指向的对象还是不能修改。
数组实参
如果传入的实参是数组的话,有两种情况:
1.按值传递
数组会退化成指向首元素的指针,这时候类似于传入的是一个指针。
const char name[] = "123456789";
template <typename T>
void func1(T param) //按值传递
{
cout << param << endl;
}
func1(name); //name会从const char[]退化为const char *
2.按引用传递
函数将形参声明成数组的引用。
const char name[] = "123456789";
template <typename T>
void func1(T& param) //指针或引用,但不是万能引用
{
cout << param << endl;
}
func1(name); //T的型别会被推导为实际的数组类型,类型中包含数组尺寸
上例中,T的类型是const char[10],而f的形参是const char(&)[10]。
所以我们可以使用这个模板来推导数组元素的个数
template <typename T, size_t N>
const size_t func4(T (&)[N]) //不是指针也不是引用
{
return N;
}
const char name[] = "123456789";
const int ret = func4(name);
cout << ret << endl; //10
函数实参
函数实参很简单,就是函数退化为函数指针(func1())和函数引用(func2())
template <typename T>
void func1(T& param) //指针或引用,但不是万能引用
{
cout << param << endl;
}
template <typename T>
void func2(T&& param) //万能引用
{
cout << param << endl;
}
void somef(int, int);
func1(somef); //退化为函数指针void (*)(int, int)
func2(somef); //退化为函数引用void (&)(int, int)
二 理解auto型别推导
auto关键字可以进行型别推导,auto的型别推导是类似于模板的型别推导的。
void func(int a, int b)
{
cout << a << ' ' << b << endl;
}
template <typename T>
void f(T param)
{
cout << typeid(param).name() << endl;
}
template <typename T>
void f11(initializer_list<T> param)
{
cout << typeid(T).name() << endl;
cout << typeid(param).name() << endl;
}
int main()
{
auto x = 27;
const auto cx = x;
const auto &rx = x;
auto &&uref1 = x; //uref1:int &
auto &&uref2 = cx; //uref2:const int &
auto &&uref3 = 10; //uref3:int &&
const char name[] = "fuhao";
auto arr1 = name; //arr1:const char *
auto &arr2 = name; //arr2:const char (&)[5]
auto f1 = func; //f1:void (*)(int, int)
cout << typeid(f1).name() << endl;
auto &f2 = func; //f2:void (&)(int, int)
cout << typeid(f2).name() << endl;
auto x1 = 10;
auto x2(10);
cout << typeid(x2).name() << endl;
auto x3 = { 10 };
cout << typeid(x3).name() << endl;
auto x4{10};
cout << typeid(x4).name() << endl;
//auto x4 = { 10, 20, 5.0 }; //无法运行,原因是initializer_list中成员类型不一致的情况下,无法推导出initializer_list的类型,也就无法推导x4的类型
auto p = {11,22,33};
//f({ 11,22,33 }); //模板推导不能推导initializer_list类型
f11({ 11,22,33 });
return 0;
}
这里对x、cx、rx的型别推导就类似于模板的型别推导。但是有一种例外,auto型别推导出来的结果和模板推导是不一样的。
对于模板推导来说,无法进行initializer_list
的推导,所以只能使用f11那种方式,这样T的类型被推导为int,而param的类型为initializer_list<int>
。上述代码的编译器报错如下:
同样的,用auto来指定C++14中lambda表达式的形参型别时,也不能使用大括号括起来。
vector<int> v;
auto ret = &v{ v = val; };
ret({1,2,3}); //有错,无法进行类型推导
三 理解decltype
decltype可以返回给定的变量或者表达式的型别,不对其做任何修改。
例子如下:
class Widget {
public:
Widget(int _w = 10) : w(_w) { }
int getW() const { return w; };
private:
int w;
};
struct point
{
int x, y; //decltype(x)、decltype(y):int
};
bool f(const Widget &w) decltype(w):const Widget &; decltype(f):bool(const Widget &)
{
if (w.getW() != 0)
return true;
else
return false;
}
template <typename T>
class myvector
{
public:
T & operator[](size_t index) {
return x;
}
private:
T x;
};
int main()
{
const int i = 0; //decltype(i):const int
Widget w; //decltype(w):Widget
if (f(w)) ; //decltype(f(w)):bool
myvector<int> v;//decltype(v):myvector<int>
if (v[0] == 0) ;//decltype(v[0])是int
return 0;
}
C++11中,decltype的主要用于就是声明那些返回值型别依赖于形参型别的函数模板。例如模板函数中operator[]要返回容器内部的元素,对于含有型别T的容器,operator[]一般要返回T&(std::vetor<bool>
除外),decltype可以较好的实现这点。
在C++11中,可以配合使用auto和返回值型别尾序语法,即该返回值的型别在形参列表之后,用->标识出,尾序语法的好处是在指定返回值型别的时候可以可以使用形参函数。
template<typename Container, typename Index>
auto access(Container &&c, Index i)
//完美转发,将右值c转化为左值,最终返回的是内部型别的左值引用
-> decltype(forward<Container>(c)[i])
{ }
在C++14中可以对所有lambda表达式和函数的返回值型别进行推导,所以可以去掉尾序语法,但是这样做,auto在推导返回型别的时候会去掉引用性质,如果我们想返回T&,通过auto的推导,最终返回的是T而不是T&,这样返回值是一个右值,就无法对其进行赋值了。C++14中可以使用一个新的饰词,就是decltype(auto),这样auto指定了要实施推导的型别,而推导过程采用的是decltype的规则。这样,返回值型别为:
template<typename Container, typename Index>
decltype(auto) access2(Container &&c, Index i)
{ }
这样,decltype(auto)就和函数内部返回的结果是一样的类型了,而不会去掉内部return返回值的引用饰词。
由上图可见,auto会去掉引用性质,而decltype(auto)不会。
上述两个例子的容器用的都是万能引用,这是因为传进来的容器不一定是左值,也有可能是一个右值,但是我们在函数内部已经明确return的值是一个左值引用,这样我们就要用到完美转发,无论传入的值是左值还是右值,否会返回右值引用,上述两个例子的函数体如下:
//C++11
template<typename Container, typename Index>
auto access(Container &&c, Index i)
-> decltype(forward<Container>(c)[i])
{
return forward<Container>(c)[i]; //完美转发,无论c是左值还是右值,通通返回左值引用(如果c[i]返回的是引用的话)
}
//C++14
template<typename Container, typename Index>
decltype(auto) access2(Container &&c, Index i)
{
return forward<Container>(c)[i];
}
要记住的一点是,千万不能用decltype(auto)返回局部变量的引用。
decltype推导的规则如下:
- 如果e是一个没有带括号的标记符表达式或者类成员访问表达式,那么decltype(e)就是e所命名的实体的类型,此外如果e是一个被重载的函数,那么会导致编译错误。
- 否则假设e的类型是T,如果e是一个将亡值,那么decltype(e)是T&&
- 否则假设e的类型是T,如果e是一个左值,那么decltype(e)是T&
- 否则,假设e的类型是T,则decltype(e)为T
标记符指的是去除关键字,字面量等编译器需要使用的标记之外的程序员自定义的标记,而单个标记符对应的表达式即为标记符表达式
int arr[4]; //arr是一个标记符,arr[1]+3就不是
例如:
int i = 10;
decltype(i) a; //a被推导为int
decltype((i)) b = 10; //b被推导为int &,必须对其初始化,否则无法通过编译
i是一个标记符表达式,而(i)是一个左值表达式,这样推导出来的结果必然是不同的。
#include <iostream>
using namespace std;
int main()
{
int i = 4;
int arr[5] = { 0 };
int *ptr = arr;
struct S { double d; }s;
void Overloaded(int);
void Overloaded(char);//重载的函数
int && RvalRef();
const bool Func(int);
//规则一:推导为其类型
decltype (arr) var1; //int [5]
decltype (ptr) var2;//int *
decltype(s.d) var3;//doubel 成员访问表达式
//decltype(Overloaded) var4;//重载函数。编译错误。
//规则二:将亡值。推导为类型的右值引用。
decltype (RvalRef()) var5 = 1;
//规则三:左值,推导为类型的引用。
decltype ((i))var6 = i; //int&
decltype (true ? i : i) var7 = i; //int& 条件表达式返回左值
decltype (++i) var8 = i; //int&。
decltype(arr[5]) var9 = i;//int&
decltype(*ptr) var10 = i;//int& *操作返回左值
decltype("hello") var11 = "hello"; //const char(&)[6] 字符串字面常量为左值,且为const左值。
//规则四:以上都不是,则推导为本类型
decltype(1) var12;//int
decltype(Func(1)) var13 = true;//bool
decltype(i++) var14 = i;//int i++返回右值
var12 = 10;
var13 = false;
return 0;
}
P.S. C++11标准库中的模板类is_lvalue_reference
来判断表达式是否为左值。
cout << is_lvalue_reference<decltype(++i)>::value << endl;
结果是true表示左值,false表示右值。
四 掌握查看型别推导结果的方法
一般情况下可以通过IDE(我用的是VS2017社区版)和编译器的报错信息,知道型别是什么,这里不展开来说了。
这里主要说一下运行时的输出。
一般情况下可以通过typeid返回的type_info中的type_info::name()来输出输出成员信息,但是这样的输出不一定是对的。因为type_info::name()在返回的时候会去除掉const、volatile的饰词信息,也回去除引用的信息,这样无论是const int还是int &,或者是const/volatile int &都只返回int。
//1
int x = 10;
const type_info &obj = typeid(x);
cout << obj.name();
//2
cout << typeid(x).name() << endl;
还可以使用boost::typeindex::type_id_with_cvr::pretty_name()
来返回正确的型别。