12C++11新手易学,老兵易用
1. auto 关键字的限制
auto 虽然功能很强大,但是也不是万能的,受制于语法的二义性,或者是实现的困难性, auto 往往也会有使用上的限制。
#include <vector>
//1.auto 函数参数,无法通过编译
void fun(auto x = 1) {}
struct str
{
//2.auto 非静态成员变量,无法通过编译
auto var = 10;
}
int main()
{
char x[3];
auto y = x;
//3.auto 数组,无法通过编译
auto z[3] = x;
//4.auto 模板参数(实例化时),无法通过编译
vector<auto> v = { 1 };
reutrn 0;
}
2. decltype
2.1 引入
c++98 对类型推导的支持采用运行时类型识别的机制。
void testDecltype()
{
cout << "enter testDecltype()...................................................................." << endl;
While a;
Black b;
cout << "typeid(a).name():" << typeid(a).name() << endl; //typeid(a).name() :class While
cout << "typeid(a).hash_code():" << typeid(a).hash_code() << endl; //typeid(a).hash_code() :2404998906(每次运行返回的值都是一样的)
cout << "typeid(b).name():" << typeid(b).name() << endl; //typeid(b).name() : class Black
cout << "typeid(b).hash_code():" << typeid(b).hash_code() << endl; //typeid(b).hash_code() :2071569968
While c;
cout << "typeid(c).name():" << typeid(c).name() << endl; //typeid(c).name() : class While
cout << "typeid(c).hash_code():" << typeid(c).hash_code() << endl; //typeid(c).hash_code() :2404998906
MyStruct stru;
MyStruct1 stru1;
cout << "typeid(c).name():" << typeid(stru.a).name() << endl; //typeid(c).name() : int
cout << "typeid(stru.c).name():" << typeid(stru.c).name() << endl; //typeid(stru.c).name() : char
cout << "typeid(c).name():" << typeid(stru1.m).name() << endl; //typeid(c).name() : struct MyStruct
}
decltype 的类型推导并不是像 auto 一样是从变量声明的初始化标识获得变量的类型, decltype 总是以一个普通的表达式为参数,返回该表达式的类型。
而与 auto 相同的是,作为一个类型指示符, decltype 可以将获得的类型来定义另外一个变量。
另外,与 auto 相同, decltype 类型推导也是在编译时进行的。
2.2 应用
在 c++11 中,使用 decltype 推导类型是非常常见的事情。比较典型的就是 decltype 与 tydef/using 的合用。在 c++11 头文件中,我们常能看到以下这样的代码:
using size_t = decltype(sizeof(0));
using ptrdiff_t = decltype((int*)0 - (int*0));
using nullptr_t = decltype(nullptr);
在 c++ 中,我们有时会遇到匿名的类型,而拥有了 decltype 这个利器之后,重用匿名类型也并非难事。
struct
{
int a;
} stru;
void test()
{
decltype(stru) stru1;
stru1.a = 11;
}
事实上,在一些 c 代码中,匿名的结构体和联合体并不少见。不过匿名一般都有匿名理由,一般程序员都不希望匿名后的类型被重用。这里 decltype 只是提供了一种语法上的可能。
2.3 推导四规则
大多时候, decltype 的使用看起来非常平易近人,但是有时我们也会落入一些令人疑惑的陷阱。
int i;
decltype(i) a; //a:int
decltype((i)) b; //b: int& 无法通过编译
decltype((i)) g = i; // 可以通过编译
具体的,当程序员用 decltype(e) 来获取类型时,编译器将依序判断以下四规则:
-
(1)如果 e 是一个没有带括号的标记符表达式或者类成员访问表达式,那么 decltype(e) 就是 e 所命名的实体的类型。此外,如果 e 是一个被重载的函数,则会导致编译时错误。
-
(2)否则,假设 e 的类型是 T , 如果 e 是一个将亡值,那么 decltype(e) 为 T&&。
-
(3)否则,假设 e 的类型是 T,如果 e 是一个左值, 则 decltype(e) 为 T&。
-
(4)否则,假设 e 的类型是 T,则 decltype 为 T。
在上面的代码中,decltype(i) a; 使用了推导规则1——因为 i 是一个标记符表达式(基本上,所有除去关键字、字面量量 等编译器需要使用的标记之外的程序员自定义的标记都可以是标记符。而单个标记符对应的表达式就是标记符表达式),所以类型被推导为 int。而 decltype((i)) b; 中,由于 (i) 不是一个标记符表达式,但却是一个左值表达式(可以由具名的地址),因此,按照推导规则3,器类型应该是一个 int&。
int arr[5] = { 0 };
int* ptr = arr;
struct S
{
double d;
}s;
void overloaded(int);
void overloaded(char);
int && rValRef();
const bool foo(int);
//规则1:单个标记符表达式以及访问类成员,推导为本类型
decltype(arr) var1; //int[5], 标记符表达式
decltype(ptr) var2; //int*, 标记符表达式
decltype(s.d) var4; //double, 成员访问表达式
//decltype(overloaded) var5; //无法通过编译,是个重载函数
//规则2: 将亡值,推导为类型的右值引用
decltype(rValRef()) var6 = 1; // int&&
//规则3: 左值,推导为类型的引用
decltype(true ? i : i) var7 = i; //int&, 三元运算符,这里返回一个 i 的左值
decltype((i)) var8 = i; //int&, 带圆括号的左值
decltype(++i) var9 = i; //int&, ++i 返回 i 的左值
decltype(arr[3]) var10 = i; //int&, [] 操作返回左值
decltype(*ptr) var11 = i; //int&, *操作返回左值
decltype("lval") var12 = "lval"; //const char(&)[5],字符串字母常量为左值
//规则4:以上都不是,推导为本类型
decltype(1) var13; //int, 除字符串外字面常量值为右值
decltype(i++) var14; //int, i++ 返回右值
decltype((foo(1))) var15; //const bool, 圆括号可以忽略
使用 decltype 时,一个简单的能够让编译器提示的方法是:先声明这个变量,再在其他语句里对其进行初始化。这样一来,由于左值引用总是需要初始化的,编译器会报错提示。
2.4 cv 限制符的继承与冗余的符号
与 auto 类型推导时不能 “带走” cv 限制符不同, decltype 是能够 “带走” 表达式的 cv 限制符的。不过,如果对象的定义中有 const 或 volatile 限制符,使用 decltype 进行推导时,器成员不会继承 const 或则 volatile 限制符。
const int ic = 0;
volatile int iv;
struct SS { int i; };
const SS a = { 0 };
volatile SS b;
volatile SS* pp = &b;
cout << is_const<decltype(ic)>::value << endl; //1
cout << is_volatile<decltype(iv)>::value << endl; //1
cout << is_const<decltype(a)>::value << endl; //1
cout << is_volatile<decltype(b)>::value << endl; //1
cout << is_const<decltype(a.i)>::value << endl; //0, 成员不是 const
cout << is_volatile<decltype(pp->i)>::value << endl;//0 , 成员不是 volatile
3. 追踪返回类型
template<typename T1, typename T2>
auto sum(T1& t1, T2& t2) -> decltype(t1 + t2)
{
return t1 + t2;
}
void testReturnType()
{
int i = 1;
double d = 100.1;
auto ret = sum(i, d); //推导为 double
cout << "ret = " << ret << endl;
}
如上面的写法所示,我们把函数的返回值移至参数声明之后,复合符号 ->decltype(t1+t2) 被称为追踪返回类型。而原本函数返回值的位置由 auto 关键字占据。这样,我们就可以让编译器来推导 sum 函数模板的返回类型了。 而 auto 占位符和 ->return_type 也就构成追踪返回类型函数的两个基本元素。
//1.普通函数声明
int func(char* a, int b);
//2.追踪返回类型声明的函数
auto func(char* a, int b) -> int;
程序员在编写代码时无需关心任何时段的类型选择,编译器会合理地进行推导,而简单程序的书写也由此得到了极大的简化。
int(*(*pf())())()
{
return nullptr;
}
//auto (*)() -> int(*)(); // 一个返回函数指针的函数(假设为 a 函数)
//auto pf1() -> auto (*)() -> int(*)(); //一个返回 a 函数的指针的函数
auto pf1() -> auto(*)() ->int(*)()
{
return nullptr;
}
void testReturnType()
{
cout << is_same<decltype(pf), decltype(pf1)>::value << endl; //1
}
上面的例子中,pf 和 pf1 是两个类型完全一样的函数。其返回值都是一个函数指针。而该函数指针又指向一个返回函数指针的函数。这一点通过 is_same 的成员 value 已经能够确定。而仔细一看函数类型的声明,可以发现老式的声明法可读性非常差。而追踪返回类型只需要依次从右向左的方式,就可以将嵌套的声明解析出来。这大大提高了嵌套函数这类代码的可读性。
4. 基于范围的 for 循环
//范围for循环可以很方便的用来遍历数组以及容器
int arr[5] = { 1,2,3,4,5 };
for (auto& e : arr)
{
e *= 2;
}
for (const auto& e: arr)
{
cout << e << '\t';
}