理解C++ 11新特性

auto & decltype

关于类型推导,C++11引入了auto和decltype关键字,使用它们可以在编译期就推导出变量或者表达式的类型,方便开发者编码也简化了代码。

auto

用于声明变量的生存期为自动,即将不在任何类、结构、枚举、联合和函数中定义的变量视为全局变量,而在函数中定义的变量视为局部变量。

auto让编译器在编译时可推导出变量的类型,通过 = 右边的类型推导出变量的类型;自动类型推断发生在编译期,所以使用 auto 关键字不会降低程序的运行效率。

C程序是面向过程的,在C代码中会出现大量的函数模块,每个函数都有其生命周期(也称作用域),在函数生命周期中声明的变量通常叫做局部变量,也叫自动变量。

整型变量a在fun函数内声明,其作用域为fun函数内,出来fun函数,不能被引用,a变量为自动变量。也就是说编译器会有int a = 10之前会加上auto的关键字。

auto的出现意味着,当前变量的作用域为当前函数或代码段的局部变量,意味着当前变量会在内存栈上进行分配。

使用auto进行类型推断

  1. 普通类型的推断
    auto x = 127; // auto 被推断为 int 类型
    auto y = 3.14; // auto 被推断为 double 类型
    auto z; // 错误,必须有初始值才能进行推断
    
    class A {
        auto a = 1; // 错误,在类中auto不能用作非静态成员变量
        static auto b = 1; // 错误,这里与auto无关,正常static int b = 1也不可以
        static const auto c = 1; // ok
    };
    vector<auto> f = d; // 错误,auto无法推导出模板参数
    
  2. 引用类型推断
    int x = 0, &rx = x;
    auto a1 = rx; // 使用引用其实是使用引用的对象, 此时auto以引用对象的类型作为auto的类型, 所以auto这里被推断为int
    auto &a2 = rx; // 此时 auto 被推断为 int 类型,a2 对象本身就是 int & 类型
    const auto &a3 = rx; // auto 被推断为 int 类型,a3 对象本身是 const int & 类型,不能通过 a3 去修改 rx 引用的对象值
    
  3. const类型的推断
    const int c = 10;
    auto a1 = c; // auto 此时是 int,顶层 const 属性被丢弃
    auto &a2 = c; // auto 此时是 const int,底层 const 属性保留
    
  4. 针对数组和函数的推断
    const char arr[] = "I Love China";
    auto r1 = arr; // 如果将数组名赋值给auto变量, 那么auto推断的结果是指针类型, 如果有const属性
    // 会被保留, auto推断的结果是 const char *, r1为const char *
    auto &r2 = arr; // 如果将数组名赋值给 auto & 变量, auto & 变量的类型是一个数组引用类型, 即为
    // const char (&)[14]
    int add(int a, int b); // 函数声明
    auto r3 = add; // r3为 int(*)(int, int)
    auto &r4 = add; // r4为 int(&)(int, int)
    void func(auto value) {} // 错误,auto不能用作函数参数
    

推断规则说明

① 对于普通类型推断(没有 const 也不掺杂引用的),直接根据表达式右边值的类型进行推断,注意必须有初始值

② 使用引用进行推断,实际上就是使用引用对象的类型进行推断,此时引用特性会被丢弃,如果定义的对象需要成为引用,就自己在 auto 后面加上 & 来定义对象

③ 使用带有 const 属性的对象进行推断时,如果定义的对象不是指针或者引用,则 const 属性会被丢弃,否则 const 属性会保留,见 const 类型的推断

auto关于CV的使用

首先介绍下cv是指 const 和 volatile

  • 在不声明为引用或指针时,auto会忽略等号右边的引用类型和cv限定
  • 在声明为引用或者指针时,auto会保留等号右边的引用和cv属性

auto关键字使用的场景

适用的场景

  1. 一些类型长度书写很长的,可以使用 auto 来简化,如

    for(std::vector<int>::iterator it = v.begin();it != v.end();++it)
    

    如果使用 auto 可以直接写为

    for(auto it = v.begin();it != v.end();++it)
    
  2. 当函数返回的值不确定时,可以使用 auto 做返回值类型,更加方便,编译器会根据返回值的类型推断 auto 的类型,这种语法是在 C++14 才出现的

    auto func() { // 这种写法在 C++14 是允许的
    	return 0;
    }
    

不适用的场景

  1. 函数参数类型不能是 auto 类型,比如

    // 是不允许的
    int add(auto a, auto b) { return a + b; }
    
  2. 类的成员变量不可以是 auto 类型。类的静态成员变量可以是 auto 类型的,但是需要使用 const 修饰,而且该变量的值在类内初始化

decltype

相对于auto用于推导变量类型,而decltype则用于推导表达式类型,这里只用于编译器分析表达式的类型,表达式实际不会进行运算。

int func() { return 0; }
decltype(func()) i; // i为int类型

int x = 0;
decltype(x) y; // y是int类型
decltype(x + y) z; // z是int类型

decltype关于CV的使用

decltype不会像auto一样忽略引用和cv属性,decltype会保留表达式的引用和cv属性

cont int &i = 1;
int a = 2;
decltype(i) b = 2; // b是const int&

decltype推导规则

对于decltype(exp)有

  • exp是表达式,decltype(exp)和exp类型相同
  • exp是函数调用,decltype(exp)和函数返回值类型相同
  • 其它情况,若exp是左值,decltype(exp)是exp类型的左值引用
int a = 0, b = 0;
decltype(a + b) c = 0; // c是int,因为(a+b)返回一个右值
decltype(a += b) d = c;// d是int&,因为(a+=b)返回一个左值

d = 20;
cout << "c " << c << endl; // 输出c 20

auto和decltype的配合使用

auto和decltype一般配合使用在推导函数返回值的类型问题上。下面这段代码

template<typename T, typename U>
return_value add(T t, U u) { // t和v类型不确定,无法推导出return_value类型
    return t + u;
}

上面代码由于t和u类型不确定,那如何推导出返回值类型呢,我们可能会想到这种

template<typename T, typename U>
decltype(t + u) add(T t, U u) { // t和u尚未定义
    return t + u;
}

这段代码在C++11上是编译不过的,因为在decltype(t +u)推导时,t和u尚未定义,就会编译出错,所以有了下面的叫做返回类型后置的配合使用方法:

template<typename T, typename U>
auto add(T t, U u) -> decltype(t + u) {
    return t + u;
}

返回值后置类型语法就是为了解决函数返回制类型依赖于参数但却难以确定返回值类型的问题。

左值和右值的概念

左值是可以放在赋值号左边可以被赋值的值,左值必须要在内存中有实体

右值当在赋值号右边取出值赋给其他变量的值,右值可以在内存也可以在CPU寄存器

众所周知C++11新增了右值引用,这里涉及到很多概念:

  • 左值:可以取地址并且有名字的东西就是左值。
  • 右值:不能取地址的没有名字的东西就是右值。
  • 纯右值:运算表达式产生的临时变量、不和对象关联的原始字面量、非引用返回的临时变量、lambda表达式等都是纯右值。
  • 将亡值:可以理解为即将要销毁的值。
  • 左值引用:对左值进行引用的类型。
  • 右值引用:对右值进行引用的类型。
  • 移动语义:转移资源所有权,类似于转让或者资源窃取的意思,对于那块资源,转为自己所拥有,别人不再拥有也不会再使用。
  • 完美转发:可以写一个接受任意实参的函数模板,并转发到其它函数,目标函数会收到与转发函数完全相同的实参。
  • 返回值优化:当函数需要返回一个对象实例时候,就会创建一个临时对象并通过复制构造函数将目标对象复制到临时对象,这里有复制构造函数和析构函数会被多余的调用到,有代价,而通过返回值优化,C++标准允许省略调用这些复制构造函数。

浅拷贝 & 深拷贝

在介绍右值引用等概念之前,先来认识下浅拷贝(shallow copy)和深拷贝(deep copy)

这里举个例子:

class Vector {
	int num;
	int* a;
public:
	void ShallowCopy(Vector& v);
	void DeepCopy(Vector& v);
}

浅拷贝:按位拷贝对象,创建的新对象有着原始对象属性值的一份精确拷贝(但不包括指针指向的内存)

//浅拷贝
void Vector::ShallowCopy(Vector& v){
    this->num = v.num;
    this->a = v.a;
    v.a = nullptr;
}

深拷贝:拷贝所有的属性(包括属性指向的动态分配的内存);换句话说,当对象和他所引用的对象一起拷贝时即发生深拷贝

//深拷贝
void Vector::DeepCopy(Vector& v){
    this->num = v.num;
    this->a = new int[num];
    for(int i=0;i<num;++i){a[i]=v.a[i]}
}

由此可见,深拷贝的开销往往比浅拷贝大(除非没有指向动态分配内存的属性),所以我们就倾尽可能使用浅拷贝

但是浅拷贝的有一个问题:当有指向动态分配内存的属性时,会造成多个对象共用这块动态分配内存,从而可能导致冲突。一个可行的办法是:每次做浅拷贝后,必须保证原始对象不再访问这块内存(即转移所有权给新对象),这样就保证这块内存永远只被一个对象使用——临时对象

左值 & 右值

C++在C++98时便遵循C模型,引入了左值、右值的概念。

  • 左值(left value):表达式结束后依然存在的持久对象
  • 右值(right value):表达式结束后就不再存在的临时对象

之所以取名左值右值,是因为在等式左边的值往往是持久存在的左值类型,在等式右边的表达式值往往是临时对象。

a = 152;
a = ++b;
a = b+c*2;
a = func();

字面量(字符字面量除外)、临时的表达式值、临时的函数返还值这些短暂存在的值都是右值。更直观的理解是:有变量名的对象都是左值,没有变量名的都是右值。(因为有无变量名意味着这个对象是否在下一行代码时依然存在)

值得注意的是,字符字面量是唯一不可算入右值的字面量,因为它实际存储在静态内存区,是持久存在的。

右值引用类型

有了左值、右值的概念,我们就很清楚认识到右值都是些短暂存在的临时对象。

于是,C++11 为了匹配这些左右值,引入了右值引用类型 && 。
右值引用类型负责匹配右值,左值引用则负责匹配左值。

因此刚刚的浅拷贝、深拷贝例子,我们可以无需显式调用浅拷贝或深拷贝函数,而是调用重载函数

//左值引用形参=>匹配左值
void Vector::Copy(Vector& v){
    this->num = v.num;
    this->a = new int[num];
    for(int i=0;i<num;++i){a[i]=v.a[i]}
}

//右值引用形参=>匹配右值
void Vector::Copy(Vector&& temp){
    this->num = temp.num;
    this->a = temp.a;
}

当然,最标准还是编写成各种构造函数(拷贝构造、移动构造、赋值构造、移动赋值构造):

移动的意思是转移所有权。由于右值都是临时的值(右值其实也不一定是临时值,后文会说到),临时值释放后也就不再持有属性的所有权,因此这相当于转移资源所有权的行为。

//拷贝构造函数:这意味着深拷贝
Vector::Vector(Vector& v){
    this->num = v.num;
    this->a = new int[num];
    for(int i=0;i<num;++i){a[i]=v.a[i]}
}
//移动构造函数:这意味着浅拷贝
Vector::Vector(Vector&& temp){
    this->num = temp.num;
    this->a = temp.a;
    temp.a = nullptr;    //实际上Vector一般都会在析构函数来释放指向的内存,所以需赋值空地址避免释放
}

虽然从优雅的实现深、浅拷贝这个目的开始出发,C++11的移动语义可以不止用于浅拷贝。得益于转移所有权的特性,我们还可以做其它事情,例如在右值所占有的空间临时存放一些东西。

强转右值 std::move

除了上面说的临时值,有些左值其实也很适合转移所有权:

void func(){
    Vector result;
    //...DoSomething with result
    if(xxx){ans = result;}  //现在我希望把结果提取到外部的变量a上。
    return;
}

可以看到result赋值给ans后就不再被使用,我们期望它调用的是移动赋值构造函数。
但是result是一个有变量名的左值类型,因此ans = result 调用的是赋值构造函数而非移动赋值构造函数。

为了将某些左值当成右值使用,C++11 提供了 std::move 函数以用于将某些左值转成右值,以匹配右值引用类型。

void func(){
    Vector result;
    //...DoSomething with result
    if(xxx){ans = std::move(result);}   //调用的是移动赋值构造函数
    return;
}

重新审视右值、右值引用

右值引用类型和右值的关系

有了上面的知识后,我们来重新审视一下右值引用类型。

先看看如下代码:

void test(Vector& o) {std::cout << "为左值。" << std::endl;}
void test(Vector&& temp) {std::cout << "为右值。" << std::endl;}

int main(){
    Vector a;
	Vector&& b = Vector();
    //请分别回答:a、std::move(a)、b 分别是左值还是右值?
	test(a);
	test(std::move(a));
	test(b);
}

答:a是左值,std::move(a)是右值,但b却是左值。

在这里b虽然是 Vector&& 类型,但却因为有变量名(即可持久存在),被编译器认为是左值。

结论:右值引用类型只是用于匹配右值,而并非表示一个右值。因此,尽量不要声明右值引用类型的变量,而只在函数形参使用它以匹配右值。

左值、右值、纯右值、将亡值

前面对移动语义的认识我们都是基于C++98时左值、右值概念,而C++11对左值、右值类别被重新进行了定义,因此现在我们重新认识一下新的类别。

C++11使用下面两种独立的性质来区别类别:

  1. 拥有身份:指代某个非临时对象
  2. 可被移动:可被右值引用类型匹配

每个C++表达式只属于三种基本值类别中的一种:左值 (lvalue)、纯右值 (prvalue)、将亡值 (xvalue)

  • 拥有身份且不可被移动的表达式被称作 左值 (lvalue) 表达式,指持久存在的对象或类型为左值引用类型的返还值。
  • 拥有身份且可被移动的表达式被称作 将亡值 (xvalue) 表达式,一般是指类型为右值引用类型的返还值。
  • 不拥有身份且可被移动的表达式被称作 纯右值 (prvalue) 表达式,也就是指纯粹的临时值(即使指代的对象是持久存在的)。
  • 不拥有身份且不可被移动的表达式无法使用。

如此分类是因为移动语义的出现,需要对类别重新规范说明。例如不能简单定义说右值就是临时值(因为也可能是std::move过的对象,该代指对象并不一定是临时值)。

Vector& func1();
Vector&& func2();
Vector func3();

int main(){
    Vector a;

    a;              //左值表达式
    func1();        //左值表达式,返还值是临时的,返还类型是左值引用,因此被认为不可移动。
    func2();        //将亡值表达式,返还值是临时的,返还类型是右值引用,因此指代的对象即使非临时也会被认为可移动。
    func3();        //纯右值表达式,返还值为临时值。
    std::move(a)//将亡值表达式,std::move本质也是个函数,同上。
    Vector();       //纯右值表达式
}

而现在我们可以归纳为:

  • 左值(lvalue) 指持久存在(有变量名)的对象或返还值类型为左值引用的返还值,是不可移动的。
  • 右值(rvalue) 包含了 将亡值、纯右值,是可移动(可被右值引用类型匹配)的值。

实际上C++ std::move函数的实现原理就是的强转成右值引用类型并返还之,因此该返还值会被判断为将亡值,更宽泛的说是被判定为右值。

函数参数传递

void func1(Vector v) {return;}
void func2(Vector && v) {return;}

int main() {
	Vector a;
	Vector &b = a;
	Vector c;
	Vector d;

    //请回答:不开优化的版本下,调用以下函数分别有多少Copy Consturct、Move Construct的开销?
	func1(a);
	func1(b);
	func1(std::move(c));
	func2(std::move(d));
}

实际上在不开优化的版本下,如果实参为右值,调用func1的开销只比func2多了一次移动构造函数和析构函数。

实参传递给形参,即形参会根据实参来构造。其结果是调用了移动构造函数;函数结束时则释放形参。

倘若说对象的移动构造函数开销较低(例如内部仅一个指针属性),那么使用无引用类型的形参函数是更优雅的选择,而且还能接受左值引用类型或无引用的实参(尽管这两种实参都会导致一次Copy Consturct)。可以说,这种情况下,只提供非引用类型的版本,也是可以接受的。

那我们在写一般函数形参的时候,若参数有支持移动构造(或移动赋值)的类型,是否有必要每个函数都提供关于&&形参的重载版本吗?

回答:从极致的优化角度来看是有必要的,应该提供右值引用类型的重载版本,更准确说应该同时提供左值引用(匹配左值)和右值引用(匹配右值)两种重载版本。

函数返还值传递

Vector func1() {
    Vector a;
	return a;
}

Vector func2() {
	Vector a;
	return std::move(a);
}

Vector&& func3() {
	Vector a;
	return std::move(a);
}

int main() {
    //请回答:不开优化的版本下,执行以下3行代码分别有多少Copy Consturct、Move Construct的开销?
	Vector test1 = func1();
	Vector test2 = func2();
	Vector test3 = func3();
}

同样的道理,执行这3行代码实际上都没有任何Copy Construct的开销(这其中也有NRV技术的功劳),都是只有一次Move Construct的开销。

此外一提,func3是危险的。因为局部变量释放后,函数返还值仍持有它的右值引用。

因此,这里也不建议函数返还右值引用类型,同前面传递参数类似的,移动构造开销不大的时候,直接返还非引用类型就足够了(在某些特殊场合有特别作用,准确来说一般用于表示返还成一个右值,如std::move的实现)。

结论:

  1. 我们应该首先把编写右值引用类型相关的任务重点放在对象的构造、赋值函数上。从源头上出发,在编写其它代码时会自然而然享受到了移动构造、移动赋值的优化效果。
  2. 形参:从优化的角度上看,若参数有支持移动构造(或移动赋值)的类型,应提供左值引用和右值引用的重载版本。移动开销很低时,只提供一个非引用类型的版本也是可以接受的。
  3. 返还值:不要且没必要编写返还右值引用类型的函数,除非有特殊用途。

万能引用

接下来的内容都是属于模板的部分了:万能引用、引用折叠、完美转发。这部分更加难以理解,不编写模板代码的话可以绕道了。

万能引用(Universal Reference):

  • 发生类型推导(例如模板、auto)的时候,使用T&&类型表示为万能引用,否则表示右值引用。
  • 万能引用类型的形参既能匹配任意引用类型的左值、右值。

也就是说编写模板函数时,只提供万能引用形参一个版本就可以匹配左值、右值,不必编写多个重载版本。

template<class T>
void func(T&& t){
    return;
}

int main() {
    Vector a,b;
	func(a);                //OK
	func(std::move(b));     //OK
}

此外需要注意的是,使用万能引用参数的函数是最贪婪的函数,容易让需要隐式转换的实参匹配到不希望的转发引用函数。例如下面代码:

template<class T>
  void f(T&& value);

void f(int a);
//当调用f(long类型的参数)或者f(short类型的参数),则不会匹配int版本而是匹配到万能引用的版本

引用折叠

使用万能引用遇到的第一个问题是推导类型会出现不正确的引用类型:例如当模板参数T为Vector&或Vector&&,模板函数形参为T&&时,展开后变成Vector& &&或者Vector&& &&。

template<class T>
void func(T&& t){
    return;
}

int main(){
    func(Vector()); //模板参数T被推导为Vector&&
}

但显然C++中是不允许对引用再进行引用的,于是为了让模板参数正确传递引用性质,C++定义了一套用于推导类型的引用折叠(Reference Collapse)规则:

所有的折叠引用最终都代表一个引用,要么是左值引用,要么是右值引用。

完美转发 std::forward

当我们使用了万能引用时,即使可以同时匹配左值、右值,但需要转发参数给其他函数时,会丢失引用性质(形参是个左值,从而无法判断到底匹配的是个左值还是右值)。

//当然我们也可以写成如下重载代码,但是这已经违背了使用万能引用的初衷(仅编写一个模板函数就可以匹配左值、右值)
template<class T>
void func(T& t){
    doSomething(t);
}

template<class T>
void func(T&& t){
    doSomething(std::move(t));
}

完美转发(Perfect Forwarding):C++11提供了完美转发函数 std:forward 。它可以在模板函数内给另一个函数传递参数时,将参数类型保持原本状态传入(如果形参推导出是右值引用则作为右值传入,如果是左值引用则作为左值传入)。

于是现在我们可以这样做了:

template<class T>
void func(T&& object){
    doSomething(std::forward<T>(object));
}

不借助std::forward间接传入参数的话,无论object是左值引用类型,还是右值引用类型,都会被视为左值。

std::forward()的实现主要就一句return static_cast<T&&>(形参),实际上也是利用了折叠规则。从而接受右值引用类型时,将右值引用类型的值返还(返还值为右值)。接受左值引用类型时,将左值引用类型的值返还(返还值为左值)。

而std::move()的实现还需要先移除形参的所有引用性质得到无引用性质的类型(假设为T2),然后再return static_cast<T2&&>(形参),从而保证不会发生引用折叠,而是直接作为右值引用类型的值返还(返还值为右值)。

列表初始化

在C++11中可以直接在变量名后面加上初始化列表来进行对象的初始化。

struct A {
	public:
	    A(int) {}
	private:
	    A(const A&) {}
};
int main() {
    A a(123);
    A b = 123; // error
    A c = { 123 };
    A d{123}; // c++11

    int e = {123};
    int f{123}; // c++11

    return 0;
}

列表初始化也可以用在函数的返回值上

std::vector<int> func() {
    return {};
}

列表初始化的一些规则

首先说下聚合类型可以进行直接列表初始化,这里需要了解什么是聚合类型:

  1. 类型是一个普通数组,如int[5],char[],double[]等
  2. 类型是一个类,且满足以下条件:
    • 没有用户声明的构造函数
    • 没有用户提供的构造函数(允许显示预置或弃置的构造函数)
    • 没有私有或保护的非静态数据成员
    • 没有基类
    • 没有虚函数
    • 没有{}和=直接初始化的非静态数据成员
    • 没有默认成员初始化器
struct A {
    int a;
    int b;
    int c;
    A(int, int){}
};
int main() {
    A a{1, 2, 3};// error,A有自定义的构造函数,不能列表初始化
}

上述代码类A不是聚合类型,无法进行列表初始化,必须以自定义的构造函数来构造对象。

struct A {
    int a;
    int b;
    virtual void func() {} // 含有虚函数,不是聚合类
};

struct Base {};
// B 继承 Base
struct B : public Base { // 有基类,不是聚合类
    int a;
    int b;
};

struct C {
    int a;
    int b = 10; // 有等号初始化,不是聚合类
};

struct D {
    int a;
    int b;
private:
    int c; // 含有私有的非静态数据成员,不是聚合类
};

struct E {
    int a;
    int b;
    E() : a(0), b(0) {} // 含有默认成员初始化器,不是聚合类
};

上面列举了一些不是聚合类的例子,对于一个聚合类型,使用列表初始化相当于对其中的每个元素分别赋值;对于非聚合类型,需要先自定义一个对应的构造函数,此时列表初始化将调用相应的构造函数。

std::initializer_list

我们平时开发使用STL过程中可能发现它的初始化列表可以是任意长度,大家有没有想过它是怎么实现的呢,答案是std::initializer_list,看下面这段示例代码:

struct CustomVec {
    std::vector<int> data;
    CustomVec(std::initializer_list<int> list) {
        for (auto iter = list.begin(); iter != list.end(); ++iter) {
            data.push_back(*iter);
        }
    }
};

我想通过上面这段代码大家可能已经知道STL是如何实现的任意长度初始化了吧,这个std::initializer_list其实也可以作为函数参数。

注意:std::initializer_list,它可以接收任意长度的初始化列表,但是里面必须是相同类型T,或者都可以转换为T。

列表初始化的好处

个人认为列表初始化的好处如下:

  1. 方便,且基本上可以替代括号初始化
  2. 可以使用初始化列表接受任意长度
  3. 可以防止类型窄化,避免精度丢失的隐式类型转换

什么是类型窄化,列表初始化通过禁止下列转换,对隐式转化加以限制:

  • 从浮点类型到整数类型的转换
  • 从 long double 到 double 或 float 的转换,以及从 double 到 float
    的转换,除非源是常量表达式且不发生溢出
  • 从整数类型到浮点类型的转换,除非源是其值能完全存储于目标类型的常量表达式
  • 从整数或无作用域枚举类型到不能表示原类型所有值的整数类型的转换,除非源是其值能完全存储于目标类型的常量表达式

示例:

int main() {
    int a = 1.2; // ok
    int b = {1.2}; // error

    float c = 1e70; // ok
    float d = {1e70}; // error

    float e = (unsigned long long)-1; // ok
    float f = {(unsigned long long)-1}; // error
    float g = (unsigned long long)1; // ok
    float h = {(unsigned long long)1}; // ok

    const int i = 1000;
    const int j = 2;
    char k = i; // ok
    char l = {i}; // error

    char m = j; // ok
    char m = {j}; // ok,因为是const类型,这里如果去掉const属性,也会报错
}

打印如下:

test.cc:24:17: error: narrowing conversion of ‘1.2e+0’ from ‘double’ to ‘int’ inside { } [-Wnarrowing]
     int b = {1.2};
                 ^
test.cc:30:38: error: narrowing conversion of ‘18446744073709551615’ from ‘long long unsigned int’ to ‘float’ inside { } [-Wnarrowing]
     float f = {(unsigned long long)-1};
                                      ^
test.cc:36:14: warning: overflow in implicit constant conversion [-Woverflow]
     char k = i;
              ^
test.cc:37:16: error: narrowing conversion of ‘1000’ from ‘int’ to ‘char’ inside { } [-Wnarrowing]
     char l = {i};

std::function和lambda表达式

std::function

讲std::function前首先需要了解下什么是可调用对象

满足以下条件之一就可称为可调用对象:

  • 是一个函数指针
  • 是一个具有operator()成员函数的类对象(传说中的仿函数),lambda表达式
  • 是一个可被转换为函数指针的类对象
  • 是一个类成员(函数)指针
  • bind表达式或其它函数对象

而std::function就是上面这种可调用对象的封装器,可以把std::function看做一个函数对象,用于表示函数这个抽象概念。std::function的实例可以存储、复制和调用任何可调用对象,存储的可调用对象称为std::function的目标,若std::function不含目标,则称它为空,调用空的std::function的目标会抛出std::bad_function_call异常。

使用参考如下实例代码:

std::function<void(int)> f; // 这里表示function的对象f的参数是int,返回值是void
#include <functional>
#include <iostream>

struct Foo {
    Foo(int num) : num_(num) {}
    void print_add(int i) const { std::cout << num_ + i << '\n'; }
    int num_;
};

void print_num(int i) { std::cout << i << '\n'; }

struct PrintNum {
    void operator()(int i) const { std::cout << i << '\n'; }
};

int main() 
{
    // 存储自由函数
    std::function<void(int)> f_display = print_num;
    f_display(-9);

    // 存储 lambda
    std::function<void()> f_display_42 = []() { print_num(42); };
    f_display_42();

    // 存储到 std::bind 调用的结果
    std::function<void()> f_display_31337 = std::bind(print_num, 31337);
    f_display_31337();

    // 存储到成员函数的调用
    std::function<void(const Foo&, int)> f_add_display = &Foo::print_add;
    const Foo foo(314159);
    f_add_display(foo, 1);
    f_add_display(314159, 1);

    // 存储到数据成员访问器的调用
    std::function<int(Foo const&)> f_num = &Foo::num_;
    std::cout << "num_: " << f_num(foo) << '\n';

    // 存储到成员函数及对象的调用
    using std::placeholders::_1;
    std::function<void(int)> f_add_display2 = std::bind(&Foo::print_add, foo, _1);
    f_add_display2(2);

    // 存储到成员函数和对象指针的调用
    std::function<void(int)> f_add_display3 = std::bind(&Foo::print_add, &foo, _1);
    f_add_display3(3);

    // 存储到函数对象的调用
    std::function<void(int)> f_display_obj = PrintNum();
    f_display_obj(18);
}

从上面可以看到std::function的使用方法,当给std::function填入合适的参数表和返回值后,它就变成了可以容纳所有这一类调用方式的函数封装器。std::function还可以用作回调函数,或者在C++里如果需要使用回调那就一定要使用std::function,特别方便,这方面的使用方式大家可以读下我之前写的关于线程池和定时器相关的文章。

std::bind

使用std::bind可以将可调用对象和参数一起绑定,绑定后的结果使用std::function进行保存,并延迟调用到任何我们需要的时候。

std::bind通常有两大作用:

  • 将可调用对象与参数一起绑定为另一个std::function供调用
  • 将n元可调用对象转成 m(m < n)元可调用对象,绑定一部分参数,这里需要使用std::placeholders

具体示例:

#include <functional>
#include <iostream>
#include <memory>

void f(int n1, int n2, int n3, const int& n4, int n5) {
    std::cout << n1 << ' ' << n2 << ' ' << n3 << ' ' << n4 << ' ' << n5 << std::endl;
}

int g(int n1) { return n1; }

struct Foo {
    void print_sum(int n1, int n2) { std::cout << n1 + n2 << std::endl; }
    int data = 10;
};

int main() {
    using namespace std::placeholders;  // 针对 _1, _2, _3...

    // 演示参数重排序和按引用传递
    int n = 7;
    // ( _1 与 _2 来自 std::placeholders ,并表示将来会传递给 f1 的参数)
    auto f1 = std::bind(f, _2, 42, _1, std::cref(n), n);
    n = 10;
    f1(1, 2, 1001);  // 1 为 _1 所绑定, 2 为 _2 所绑定,不使用 1001
                     // 进行到 f(2, 42, 1, n, 7) 的调用

    // 嵌套 bind 子表达式共享占位符
    auto f2 = std::bind(f, _3, std::bind(g, _3), _3, 4, 5);
    f2(10, 11, 12);  // 进行到 f(12, g(12), 12, 4, 5); 的调用

    // 绑定指向成员函数指针
    Foo foo;
    auto f3 = std::bind(&Foo::print_sum, &foo, 95, _1);
    f3(5);

    // 绑定指向数据成员指针
    auto f4 = std::bind(&Foo::data, _1);
    std::cout << f4(foo) << std::endl;

    // 智能指针亦能用于调用被引用对象的成员
    std::cout << f4(std::make_shared<Foo>(foo)) << std::endl;
}

lambda表达式

lambda表达式可以说是c++11引用的最重要的特性之一,它定义了一个匿名函数,可以捕获一定范围的变量在函数内部使用,一般有如下语法形式:

auto func = [capture] (params) opt -> ret { func_body; };

其中func是可以当作lambda表达式的名字,作为一个函数使用,capture是捕获列表,params是参数表,opt是函数选项(mutable之类), ret是返回值类型,func_body是函数体。

一个完整的lambda表达式:

auto func1 = [](int a) -> int { return a + 1; };
auto func2 = [](int a) { return a + 2; };
cout << func1(1) << " " << func2(2) << endl;

如上代码,很多时候lambda表达式返回值是很明显的,c++11允许省略表达式的返回值定义。

lambda表达式允许捕获一定范围内的变量:

  • []不捕获任何变量
  • [&]引用捕获,捕获外部作用域所有变量,在函数体内当作引用使用
  • [=]值捕获,捕获外部作用域所有变量,在函数内内有个副本使用
  • [=, &a]值捕获外部作用域所有变量,按引用捕获a变量
  • [a]只值捕获a变量,不捕获其它变量
  • [this]捕获当前类中的this指针

lambda表达式示例代码:

int a = 0;
auto f1 = [=](){ return a; }; // 值捕获a
cout << f1() << endl;

auto f2 = [=]() { return a++; }; // 修改按值捕获的外部变量,error
auto f3 = [=]() mutable { return a++; };

代码中的f2是编译不过的,因为我们修改了按值捕获的外部变量,其实lambda表达式就相当于是一个仿函数,仿函数是一个有operator()成员函数的类对象,这个operator()默认是const的,所以不能修改成员变量,而加了mutable,就是去掉const属性。

还可以使用lambda表达式自定义stl的规则,例如自定义sort排序规则:

struct A {
    int a;
    int b;
};

int main() {
    vector<A> vec;
    std::sort(vec.begin(), vec.end(), [](const A &left, const A &right) { 
    	return left.a < right.a; 
    });
}

总结

std::function和std::bind使得我们平时编程过程中封装函数更加的方便,而lambda表达式将这种方便发挥到了极致,可以在需要的时间就地定义匿名函数,不再需要定义类或者函数等,在自定义STL规则时候也非常方便,让代码更简洁,更灵活,提高开发效率。

模板的改进

C++11 关于模板有一些细节的改进:

  • 模板的右尖括号
  • 模板的别名
  • 函数模板的默认模板参数

模板的右尖括号

C++11之前是不允许两个右尖括号出现的,会被认为是右移操作符,所以需要中间加个空格进行分割,避免发生编译错误。

int main()
{
	std::vector<std::vector<int>> a;	// error
	std::vector<std::vector<int> > b;	// ok
}

模板的别名

C++11引入了using,可以轻松的定义别名,而不是使用繁琐的typedef。

typedef std::vector<std::vector<int>> vvi; // before c++11
using vvi = std::vector<std::vector<int>>; // c++11

template<class T>
struct Alloc { };
template<class T>
using Vec = vector<T, Alloc<T>>; // 类型标识为 vector<T, Alloc<T>>
Vec<int> v; // Vec<int> 同 vector<int, Alloc<int>>

使用using明显简洁并且易读,大家可能之前也见过使用typedef定义函数指针之类的操作,那烂代码我就不列出来了,反正我是看不懂也不想看…以后都可以使用using,额还是列出来吧。

typedef void (*func)(int, int); // 啥玩意,看不懂
using func = void (*)(int, int); // 起码比typedef容易看的懂吧

上面的代码使用using起码比typedef容易看的懂一些吧,但是我还是看不懂,因为我从来不用这种来表示函数指针,用std::function()、std::bind()、std::placeholder()、lambda表达式它不香吗。

函数模板的默认模板参数

C++11之前只有类模板支持默认模板参数,函数模板是不支持默认模板参数的,C++11后都支持。

template <typename T, typename U=int>
class A {
    T value;  
};

template <typename T=int, typename U> // error
class A {
    T value;  
};

类模板的默认模板参数必须从右往左定义,而函数模板则没有这个限制。

template <typename R, typename U=int>
R func1(U val) {
    return val;
}

template <typename R=int, typename U>
R func2(U val) {
    return val;
}

int main() {
    cout << func1<int, double>(99.9) << endl; // 99
    cout << func1<double, double>(99.9) << endl; // 99.9
    cout << func1<double>(99.9) << endl; // 99.9
    cout << func1<int>(99.9) << endl; // 99
    cout << func2<int, double>(99.9) << endl; // 99
    cout << func1<double, double>(99.9) << endl; // 99.9
    cout << func2<double>(99.9) << endl; // 99.9
    cout << func2<int>(99.9) << endl; // 99
    return 0;
}

对于函数模板,参数的填充顺序是从左到右的。

同时C++11支持变长参数模板:

template <typename T>
void func(const T& t){
    cout << t << '\n';
}

template <typename T, typename ... Args>
void func(const T& t, Args ... args){
    cout << t << ',';
    func(args...);
}

并发

c++11关于并发引入了好多好东西,有:

  • std::thread相关
  • std::mutex相关
  • std::lock相关
  • std::atomic相关
  • std::call_once相关
  • volatile相关
  • std::condition_variable相关
  • std::future相关
  • async相关

std::thread相关

c++11之前你可能使用pthread_xxx来创建线程,繁琐且不易读,c++11引入了std::thread来创建线程,支持对线程join或者detach。直接看代码:

#include <iostream>
#include <thread>

using namespace std;

int main() {
    auto func = []() {
        for (int i = 0; i < 10; ++i) {
            cout << i << " ";
        }
        cout << endl;
    };
    std::thread t(func);
    if (t.joinable()) {
        t.detach();
    }
    auto func1 = [](int k) {
        for (int i = 0; i < k; ++i) {
            cout << i << " ";
        }
        cout << endl;
    };
    std::thread tt(func1, 20);
    if (tt.joinable()) { // 检查线程可否被join
        tt.join();
    }
    return 0;
}

持续更新---------

  • 22
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值