为什么需要类型推导
在我们编程的时候会发现:在定义变量时必须先给出变量的实际类型编译器才会允许定义,但是有些情况可能不知道实际需要类型怎么给,或者类型写起来特别复杂
#include <iostream>
#include <map>
#include <string>
using namespace std;
int main()
{
short a = 32670;
short b = 32670;
// c如果给成short,会造成数据丢失,如果能够让编译器根据a+b的结果推导c的实际类型,就不会存在问题
short c = a + b;
cout << c << endl;
std::map<std::string, std::string> m{ { "apple", "苹果" },{ "banana","香蕉" } };
// 使用迭代器遍历容器, 迭代器类型太繁琐
std::map<std::string, std::string>::iterator it = m.begin();
while (it != m.end())
{
cout << it->first << " " << it->second << endl;
++it;
}
return 0;
}
在C++11中,可以使用auto来根据变量初始化表达式类型来推导变量的实际类型。将以上程序中的c与it的类型转换为auto程序就可以正常输出,并且更加简洁。
但是auto使用的前提是:必须要对auto声明的类型进行初始化,否则编译器无法推导出auto的实际类型。但是有时候可能需要根据表达式运行完成后的结果类型进行推导,因为编译期间代码不会运行,此时auto也就无能为力
template<class T1, class T2>
T1 Add(const T1& left, const T2& right)
{
return left + right;
}
像这里如果能用加完之后结果的实际类型作为函数的返回值类型就不会出错,但这需要程序运行完才能知道结果的实际类型,即RTTI(Run-Time Type Identifification 运行时类型识别)。
C++98中确实已经支持RTTI:
- typeid只能查看类型不能用其结果类定义类型
- dynamic_cast只能应用于含有虚函数的继承体系中
运行时类型识别的缺陷是降低程序运行的效率。
decltype类型推导
decltype是根据表达式的实际类型推演出定义变量时所用的类型,比如:
-
推演表达式类型作为变量的定义类型
int main() { int a = 10; int b = 20; // 用decltype推演a+b的实际类型,作为定义c的类型 decltype(a+b) c; cout<<typeid(c).name()<<endl; return 0; }
-
推演匿名类型对象
struct { int _x; int _y; }pt; // 注意:pt的实际类型为结构体类型,但是该结构体为给出其类型的名字 // 此时如果想要定义与pt相同类型的对象,在C++98中无法实现 int main() { decltype(pt) p; cout<<typeid(p).name()<<endl; return 0; }
-
推演函数的类型
void* GetMemory(size_t size)
{
return malloc(size);
}
int main()
{
// 如果没有带参数,推导函数的类型
cout << typeid(decltype(GetMemory)).name() << endl;
// 如果带参数列表,推导的是函数返回值的类型,注意:此处只是推演,不会执行函数
cout << typeid(decltype(GetMemory(0))).name() <<endl;
return 0;
}
decltype推导的四个规则
假设decltype需要推导表达式e的类型,如果最后推导出来的类型和用户原期望有差别,可参考以下规则:
-
如果e是一个没有带括号的标记符表达式或者类成员访问表达式,那么decltype(e)就是e所命名的实体的 类型。如果e是一个被重载的函数,则会编译失败。 标记符表达式:单个标记符对应的表达式即为标记符表达式。(除去关键字、字面常量等编译器需要用到的标记)比如: int arr[3]; arr为标记符表达式,但是arr[0] + 3和arr[3]就不是。
struct Point { int _x; int _y; }; int Add(int left, int right) { return left + right; } int Add(int left, int middle, int right) { return left + middle + right; } int main() { int arr[5] = { 0 }; Point pt; //单个标记符表达式和类成员访问符,推导为本类型 decltype(arr) var1; // 单个标记表达式 decltype(pt._x) var2; // 成员访问表达式 //decltype(Add) var3; // 函数重载 编译失败 return 0; }
-
否则,假设e的类型是T,如果e是将亡值(比如临时变量或者函数局部变量),decltype(e)被推导为T&&
string&& GetString()
{
return "hello";
}
int main()
{
// 检测decltype推导的GetString()函数的返回值类型是否为右值引用
cout << std::is_rvalue_reference<decltype(GetString())>::value << endl;
return 0;
}
-
否则,假设e的类型是T,如果e是一个左值,则decltype(e)为T的引用
int main() { int a = 10; // ++a的结果为一个左值, b为a的引用 decltype(++a) b = a; b = 20; cout << a <<","<< b << endl; return 0; }
-
否则,假设e的类型是T,则decltype(e)为T
int main() { int a = 10; // ++a的结果为一个右值 decltype(a++) b = a; b = 20; cout << a <<","<< b << endl; return 0; }
注意:规则三稍微复杂点,通过C++11中的类模板is_lvalue_reference来检测类型是否为右值引用类型。
返回值类型追踪
通过对decltype的了解,上述Add模板函数可以通过以下方式解决:
template<class T1, class T2>
void Add(const T1& left, const T2& right, decltype(left+right)& ret)
{
ret = left + right;
}
通过参数将返回值结果带出虽然可以,但是在调用时,必须传递第三个参数,而该参数的类型还需要用户去确认,使用起来不是很方便。
那么能否将上述第三个参数放在返回值类型的位置?
template<class T1, class T2>
decltype(left+right) ret Add(const T1& left, const T2& right)
{
ret = left + right;
}
但遗憾的是在编译器推导decltype(left+right)的类型时,由于left和right都未声明,虽然近在咫尺,却无法使 用,因为按照C/C++编译器规则,变量必须先声明再使用。
返回值类型追踪:把函数的返回值移至参数声明之后,复合符号->decltype(left+right)被称为:追踪返回类型。原本函数返回值位置由auto占据,这样就可让编译器来推导函数的返回值类型了。
template<class T1, class T2>
auto Add(const T1& left, const T2& right)->decltype(left + right)
{
return left + right;
}