C++11中的 auto 和 decltype 关键字
0x1 auto 关键字
在C++程序中,每个变量都有自己的类型,这就要求变量在声明的时候必须清楚地知道其右侧初始值的类型。然而做到这一点并非易事,有时候甚至无法做到。在C++11之前的版本中,变量在声明或者初始化的时候,必须显式的指定其类型,但是在C++11中引入了 auto 关键字,用它可以让编译器替我们去分析初始值的类型来推算变量的类型。
auto 的语法规则
auto 是让编译器根据初始值来推断所定义的变量的类型,在 auto 的推断过程中,一般遵循四个原则:
- 同一条声明语句只能有一个基本数据类型,所以该语句中所有变量的初始基本数据类型都必须一样。
- 符号
&
和*
只从属于某个声明符,而非基本类型的一部分。 - 当引用被当做初始值的时候,真正用于推断 auto 类型的初始值实际上是引用对象的值。
- 当用于推断 auto 类型的初始值是常量时,如果可以忽略其常量性质,则忽略其常量性质。
int i = 0;
const int c1 = i;
const int &c2 = i;
const int *p2 = &i;
int *const p3 = &i;
// 前两条规则的示例
auto j = 0, *p = &j; //j的类型是int,p的类型是int *,该语句的基本数据类型是int
auto sz = 0, pi= 3.14; //错误,sz和pi的类型不同
auto &n = c1, *p1 = &c1; //n的类型是const int &,p1的类型是const int *,该语句的基本数据类型是const int
// 后两条规则的示例
auto a = i; //i是一个int型变量,所以a是int型
auto b = c1; //c1是一个const int型的常量,初始值本身是个常量,故忽略其常量性质,所以b是int型
auto c = c2; //c2是一个const int &型的引用,所以实际上的初始值是引用对象c1,故c是int型
auto d = &c1; //c1是一个const int型的常量,对其取地址得到的const int*型的初始值,注意该初始值本身并不是一个常量,而是其指向的对象是常量,故d是const int*型
auto e = p2; //p2是一个const int*型,初始值并不是常量,故e是const int*型
auto f = p3; //p3是一个int *类型的常量指针,所以忽略其常量性质,则f是int *类型
auto &g = c1; //c1是一个const int型的常量,如果忽略其常量性质,则得到g是与从绑定的int &型变量,显然违背了基本原则,所以该处不能忽略c1的常量性质,则g是const int &类型
auto的常见用法
1、 auto 用于代替冗长复杂的变量声明
// 不使用auto关键字,变量的类型冗长不宜读
#include <map>
int main(void)
{
std::unordered_multimap<int, int> resultMap;
// ...
std::pair<std::unordered_multimap<int,int>::iterator, std::unordered_multimap<int, int>::iterator> range = resultMap.equal_range(key);
return 0;
}
// 这个 equal_range 返回的类型声明显得烦琐而冗长,而且实际上并不关心这里的具体类型.
// 这时,通过 auto 就能极大的简化书写,省去推导具体类型的过程:
#include <map>
int main(void)
{
std::unordered_multimap<int, int> map;
// ...
auto range = map.equal_range(key);
return 0;
}
2、auto 用于泛型编程
// 在定义模板函数时,用于声明依赖模板参数的变量类型
template <typename T1,typename T2>
void add(T1 a, T2 b)
{
auto v = a+b;
std::cout << v;
}
0x2 decltype 关键字
有时候在编程时会遇到一种情况:希望从表达式的类型推断出要定义的变量的类型,但是不希望用该表达式的值初始化变量。C++11针对这种情况引用了 decltype 关键字,其作用是根据表达式返回数据类型。
decltype 的语法规则
decltype 的用法:
decltype(exp) varname = value;
跟据表达式推导出来的类型的必须初始化,比如说引用decltype(exp) varname;
根据表达式推导出来的类型声明变量可以不初始化,比如说int
decltype 在推导类型时完全依据其括号内 exp 的类型,其所遵循的规则可以总结为三点:
- 和 auto 相比,decltype 不忽视引用和 const ,如果 exp 是一个变量的话,则 decltype(exp) 的类型和 exp 的类型完全一致,包括 const 属性和引用在内。
- 如果 exp 是一个表达式,则 decltype(exp) 的类型和表达式返回的类型一致。
- 如果 exp 加上一组括号,即 decltype((exp)) 形式,则推断的类型必然是一个引用。
int i = 0;
int *p = &i;
int &r = i;
const int ci = 0, &cj = ci;
decltype(ci) a = 0; //a是const int型,因为ci是const int型
decltype(cj) b = a; //b是const int&型,因为cj是const int&型
decltype((i)) c1 = i; //c1是int &型,因为i是int型,且被括号包围
decltype(i) c2; //c2是int型,因为i是int型
decltype(*p) d = i; //d是int &型,因为p是int *型,对变量的解引用运算会返回对应引用
decltype(i+0) e; //e是int型,因为i+0表达式返回到是一个int型右值
decltype(i=i+1) f; //报错,f是int &型,必须初始化,因为赋值运算返回对象的引用
用 clang 语法树验证如下:
decltype的常见用法
#include <vector>
using namespace std;
template <typename T>
class Base {
public:
void func(T& container) {
m_it = container.begin(); //在运行时可能会出错,因为m_it的类型是T::iterator,但是begin()可能会返回const_iterator
}
private:
typename T::iterator m_it;
};
int main()
{
const vector<int> v;
Base<const vector<int>> obj;
obj.func(v);
return 0;
}
单独看 Base 类中 m_it 成员的定义,很难看出会有什么错误,但在使用 Base 类的时候,如果传入一个 const 类型的容器,编译器马上就会弹出错误信息,提示没有重载的操作符 =
。原因就在于,T::iterator
并不能包括所有的迭代器类型,当 T 是一个 const 容器时,应当使用 const_iterator。如果不使用decltype的话,则只能想办法把 const 类型的容器用模板特化单独处理,增加了不少工作量。但是有了 C++11 的 decltype 关键字,就可以直接这样写:
#include <vector>
using namespace std;
template <typename T>
class Base {
public:
void func(T& container) {
m_it = container.begin();
}
private:
decltype(T().begin()) m_it; //m_it的类型由begin()返回的类型决定
};
int main()
{
const vector<int> v;
Base<const vector<int>> obj;
obj.func(v);
return 0;
}
参考资料: 《C++ Prime》、C语言中文网