1. auto
C++11之前auto和static是对应的,表示变量是自动存储的,但是非static的局部变量默认都是自动存储的,因此这个关键字变得非常鸡肋, 在C++11中他们赋予了新的含义,使用这个关键字能够像别的语言一样自动推导出变量的实际类型。
1.1 推导规则
-
使用auto声明的变量必须要进行初始化,以让编译器推导出它的实际类型,在
编译时
将auto占位符替换为真正的类型。 -
当变量不是指针或者引用类型时,推导的结果中不会保留const、volatile关键字
-
当变量是指针或者引用类型时,推导的结果中会保留const、volatile关键字
1.2 auto的限制
- 不能作为函数参数使用。因为只有在函数调用的时候才会给函数参数传递实参,auto要求必须要给修饰的变量赋值,因此二者矛盾。
int func(auto a, auto b) // error { cout << "a: " << a <<", b: " << b << endl; }
- 不能使用auto关键字定义数组
int func() { int array[] = {1,2,3,4,5}; // 定义数组 auto t1 = array; // ok, t1被推导为 int* 类型 auto t2[] = array; // error, auto无法定义数组 auto t3[] = {1,2,3,4,5};; // error, auto无法定义数组 }
- 无法使用auto推导出模板参数
template <typename T> struct Test{} int func() { Test<double> t; Test<auto> t1 = t; // error, 无法推导出模板类型 return 0; }
1.3 auto的应用
下面列举几个比较常用的场景:
用于STL的容器遍历。
在C++11之前,定义了一个stl容器之后,遍历的时候常常会写出这样的代码:
#include <map> int main() { map<int, string> person; map<int, string>::iterator it = person.begin(); for (; it != person.end(); ++it) { // do something } return 0; }
可以看到在定义迭代器变量 it 的时候代码是很长的,写起来就很麻烦,使用了auto之后,就变得清爽了不少:
#include <map> int main() { map<int, string> person; // 代码简化 for (auto it = person.begin(); it != person.end(); ++it) { // do something } return 0; }
用于泛型编程
在使用模板的时候,很多情况下我们不知道变量应该定义为什么类型,比如下面的代码:
#include
class T1 { public: static int get() { return 10; } }; class T2 { public: static string get() { return "hello, world"; } }; template <class A> void func(void) { auto val = A::get(); cout << "val: " << val << endl; } int main() { func<T1>(); func<T2>(); return 0; }
在这个例子中定义了泛型函数func,在函数中调用了类A的静态方法 get() ,这个函数的返回值是不能确定的,如果不使用auto,就需要再定义一个模板参数,并且在外部调用时手动指定get的返回值类型,具体代码如下:
#include <iostream> #include <string> using namespace std; class T1 { public: static int get() { return 0; } }; class T2 { public: static string get() { return "hello, world"; } }; template <class A, typename B> // 添加了模板参数 B void func(void) { B val = A::get(); cout << "val: " << val << endl; } int main() { func<T1, int>(); // 手动指定返回值类型 -> int func<T2, string>(); // 手动指定返回值类型 -> string return 0; }
1.4 范围for
对应基于范围的for循环来说,冒号后边的表达式只会被执行一次。在得到遍历对象之后会先确定好迭代的范围,基于这个范围直接进行遍历。如果是普通的for循环,在每次迭代的时候都需要判断是否已经到了结束边界。
#include <iostream> #include <vector> using namespace std; vector<int> v{ 1,2,3,4,5,6 }; vector<int>& getRange() { cout << "get vector range..." << endl; return v; } int main(void) { for (auto val : getRange()) { cout << val << " "; } cout << endl; return 0; }
get vector range... 1 2 3 4 5 6
2. decltype
在某些情况下,不需要或者不能定义变量,但是希望得到某种类型,这时候就可以使用C++11提供的
decltype
关键字了,它的作用是在编译器编译的时候推导出一个表达式的类型
//语法格式 decltype (表达式)
decltype 是“declare type”的缩写,意思是“声明类型”。
decltype的推导是在编译期完成
的,它只是用于表达式类型的推导,并不会计算表达式的值。
一组简单的例子:
int a = 10; decltype(a) b = 99; // b -> int decltype(a+3.14) c = 52.13; // c -> double decltype(a+b*c) d = 520.1314; // d -> double
可以看到decltype推导的表达式可简单可复杂,在这一点上auto是做不到的,auto只能推导已初始化的变量类型。
2.1 推导规则
通过上面的例子我们初步感受了一下 decltype 的用法,但不要认为 decltype 就这么简单,在它简单的背后隐藏着很多的细节 分三个场景依次讨论一下:
表达式为普通变量或者普通表达式或者类表达式,在这种情况下,使用decltype推导出的类型和表达式的类型是一致的。
#include <iostream> #include <string> using namespace std; class Test { public: string text; static const int value = 110; }; int main() { int x = 99; const int &y = x; decltype(x) a = x; decltype(y) b = x; decltype(Test::value) c = 0; Test t; decltype(t.text) d = "hello, world"; return 0; }
- 变量a被推导为 int类型
- 变量b被推导为 const int &类型
- 变量c被推导为 const int类型
- 变量d被推导为 string类型
表达式是函数调用,使用decltype推导出的类型和函数返回值一致。
class Test{...}; //函数声明 int func_int(); // 返回值为 int int& func_int_r(); // 返回值为 int& int&& func_int_rr(); // 返回值为 int&& const int func_cint(); // 返回值为 const int const int& func_cint_r(); // 返回值为 const int& const int&& func_cint_rr(); // 返回值为 const int&& const Test func_ctest(); // 返回值为 const Test //decltype类型推导 int n = 100; decltype(func_int()) a = 0; decltype(func_int_r()) b = n; decltype(func_int_rr()) c = 0; decltype(func_cint()) d = 0; decltype(func_cint_r()) e = n; decltype(func_cint_rr()) f = 0; decltype(func_ctest()) g = Test();
- 变量a被推导为 int类型
- 变量b被推导为 int&类型
- 变量c被推导为 int&&类型
- 变量d被推导为 int类型
- 变量e被推导为 const int &类型
- 变量f被推导为 const int &&类型
- 变量g被推导为 const Test类型
函数 func_cint()
返回的是一个纯右值(在表达式执行结束后不再存在的数据,也就是临时性的数据)
对于纯右值而言,只有类类型可以携带const、volatile限定符,除此之外需要忽略掉这两个限定符
因此推导出的变量d的类型为 int 而不是 const int。
表达式是一个左值,或者被括号( )包围,使用 decltype推导出的是表达式类型的引用(如果有const、volatile限定符不能忽略)。
#include <iostream> #include <vector> using namespace std; class Test { public: int num; }; int main() { const Test obj; //带有括号的表达式 decltype(obj.num) a = 0; decltype((obj.num)) b = a; //加法表达式 int n = 0, m = 0; decltype(n + m) c = 0; decltype(n = n + m) d = n; return 0; }
- obj.num 为类的成员访问表达式,符合场景1,因此 a 的类型为int
- obj.num 带有括号,符合场景3,因此b 的类型为 const int&。
- n+m 得到一个右值,符合场景1,因此c的类型为 int
- n=n+m 得到一个左值 n,符合场景3,因此d的类型为 int&
2.2 decltype的应用
关于
decltype
的应用多出现在泛型编程中。比如我们编写一个类模板,在里边添加遍历容器的函数
#include <list> using namespace std; template <class T> class Container { public: void func(T& c) { for (m_it = c.begin(); m_it != c.end(); ++m_it) { cout << *m_it << " "; } cout << endl; } private: ??? m_it; // 这里不能确定迭代器类型 }; int main() { const list<int> lst; Container<const list<int>> obj; obj.func(lst); return 0; }
在程序的???行出了问题,关于迭代器变量一共有两种类型: 只读(T::const_iterator)和读写(T::iterator) 有了decltype就可以完美的解决这个问题了 当 T 是一个 非 const 容器得到一个 T::iterator 当 T 是一个 const 容器时就会得到一个 T::const_iterator。
#include <list> #include <iostream> using namespace std; template <class T> class Container { public: void func(T& c) { for (m_it = c.begin(); m_it != c.end(); ++m_it) { cout << *m_it << " "; } cout << endl; } private: decltype(T().begin()) m_it; // 这里不能确定迭代器类型 }; int main() { const list<int> lst{ 1,2,3,4,5,6,7,8,9 }; Container<const list<int>> obj; obj.func(lst); return 0; }
3. 返回类型后置
在泛型编程中,可能需要通过参数的运算来得到返回值的类型
比如
#include <iostream> using namespace std; // R->返回值类型, T->参数1类型, U->参数2类型 template <typename R, typename T, typename U> R add(T t, U u) { return t + u; } int main() { int x = 520; double y = 13.14; // auto z = add<decltype(x + y), int, double>(x, y); auto z = add<decltype(x + y)>(x, y); // 简化之后的写法 cout << "z: " << z << endl; return 0; }
关于返回值,从上面的代码可以推断出和表达式t+u
的结果类型是一样的,因此可以通过decltype进行推导
关于模板函数的参数t和u可以通过实参自动推导出来,因此在程序中就也可以不写。
虽然通过上述方式问题被解决了,但是解决方案有点过于理想化,因为对于调用者来说,是不知道函数内部执行了什么样的处理动作的。
因此如果要想解决这个问题就得直接在 add 函数身上做文章,先来看第一种写法:
template <typename T, typename U> decltype(t+u) add(T t, U u) { return t + u; }
当我们在编译器中将这几行代码改出来后就直接报错
了,因为decltype中的 t 和 u 都是函数参数,直接这样写相当于变量还没有定义就直接用上了,这时候变量还不存在
C++11中增加了返回类型后置语法,说明白一点就是将decltype和auto结合起来
完成返回类型的推导。
// 语法格式 // 符号 -> 后边跟随的是函数返回值的类型 auto func(参数1, 参数2, ...) -> decltype(参数表达式)
auto 会追踪 decltype() 推导出的类型
因此上边的add()函数可以做如下的修改:
#include <iostream> using namespace std; template <typename T, typename U> // 返回类型后置语法 auto add(T t, U u) -> decltype(t+u) { return t + u; } int main() { int x = 520; double y = 13.14; // auto z = add<int, double>(x, y); auto z = add(x, y); // 简化之后的写法 cout << "z: " << z << endl; return 0; }
为了进一步说明再看一个例子:
#include <iostream> using namespace std; int& test(int &i) { return i; } double test(double &d) { d = d + 100; return d; } template <typename T> // 返回类型后置语法 auto myFunc(T& t) -> decltype(test(t)) { return test(t); } int main() { int x = 520; double y = 13.14; // auto z = myFunc<int>(x); auto z = myFunc(x); // 简化之后的写法 cout << "z: " << z << endl; // auto z = myFunc<double>(y); auto z1 = myFunc(y); // 简化之后的写法 cout << "z1: " << z1 << endl; return 0; }
在这个例子中,通过decltype结合返回值后置语法很容易推导出来 test(t)函数可能出现的返回值类型,并将其作用到了函数myFunc()上。
// 输出结果
z: 520 z1: 113.14