Mythsman


乐极生悲,苦尽甘来。


C++11学习笔记1

前言

很久以前就知道C++11对我们课上讲的C++有很多改进的地方,当时也没有细学,最近一个偶然的机会陡然发现原来身边的同学好多都对C++11都颇有心得,推崇备至,回头想想在C++14,甚至C++17都不新鲜的现在,连C++11都不会的话显然有点说不过去了。。。于是呢我就打算利用最近闲着的时间,在补《人民的名义》的间隙顺便学学C++11应该也是极好的。

类型推导

auto关键字

目的

auto关键字不是C++11里诞生的关键字,在这之前,auto代表的意思是“具有全局存储期的局部变量”,限定的是变量的作用域。显然,这玩意并没什么用,于是在C++11里,他就变成了可以自动推导的变量类型。所谓自动推导,并不意味着他就跟javascript里的var一样,颠覆了C++强类型语言的性质,他其实只是在编译的过程中由电脑来推导变量类型,在运行时是不存在auto这种变量的。

用法

我们可以用auto声明其他的变量,但是需要注意下面几点:

  1. auto 声明的变量必须在编译阶段就能识别类型。
  2. auto 不能声明非静态成员变量。
  3. auto 不能用来声明函数参数。
  4. auto 的自动推导于模板的自动推导(Template argument deduction)本质相同。
  5. 当auto不被声明为指针或引用时,auto的推导结果将和初始化表达式的抛弃ref(引用)和cv(const volatile)限定符的类型一致。
  6. 当auto被声明为引用或指针时,auto的推导结果将继承初始化表达式的cv限定符。

不过细想,有时候还是有点绕人的,比如:

int x=0;

auto * x1 = &x;  //x1 -> int *, auto -> int
auto x2 = &x;    //x2 -> int *, auto -> int *

明明写法不一样,但是推导出来的x1和x2的类型竟然是一样的。这看上去有点不可思议,不过换一个看法估计就更清楚一点:

int x=0;
template<typename T>func(T *x1);
func(&x);

template<typename T>func(T x2);
func(&x);

对于x1,T被推导成了int,因此x1被推导成了int ;
对于x2,T被推导成了int,因此x2也被推导成了int *;

这很好理解,那么auto也就很好理解了。(虽然还是有点怪怪的感觉。。。)

用途

  1. 简化代码。
  2. 判断泛型中的变量类型。

他出现的目的,主要是由于我们懒得写那些巨长无比的类型名,最典型的用处就是在声明迭代器的时候:

std::map<double,double> mp;
for(std::map<double,double>::iterator it=mp.begin(),it!=mp.end();it++){
    .....
}

这迭代器的声明太傻了,我们不喜欢,我们喜欢下面的样子:

std::map<double,double> mp;
for(auto it=mp.begin(),it!=mp.end();it++){
    .....
}

当然,有时候我们用这个也的确是因为我们无法判断真正的类型是什么,比如:

class A{
    static int func(){}
};
class B{
    static int* func(){}
};
template<class T>
void work(){
    auto val=T::func():
    ...
}
int main(){
    work<A>();
    work<B>();
}

我们希望work函数能统一处理所有叫func的静态函数,但是由于返回值不同,所以我们不能直接指定val的类型,但是用auto就能暂时确定val的类型,用于后续处理。

decltype关键字

目的

decltype关键字使用来在编译时推导出一个表达式的类型,通常用这个结果来声明另一个变量。

用法

参照下面的例子:

struct A{
    int x;
};
decltype(A.x) t1=2; //t1 -> int

int a=0,b=0;
decltype(a+b) t2=2; //t2 -> int
decltype(a+=b) t3=2; //t3 ->int &

const int &c=a;
decltype(c) t4=3; // t4 -> const int &;

int func(){}
decltype(func()) t5=2; //t5 -> int 

需要注意的是当decltype的参数是函数的时候,他并不需要执行这个函数,毕竟这是在编译阶段完成的事情嘛。

用途

虽然decltype在泛型中有一些重要的用法,但是最常用的用一个类型来定义另一个类型:

typedef decltype(nullptr) nullptr_t;
typedef decltype(sizeof(0)) size_t;

这种重新定义类型的方法更加直观。

返回类型后置语法

目的

有时候我们在用模板函数的时候无法指定函数的返回值,需要通过一些参数的运算才能获得返回值类型,这时候就需要返回类型后置语法来处理了。

用法&用途

比如下面的场景:

int& foo(int &i);
float foo(float *i);

template<typename T>
??? func(T& val){
    return foo(val);
}

显然,我们不能将???处简单的用T来替换,这看上去就很无解了,这时候就需要auto+decltype来共同处理了:

int& foo(int &i);
float foo(float *i);

template<typename T>
auto func(T& val)-> decltype(foo(val)){
    return foo(val);
}

模板的优化

右尖号的细节处理

我们知道,在C++11之前,下面的声明是有问题的:

#include<vector>
vector<vector<int>>v;

编译器会报错,他把俩右尖号看成是位移符了,我们得改成这样:

#include<vector>
vector<vector<int> >v;

在C++11之后,设计者考虑了这个因素,第一种的写法也能够认了。不过带来一个小bug就是下面的语法行不通了:

vector< 16>>2 >v;

虽然没人这样写,但是他编译的时候的确会报错。实际写的时候还是加括号比较稳妥一点。

模板别名

c++11引了using的别名语法,事实上就是typedef的加强版。基本操作如下:

using uint=unsigned int ;
using func_t=void (*)(int,int);

这不新鲜,新鲜的是using语法甚至定义泛型,这是typedef所做不到的:

#include<map>
template<typename T>
using mp_str=std::map<string,T>;

void work(){
    mp_str<int>mp1;
    mp_str<string>mp2;
}

这个用法就像生成一个新模板一样将map的key的类型固定,只露出了值的类型。

基于范围的for循环(Range-for statement)

我们知道在python之类的语言里都支持类似for i in arr这样的for循环语法,这种语法相比显式用下标和迭代器来循环更加简洁。虽然在algorithm头文件里有一个for_each函数做到了类似的效果,但是他仍然需要显式地指定begin()和end(),并不是真正意义上的基于范围。
在C++11里引入了基于范围的for循环,它可以支持迭代任何容器、数组、初始化列表等类型。
具体方法如下:

int a[]={2,3,4};
for(auto i :a){
    ...
}

std::vector<int>v={1,2,3};
for(auto i : v){
    ...
}

for(auto i : {1,2,3,4}){
    ...
}

除了能循环数组,vector这类容器之外,还能迭代map等特殊容器,不过需要注意的是,对于map来说,我们获得的迭代变量是pair类型的,因此我们需要用ite.first、ite.second来获得键和值:

std::map<int,int>m={{1,2},{3,4}};
for(auto ite : m){
    std::cout<< ite.first ;
    std::cout<< ite.second ;
}

同时,这个迭代变量还可以声明成引用类型,也就是说我们可以通过他对原数组进行修改:

#include<iostream>
int main(){
    int a[]={1,2,3};
    for(auto &i : a){
        i*=2;
    }
    for(auto i : a){
        std::cout<<i<<std::endl;
    }
}

输出结果是:

2
4
6

参考资料

auto
基于模板的参数推断
decltype
类型别名
深入应用c++11
C++FAQ