C++11

目录

一、C++11的诞生

二、initializer_list

1.统一的初始化方案

2.initializer_list

三、五个关键字

1.auto

2.decltype

3.nullptr

4.final

5.override

四、STL的新容器

1.array

2.forward_list

3.unordered_map与unordered_set

4.新增成员函数

五、右值引用和移动赋值

1.左值和右值

2.左值引用和右值引用

3.右值引用的意义

4.移动构造

5.完美转化

六、新的成员函数特性

1.新的默认成员函数

2.类成员变量的缺省值

3.两个关键字

(1)default

(2)delete

七、lambda表达式

1.lambda表达式的概念和书写

2.lambda表达式与仿函数

八、可变参数模板

1.可变参数模板的概念

2.参数包的展开

(1)递归展开

(2)逗号表达式展开

3.emplace接口

九、包装器

1.function

2.bind


一、C++11的诞生

C++自诞生一来出现了两个比较重要的标准,他们是C++98和C++11。

1998年是C++标准委员会成立的第一年,当时本来计划是每5年根据实际需要更新一次标准,而当C++国际标准委员会在研究C++03的下一个版本的时候,一开始计划2007年发布,所以最初这个标准叫C++07。但是直到2006年,委员会认为2007年肯定完不成任务了,而且很可能2008年也完不成。最后标准干脆命名为C++ 0x,而x的意思就是不知道到底能在2007、2008还是2009年完成,反正不会到2010年。结果都到2010年了也没完成,最后委员会还是在2011年完成了C++标准。所以最终定名为C++11。

因为C++部分新特性使用起来非常的鸡肋,最重要的标准网络库也迟迟不更新,导致很多人对C++标准委员会并不友好。C++11也逃不过网友们的痛批,有的网友说:“C++11以后的C++越来越不像C++,更像是一门新的语言。”但是在学了之后,一些特性还是很有意义的,对于程序员使用也十分方便。

C++11与C++98相比,增加了很多新特性。相比较而言C++11能更好地用于系统开发和库开发、语法更加泛华和简单化、更加稳定和安全,不仅功能更强大,而且能提升程序员的开发效率,公司实际项目开发中也用得比较多。所以对C++11的学习也是很重要的。

附上标准C++网站:https://isocpp.org

因为VS2013对C++11的兼容性很差,所以以下所有代码均在VS2019中运行。

二、initializer_list

1.统一的初始化方案

C++98中仅允许数组和结构体使用{}初始化。

struct A
{
    int _a;
    int _b;
}

int main()
{
    int a = 0;
    int* pa = nullptr;
    int arr[10] = {0};
    A sa = {1, 2};
    return 0;
}

C++11将用初始化列表(大括号括起数据形成的列表)的使用范围,扩大到所有的内置类型和自定义的类型,使用初始化列表时,可添加赋值符号(=),也可不添加。

对于自定义类型,这样的赋值方式很有意义,简化了赋值语句。但是用在内置类型上就会出现像这样的赋值语句……

#include<iostream>
using namespace std;

int main()
{
    int a{ 0 };
    cout << a << endl;
    return 0;
}

这样的赋值没有问题,但是越看越别扭,简直不是给人看的。

添不添加赋值符,个人建议能写就写上,这样让其他人一眼就看出来这是赋值,增强代码的可读性。同时{}也可以用于new表达式中,在以前{}只能用于结构体和数组,而现在可以用于对象的初始化。

#include<iostream>
#include<vector>
using namespace std;

int main()
{
    //用于new关键字开辟空间
    int* pa = new int[4]{ 1,2,3,4 };
    for (int i = 0; i < 4; ++i)
    {
        cout << pa[i] << " ";
    }
    cout << endl;
    return 0;
}

创建对象时也可以使用列表初始化方式调用构造函数初始化,以前初始化用()调构造函数,现在{}也可以调用构造函数。

#include<iostream>

using namespace std;

struct Data
{
    int _a;
    int _b;

    Data(int a, int b)
        :_a(a)
        , _b(b)
    {}
};

int main()
{
    Data d1(1, 1); //C++98中使用构造函数初始化
    //C++11支持列表初始化,调用构造函数进行初始化
    Data d2{ 1,2 };//C++11中使用{}进行初始化
    Data d3 = { 1, 3 };
    return 0;
}

2.initializer_list

在C++98中如果我们要建立一个插入数字0到9的vector,我们要么通过默认构造空vector再不断插入,或者使用另一个容器的迭代器区间进行构造。儿在C++11中,我们就可以使用{}来进行初始化。

#include<iostream>
#include<vector>

using namespace std;

int main()
{
    //C++98
    int arr[10] = { 0,1,2,3,4,5,6,7,8,9 };
    vector<int> v1;
    for (int i = 0; i < 10; ++i)
    {
        v1.push_back(arr[i]);
    }
    for (int i = 0; i < 10; ++i)
    {
        cout << v1[i] << " ";
    }
    cout << endl;

    //C++11
    vector<int> v2 = { 0,1,2,3,4,5,6,7,8,9 };
    for (int i = 0; i < 10; ++i)
    {
        cout << v2[i] << " ";
    }
    cout << endl;
    return 0;
}

这个大括号在底层的数据结构就叫做initializer_list,在std命名空间中,它不受数据个数的限制且书写简单,灵活方便。

#include<iostream>
#include<vector>
using namespace std;
int main()
{
    //这段代码检测{}的数据类型为initializer_list<int>
    auto il = { 10, 20, 30 };
    cout << typeid(il).name() << endl;
    return 0;
}

initializer_list的出现就是为了编写能够处理不同数量的同类型实参,它与vector一样的是都是一个容器,但与vector不同的是,initializer_list对象中的元素永远是常量值,initializer_list对象一旦建立,则其中各元素的值都不可改变。所以initializer_list一般是作为构造函数的参数,当然它也可以作为赋值运算符重载函数的参数,这样就可以用大括号赋值。C++11对STL中的不少容器就都增加initializer_list为参数的构造函数。

这是C++11的vector的构造及拷贝构造函数,很明显第六行出现了initializer_list的版本。

如果我们去看string、map和set等类的构造函数,他们也都增加了相应的构造函数,它们让容器的赋值变得十分方便。

这是一段以vector和map为例的测试代码:

#include<iostream>
#include<vector>
#include<map>
using namespace std;
int main()
{
    vector<int> v = { 0,1,2,3,4 };

    for (int i = 0; i < v.size(); ++i)
    {
        cout << v[i] << " ";
    }
    cout << endl;

    map<int, int> m = { {0,5},{1,6},{2,7},{3,8},{4,9} };

    for (int i = 0; i < v.size(); ++i)
    {
        cout << m.find(i)->second << " ";
    }
    cout << endl;

    return 0;
}

在我们自己实现过的vector中加入这两个函数,就可以实现initializer_list的构造。

class vector
{
public:
    //initializer_list为参数初始化和赋值,C++11支持
    typedef T* iterator;
    vector(std::initializer_list<T> l)
    {
        _start = new T[l.size()];
        _finish = _start + l.size();
        _endofstorage = _start + l.size();
        iterator vit = _start;
        std::initializer_list<T>::iterator lit = l.begin();
        while (lit != l.end())
        {
            *vit++ = *lit++;
        }
    }

    vector<T>& operator=(std::initializer_list<T> l) 
    {
        vector<T> tmp(l);
        std::swap(_start, tmp._start);
        std::swap(_finish, tmp._finish);
        std::swap(_endofstorage, tmp._endofstorage);
        return *this;
    }
private:
    iterator _start;
    iterator _finish;
    iterator _endofstorage;
};

三、五个关键字

1.auto

在C++98中auto是一个存储类型的说明符,表明变量是局部自动存储类型,但是局部域中定义局部的变量默认就是自动存储类型。因为它大概率用不到的设计,C++98中auto就没什么价值了。而C++11中废弃auto原来的用法,将其用于实现自动对变量类型进行推断。这样要求必须进行显示初始化,让编译器将定义对象的类型设置为初始化值的类型。当然后面又印出来范围for中auto的使用。

#include<iostream>
#include<vector>
using namespace my_vector;
int main()
{
    vector<int> v1;
    v1.push_back(1);
    v1.push_back(2);
    v1.push_back(3);
    v1.push_back(4);
    for (auto& e : v1)
    {
        std::cout << e;
    }
    std::cout << std::endl;
    vector<int> v2({ 1,2,3,4 });
    for (auto& e : v2)
    {
        std::cout << e;
    }
    return 0;
}

2.decltype

关键字decltype可推断出括号内表达式最终得到的结果的数据类型,比方说下面的代码中:

decltype(x * y) ret,decltype(x * y)表示x*y这个表达式得到的结果的类型,ret的类型就是这个被推导出来的类型。主义,这里ret承接的只是类型,变量本身并没有被初始化,当然也更不能打印。ret这个变量仅仅用于传递类型,然后在下面用typeid(ret)就可以获取到ret的类型。

#include<iostream>
using namespace std;
int main()
{
    const int x = 1;
    double y = 2.2;
    //decltype是一个推断表达式结果的类型的关键字
    decltype(x * y) ret;//ret的类型是double
    decltype(&x) p;//p的类型是int*
    
    cout << typeid(ret).name() << endl;
    cout << typeid(p).name() << endl;

    return 0;
}

3.nullptr

我们在很早以前就讲过这个问题,这是C++开发者的一个小失误。由于原来C语言中NULL被定义成字面量0,C++开始也沿用了这一点这样。由于函数重载的存在,这时问题就出现了,因为字面常量0既能空指针常量,又能表示整形常量0,指代不明确。所以为了程序的安全可靠性,C++11中新增了nullptr,专门用于表示空指针。

下面是NULL的定义:

#ifndef NULL
#ifdef __cplusplus
#define NULL   0
#else
#define NULL   ((void *)0)
#endif #endif

4.final

final是在继承体系中使用的,final修饰类则表明这个类不能被继承,这个类叫最终类,final修饰成员函数则表明这个函数不能再被重写(之前重写就重写了,之后不能重写了)。

#include<iostream>
using namespace std;
class Base
{
public:
    virtual void fun()
    {
        cout << "Base::fun()" << endl;
    }
protected:
    int _a = 1;
};

class Derive : public Base
{
public:
    virtual void fun()
    {
        cout << "Derive::fun()" << endl;
    }
protected:
    int _b = 2;
};

int main()
{
    Derive d;
    d.fun();
    return 0;
}

final修饰类,很明显看到这个最终类不能再被继承。

final修饰成员函数,这个函数不能再被重写。

5.override

override用于检验派生类虚函数是否被重写,没有被重写就报错,相当于对重写的检查。

override修饰派生类的虚函数中,它确实被重写了,所以正常运行。

override修饰基类的虚函数中,它没有被重写了,所以报错。

四、STL的新容器

1.array

array是C++设计出来替换C语言数组的数据结构,它可以更有效地检查数组的越界情况,但是由于C语言的数组使用已经太普遍了而且array也并不是多好用,所以这个数据结构一直没什么人用。

#include <iostream>
#include <array>
using namespace std;

int main()
{
    array<int, 5> a1;
    a1[30] = 3;
    
    int a2[5];
    a2[30] = 5;
    

    return 0;
}

a1是array,只要越界必报错,而C数组的越界是选取部分位置抽查,太远了它就查不到了,所以并不会报错。

2.forward_list

这其实就是我们学过的单链表,只节省了一部分父指针的空间,平时用的也不多。

3.unordered_map与unordered_set

内部由哈希表实现的map和set,与普通map和set使用是一样的,我们之前都有过讲解。

(10条消息) C++哈希_聪明的骑士的博客-CSDN博客

4.新增成员函数

C++11也增加了不少新的成员函数,比如cbgin(),cend(),crbegin(),crend()返回头尾的const迭代器。shrink_to_fit()是一个少见的缩容函数会将空间缩小至只完全当前数据,没有空位置。还有data()可以返回数据,再加上emplace和emplace_back这两个特殊的插入函数。

五、右值引用和移动赋值

1.左值和右值

传统的C++语法中出现了引用,而C++11中新增了右值引用的语法特性,也就是说我们之前学习的引用就都叫做左值引用。但是,无论左值引用还是右值引用,本质都是给对象取别名。

那左值和右值到底是什么东西呢?

左值是一个表示数据的表达式(如变量名或解引用的指针),我们可以获取它的地址也可以对它赋值,左值可以出现赋值符号的左边。而右值既不能取地址,也不能出现在赋值符号左边。左值引用就是给左值的引用,给左值取别名;右值引用就是给右值的引用,给右值取别名。

说到底,左值和右值的区分标准只有一个:能取地址的变量叫左值,不能取地址的叫右值。

#include<iostream>
int fmin(int x, int y)
{
    if (x > y)
        return y;
    else
        return x;
}

int main()
{
    //以下的p、b、c、*p都是左值
    int* p = new int(0);
    int b = 1;
    const int c = 2;
    //以下是对上面左值的左值引用
    int*& rp = p;
    int& rb = b;
    const int& rc = c;
    int& pvalue = *p;


    
    double x = 2.2;
    double y = 1.1;
    // 以下几个都是常见的右值
    10;
    x + y;
    fmin(x, y);
    // 以下都是对上面右值的右值引用,右值引用用Type &&表示
    int&& rr1 = 10;
    double&& rr2 = x + y;
    double&& rr3 = fmin(x, y);

    // 这里编译会报错: 左操作数必须为左值
    10 = 1;
    x + y = 1;
    fmin(x, y) = 1;
    return 0;
}

2.左值引用和右值引用

左值引用和右值引用有以下规则:

  • 左值引用只能引用左值,不能引用右值。
  • const左值引用既可引用左值,也可引用右值。
  • 右值引用只能右值,不能引用左值。
  • 右值引用可以move以后的左值。

原因和我们之前学习const引用和非constu引用很相似:

右值是不能被修改的,所有const修饰左值引用满足条件,但非const左值引用不满足。

左值引用右值属于权限扩大,为了避免权限扩大可以使用左值const引用。

#include<iostream>
int main()
{
    //左值引用只能引用左值,不能引用右值。
    int a = 10;
    int& ra = a;//ra为a的别名,是普通左值引用
    //int& ra2 = 10;//编译出错,10是右值,不能用左值引用


    //const左值引用可引用左值和右值。
    const int& ra3 = 10;
    const int& ra4 = a;


    // 右值引用只能右值,不能引用左值。
    int&& r1 = 10;
    //int&& r2 = a;//右值引用引用左值,报错
    

    // 右值引用可以引用move以后的左值
    int&& r3 = std::move(a);//move()会将左值变为右值
    return 0;
}

3.右值引用的意义

左值引用既可以引用左值和又可以引用右值,那C++11提出的右值引用是不是画蛇添足呢?

其实也并不是,它的出现是为了进一步提升程序的运行效率。

右值引用最大的意义就是:在函数传参或者函数传返回值是减少拷贝。

比如说我们使用实现的string类,在类内的构造、析构、拷贝构造和赋值运算符重在函数中加上打印当前函数声明的内容(std::cout<<"string(char* str = "")"<<std::endl;),我们就能看到它在执行过程中调用了那些函数。

#include<iostream>
void func1(string s)
{}

void func2(const string& s)
{}


int main()
{
    string s1("hello world");
    func1(s1);
    func2(s1);
    return 0;
}

先把func1(s1)注释掉,得到如下结果:

string(const char* str = )

~string()

再把func2(s1)注释掉,得到如下结果:

string(const char* str = )

string(const string& s)

~string()

~string()

很明显能看出来传引用的函数少了一次深拷贝,提高了运行效率。不过,虽然左值引用基本使得做参数和做返回值的内容减少了不必要的拷贝,但是如果是在函数中,生成的临时变量需要被传值返回,这个短板左值引用就解决不了了。

而如果是在函数中创建临时变量再返回这个变量的引用,那么这个构建好的变量又会因为出了函数的作用域而被析构函数销毁,成品是做出来了就是被自己销毁了。

我们可以通过增加输出型参数的方式解决这个问题,但这种方式并不好用。但是编译器也不傻,有时候一些不必要的操作他就会自己灵活优化。

比如说下面的代码:

#include<iostream>
string to_string(char* s)
{
    string str(s);
    return str;
}

int main()
{
    string s = to_string("Hello world!");
    return 0;
}

首先在Debug下,string str(s)调用一次构造函数,return str还需要做一次拷贝构造形成一份临时变量,临时变量被带出,赋值运算符重载函数赋值给string s,原来函数内的那份变量被析构,然后main结束string s也被析构。

然而在Release下,sring str(s)在to_string函数内构造,然后直接通过拷贝构造拷到string s里,s出main函数被销毁。甚至有些编译器会直接将string str(s)的构造就直接在main的string s处构造,出mian函数析构。总之,传值返回不再需要临时变量的参与。

对于一个空间占用很小的返回值会直接使用寄存器返回,而对于较大的自定义类型变量类型变量,就比如说在上面的情形中,main函数先建立栈帧,to_string函数也会在main的栈帧下建立自己的栈帧,二者是挨着的。但此时,在to_string函数建立栈帧前会优先压参数和压返回值,就是在两个栈帧之间空出一部分空间来存放函数的参数和返回值。这样即使下面的to_string函数栈帧被销毁,但是不会影响中间的空间,也就不再需要临时变量的拷贝等操作。

4.移动构造

虽然编译器会优化跳过string,但是拷贝构造,只不过是两次拷贝构造变成了一次拷贝构造。但是还是没解决拷贝效率低的根本问题。

C++11中给出解决办法是在string类中多再写一个移动构造。

移动构造和拷贝构造符合以下逻辑:

  • 右值构造左值,并且该类中写了移动构造,那么发生移动构造
  • 右值构造左值,该类中未写了移动构造,那么发生拷贝构造
  • 左值构造左值,发生拷贝构造

上面的逻辑你可能不理解,看来下面的概念你就能理解了。

右值可以分为两种:

  • 纯右值(内置类型表达式的值)
  • 将亡值(自定义类型表达式的值)

C++中使用右值引用就代表着该值是将亡值(将亡就是马上就要消亡,表示该值出了某作用域就会被销毁),既然是将亡值,它都快被销毁了,你深拷贝出一个对象,再把原对象销毁,这样想是完全没有必要,我们直接将该资源进行转移。就像递东西一样,这件东西对对方不需要而对你有用,你就直接把对方的东西拿走就好了。

在我们实现的string类中加入移动构造函数:

string& operator=(string&& s)
{
    std::cout << "string& operator=(string&& s)" << std::endl;
    std::swap(s._capacity, _capacity);
    std::swap(s._size, _size);
    std::swap(s._str, _str);
    return *this;
}

代码:

#include<iostream>
string to_string(char* s)
{
    string str(s);
    return str;
}

int main()
{
    string s;
    s = to_string("Hello world!");
    return 0;
}

对于上面的函数函数传返回值的过程就被简化为一次移动构造,相比深拷贝效率大大提升。

从这里我们也可以发现C++是一门对程序性能有极致追求的语言。

5.完美转化

模板中的&&不代表右值引用,而是万能引用,它其既能接收左值又能接收右值。模板的万能引用只是提供了能够接收同时接收左值引用和右值引用的能力,但是引用类型的唯一作用就是限制了接收的类型。因为右值在后续使用中都会退化为左值,如果我们希望能够在参数传递过程中保持它的左值或者右值的属性,就需要用我们学习完美转化。

#include<iostream>
using namespace std;
void Fun(int& x) { cout << "左值引用" << endl; }

void Fun(const int& x) { cout << "const 左值引用" << endl; }

void Fun(int&& x) { cout << "右值引用" << endl; }

void Fun(const int&& x) { cout << "const 右值引用" << endl; }

//万能引用
template<typename T>
void PerfectForward(T&& t)
{
    Fun(t);
}

int main()
{
    int a = 0;
    PerfectForward(a);// 左值

    const int b = 0;
    PerfectForward(b);// const 左值
    
    PerfectForward(10);// 右值
    PerfectForward(std::move(a));// 右值

    PerfectForward(std::move(b)); // const 右值

    return 0;
}

运行结果:

有些出乎意料,不管是左值还是右值调用Fun后,最后打印的都是左值引用。

这是因为T&& t是一个函数模板参数,我们向它实例化后的函数传入参数,此时参数的性质还不会变,但是继续执行时还需要传参给内部的func函数,此时就还是需要拷贝功能,当调用Fun函数时,被拷贝第二次的参数全部变为了左值。(右值经过拷贝后得到左值)

我们对函数稍加修改,将r变为std::forward(t):

#include<iostream>
using namespace std;
void Fun(int& x) { cout << "左值引用" << endl; }

void Fun(const int& x) { cout << "const 左值引用" << endl; }

void Fun(int&& x) { cout << "右值引用" << endl; }

void Fun(const int&& x) { cout << "const 右值引用" << endl; }

//万能引用
template<typename T>
void PerfectForward(T&& t)
{
    Fun(std::forward<T>(t));
}

int main()
{
    int a = 0;
    PerfectForward(a);// 左值

    const int b = 0;
    PerfectForward(b);// const 左值
    
    PerfectForward(10);// 右值
    PerfectForward(std::move(a));// 右值

    PerfectForward(std::move(b)); // const 右值

    return 0; 
}

此时,即使再经过一次拷贝,该值的左右值属性也继续保留。

六、新的成员函数特性

1.新的默认成员函数

原来C++98的类中,有6个默认成员函数,人称六个大爷。这六个函数即使我们不写,编译器也会自己生成,包括:

  • 构造函数
  • 析构函数
  • 拷贝构造函数
  • 赋值运算符重载
  • 取地址重载
  • const 取地址重载

最重要的是前4个,后两个用处不大。

在C++11中又新增了两个:移动构造函数和移动赋值运算符重载。

移动构造函数和移动赋值运算符重载有以下三个规则:

  • 如果你没有自己实现移动构造函数,且没有实现析构函数 、拷贝构造、拷贝赋值重载中的任意一个(三个有一个或多个没实现)。那么编译器会自动生成一个默认移动构造。默认生成的移动构造函数,对于内置类型成员会执行逐成员按字节拷贝,自定义类型成员,则需要看这个成员是否实现移动构造, 如果实现了就调用移动构造,没有实现就调用拷贝构造。
  • 如果你没有自己实现移动赋值重载函数,且没有实现析构函数 、拷贝构造、拷贝赋值重载中 的任意一个,那么编译器会自动生成一个默认移动赋值。默认生成的移动构造函数,对于内置类型成员会执行逐成员按字节拷贝,自定义类型成员,则需要看这个成员是否实现移动赋 值,如果实现了就调用移动赋值,没有实现就调用拷贝赋值。(默认移动赋值跟上面移动构造 基本一致)
  • 如果你提供了移动构造或者移动赋值,编译器不会自动提供拷贝构造和拷贝赋值。

移动赋值和移动构造原理都是我们之前讲过的右值引用的相关内容。

2.类成员变量的缺省值

C++11中类的成员变量可以使用缺省值进行初始化。

class A
{
private:
    int _a = 3;//C++98是不允许加缺省值的
}

3.两个关键字

(1)default

default可以手动控制默认成员函数的生成。

假设你要使用某个默认的函数,但是因为一些原因这个函数没有默认生成,在这个函数的后面加上=default编译器就会自己给你生成,不过一定是默认成员函数。比如:我们提供了拷贝构造,就不会生成移动构造了,那么我们可以使用default关键字显示指定移动构造生成。

#include<iostream>
class Person

{

public:
    Person(const char* name = "", int age = 0)
        :_name(name)
        , _age(age)
    {}
    Person(const Person& p)
        :_name(p._name)
        , _age(p._age)
    {}
    Person(Person&& p) = default;

private:
    string _name;
    int _age;
};

int main()
{
    Person s1;
    Person s2 = s1;
    Person s3 = std::move(s1);
    return 0;
}

(2)delete

delete可以禁止生成对应默认函数。

如果能想要限制某些默认成员函数的生成,在C++98中,可以将该函数设为私有,并且只声明不实现,这样只要其他人调用就报错。而在C++11中更简单,只需在该函数声明加上=delete,编译器就不会实现默认版本的默认成员函数。

class Person
{
public:
    Person(const char* name = "", int age = 0)
        :_name(name)
        , _age(age)
    {}

    Person(const Person& p) = delete;
 
private:
    string _name;
    int _age;
};
 
int main()
{
    Person s1;
    Person s2 = s1;
    Person s3 = std::move(s1);
    return 0;
}

七、lambda表达式

1.lambda表达式的概念和书写

如果将函数作为参数,在C语言中可以传递函数指针,但是函数指针太难学了,所以C++引入了仿函数。但随着C++语法的发展,人们觉得上面的写法还是复杂了,每次为了实现一个算法,都要重新去写一个类,需要比较的逻辑不同,还要去实现多个类,尤其相同类的命名, 这些都带来了极大的不便。因此,C++借鉴了java等语言,在C++11中增加了Lambda表达式,很多人吐槽这样的语句让C++很不C++,但也并不妨碍我们学习。

  • lambda表达式书写格式:[capture-list] (parameters) mutable -> return-type { statement }
  • [capture-list] : 捕捉列表,该列表总是出现在lambda函数的开始位置,编译器根据[]来判断接下来的代码是否为lambda函数,捕捉列表能够捕捉上下文中的变量供lambda函数使用。
  • (parameters):参数列表。与普通函数的参数列表一致,如果不需要参数传递,则可以连同()一起省略
  • mutable:默认情况下,lambda函数总是一个const函数,mutable可以取消其常量性。使用该修饰符时,参数列表不可省略(即使参数为空)。
  • -> returntype:返回值类型。用追踪返回类型形式声明函数的返回值类型,没有返回 值时此部分可省略。返回值类型明确情况下,也可省略,由编译器对返回类型进行推 导。
  • {statement}:函数体。在该函数体内,除了可以使用其参数外,还可以使用所有捕获到的变量。

在lambda函数定义中,参数列表和返回值类型都是可省略的部分,而捕捉列表和函数体可以为空。因此最简单的lambda函数为:[]{};,当然该lambda函数也不能做任何事情。

捕捉列表描述了上下文中那些数据可以被lambda使用,以及使用的方式传值还是传引用,捕获列表有以下用法:

  • [var]:表示值传递方式捕捉变量
  • [=]:表示值传递方式捕获所有父作用域中的变量(包括this)
  • [&var]:表示引用传递捕捉变量
  • [&]:表示引用传递捕捉所有父作用域中的变量(包括this)
  • [this]:表示值传递方式捕捉当前的this指针

你可能会想,在捕捉列表中&var为什么是传引用,而不是传var的指针。这样想就是没有理解捕捉的意义,捕捉一定是有东西你才能捕捉,取地址是获取地址的操作,在这个地址可不在作用域中,你又谈何捕捉呢。如果你向传指针,你可以在父作用域中定义一个对应指针变量,然后用捕捉列表捕获它就可以了。

#include<iostream>
using namespace std;

int main()
{
    // 最简单的lambda表达式, 该lambda表达式没有任何意义
    [] {};

    // 省略参数列表和返回值类型,返回值类型由编译器推导为int
    int a = 3, b = 4;
    auto fun0 = [=] {return a+3; };
    cout << fun0() << endl;

    // 省略了返回值类型,无返回值类型
    auto fun1 = [&](int c) {b = a + c; };
    fun1(10);
    cout << a << " " << b << endl;

    // 各部分都很完善的lambda函数
    auto fun2 = [=, &b](int c)->int {return b += a + c; };
    cout << fun2(10) << endl;

    // 复制捕捉x
    int x = 10;
    auto add_x = [x](int a) mutable { x *= 2; return a + x; };
    cout << add_x(10) << endl;

    //&var一定是以引用方式捕获
    int y = 10;
    auto fun3 = [&y](int a) mutable { y+=a; };
    fun3(10);
    cout << y << endl;
    int* p = &y;
    auto fun4 = [p]() mutable { return *p; };
    cout << fun4() << endl;

    return 0;
}

除了上述规则还有一些需要注意的问题:

  • 父作用域指包含lambda函数的语句块
  • 语法上捕捉列表可由多个捕捉项组成,并以逗号分割。比如:[=, &a, &b]表示以引用传递的方式捕捉变量a和b,值传递方式捕捉其他所有变量;[&,a, this]表示值传递方式捕捉变量a和this,引用方式捕捉其他变量。
  • 捕捉列表不允许变量重复传递,否则会导致编译错误。比如:[=, a]:=已经以值传递方式捕捉了所有变量,重复捕捉a。
  • 在块作用域({}中的内容)以外的 lambda函数捕捉列表必须为空。
  • 在块作用域中的 lambda函数仅能捕捉父作用域中局部变量,捕捉任何非此作用域或者非局部变量都 会导致编译报错。
  • lambda表达式之间不能相互赋值,即使看起来类型相同
#include<iostream>
using namespace std;
void (*PF)();

int main()
{
    auto f1 = [] {cout << "hello world" << endl; };
    auto f2 = [] {cout << "hello world" << endl; };
    //f1 = f2;// lambda表达式不可相互赋值,编译失败

    //允许对一个lambda表达式进行拷贝构造
    auto f3(f2);
    f3();

    //可以将lambda表达式赋值给相同类型的函数指针
    PF = f2;
    PF();
    return 0;
}

2.lambda表达式与仿函数

仿函数又称为函数对象,即可以像函数一样使用的对象,就是在类中重载了operator()运算符的类对象。那么它与lambda表达式有什么关系呢?

#include<iostream>
class Rate
{
public:
    Rate(double rate) : _rate(rate)
    {}
    double operator()(double money, int year)
    {
        return money * _rate * year;
    }

private:
    double _rate;
};

int main()
{
    // 仿函数
    double rate = 0.49;
    Rate r1(rate);
    r1(10000, 2);
    
    // lambda表达式
    auto r2 = [=](double monty, int year)->double {return monty * rate * year;
    };
    r2(10000, 2);

    return 0;
}

从使用方式上来看,仿函数与lambda表达式完全一致。仿函数将_rate作为其成员变量,在定义对象时给出初始值即可,而lambda表达式则通过捕捉列表可以直接将该变量捕获到。我们通过调试查看以上函数调用的汇编代码:

实际在底层编译器对于lambda表达式的处理方式,完全就是按照处理仿函数的方式处理的,即:如果定义了一个lambda表达式,编译器会自动生成一个类,在该类中重载operator()。

八、可变参数模板

1.可变参数模板的概念

C++11的新特性可变参数模板能够让您创建可以接受可变参数的函数模板和类模板,相比C++98的类模版和函数模版中只能使用固定数量的模版参数,C++11中这个参数的数目可以是非固定个数个。然而由于可变模版参数比较抽象,使用起来需要一定的技巧,所以这部分的知识晦涩难懂。现阶段,我们了解基础的可变参数模板特性就够了,以后如果有需要可以深入学习。 下面就是一个基本可变参数的函数模板:

#include<iostream>
using namespace std;
// Args是一个模板参数包,args是一个函数形参参数包
// 声明一个参数包Args...args,这个参数包中可以包含0到任意个模板参数。
template <class ...Args >
void ShowList(Args... args) 
{
    cout << "void ShowList(Args... args) " << endl;
}

int main()
{
    int a = 0;
    char b = '0';
    char* p1 = &b;
    const char* p2 = "Hello world";
    double d = 1.0;
    ShowList();
    ShowList(a);
    ShowList(a, b);
    ShowList(a, b, p1);
    ShowList(a, b, p1, p2);
    ShowList(a, b, p1, p2, d);
    return 0;
}

代码中所有的函数都正常打印。

上面的参数args前面有省略号,就代表它是一个可变模版参数,我们把带省略号的参数称为“参数包”,它里面包含了0到N(N>=0)个模版参数。

2.参数包的展开

(1)递归展开

我们无法直接获取参数包args中的任何一个参数的,只能通过展开参数包的方式来获取参数包中的每个参数,这是使用可变模版参数的一个主要特 点,也是最大的难点,即如何展开观察可变模版参数。

由于语法上就不支持args[i]这样方式获取可变参数,所以我们的用一些奇招来一一获取参数包的值。

我解释一下下面这段代码:

template

void ShowList(T value, Args... args);

这个函数模板用于递归,它从第一个参数开始将参数与后面的参数包分开,然后打印。再次进入递归后,此时传递的就是少了第一个参数的参数包,再孤立出来打印,直到参数包只剩一个参数再次传入时就不再满足上面的函数模板,就会用下面的函数模板实例化:

template

void ShowList(const T& t);

此时就能终止递归,所有的参数也被打印出来了。

// 递归终止函数
template <class T>
void ShowList(const T& t)
{
    cout << t << endl;
}

// 递归展开函数
template <class T, class ...Args>
void ShowList(T value, Args... args)
{
    cout << value << " ";
    ShowList(args...);
}

int main()
{
    ShowList(1);
    ShowList(1, 'A');
    ShowList(1, 'A', std::string("sort"));
    return 0;
}

(2)逗号表达式展开

这种展开参数包的方式,不需要通过递归终止函数,是直接在expand函数体中展开的, printarg不是一个递归终止函数,只是一个处理参数包中每一个参数的函数。这种就地展开参数包的方式 实现的关键是逗号表达式。我们知道逗号表达式会按顺序执行逗号前面的表达式。

expand函数中的逗号表达式:(printarg(args), 0),也是按照这个执行顺序,先执行printarg(args),再得到逗号表达式的结果0。同时还用到了C++11的另外一个特性——初始化列 表,通过初始化列表来初始化一个变长数组, {(printarg(args), 0)...}将会展开成((printarg(arg1),0), (printarg(arg2),0), (printarg(arg3),0), etc... ),最终会创建一个元素值都为0的数组int arr[sizeof...(Args)]。由于是逗号表达式,在创建数组的过程中会先执行逗号表达式前面的部分printarg(args)打印出参数,也就是说在构造int数组的过程中就将参数包展开了,这个数组的目的纯粹是为了在 数组构造的过程展开参数包。

template <class T>
void PrintArg(T t)
{
     cout << t << " ";
}
 
//展开函数
template <class ...Args>
void ShowList(Args... args)
{
    int arr[] = { (PrintArg(args), 0)... };
    cout << endl;
}
 
int main()
{
    ShowList(1);
    ShowList(1, 'A');
    ShowList(1, 'A', std::string("sort"));
    return 0;
}

3.emplace接口

emplace这个接口在很多数据结构中都出现了,它就是使用了可变参数列表。

各个数据结构中的emplace和insert是对应的,emplace_back和push_back是对应的,它们的使用是基本一致的。

#include<iostream>
#include<list>
using namespace std;
int main()
{
    std::list< std::pair<int, char> > mylist;
    //push_back只能插入元素对应的数据类型或者initializer_list这样类呢提供对应构造函数的数据类型
    mylist.push_back(make_pair(40, 'd'));
    mylist.push_back({ 50, 'e' });
    
    //emplace_back支持可变参数,拿到构建pair的参数后可以自己构建对象
  
    mylist.emplace_back(10, 'a');
    mylist.emplace_back(20, 'b');
    
    //当然也可以像push_back一样使用
    mylist.emplace_back(make_pair(30, 'c'));
    mylist.push_back(make_pair(40, 'd'));
    mylist.push_back({ 50, 'e' });
 
    for (auto e : mylist)
        cout << e.first << ":" << e.second << endl;
    
    return 0;
}

九、包装器

1.function

function包装器也叫作适配器。C++中的function本质是一个类模板,也是一个包装器。

#include<iostream>
template<class F, class T>
T useF(F f, T x)
{
    static int count = 0;
    cout << "count:" << ++count << endl;
    cout << "count:" << &count << endl;
    return f(x);
}

double f(double i)
{
    return i / 3;
}

struct Functor
{
    double operator()(double d)
    {
        return d / 3;
    }
};

int main()
{
    // 函数名
    cout << useF(f, 11.11) << endl;

    // 函数对象
    cout << useF(Functor(), 11.11) << endl;

    // lamber表达式
    cout << useF([](double d)->double { return d / 3; }, 11.11) << endl;

    return 0;
}

上面的函数名、函数对象和lambda表达式都传递到函数模板中,它们都实例化出了自己的函数。三个函数操作相同,但是却因为类型不同而多生成了需要维护的重复代码,此时就可以使用function包装器来对它们进行统一。

function包装器是一个在functional.h中的类,其中class Ret表示函数的返回类型,class... Args表示函数的参数。

以下其声明:

template function;

template

class function;

应用包装器就可以使函数变量的类型统一,此时实例化多份模板的问题就被解决了。

#include<iostream>
#include<functional>
using namespace std;
int f(int a, int b)
{
    return a + b;
}

struct Functor
{
public:
    int operator() (int a, int b)
    {
        return a + b;
    }
};

class Plus
{
public:
    static int plusi(int a, int b)
    {
        return a + b;
    }
    
    double plusd(double a, double b)
    {
        return a + b;
    }
};

int main()
{
    // 函数名(函数指针)
    std::function<int(int, int)>func1 = f;
    cout << func1(1, 2) << endl;

    // 函数对象
    std::function<int(int, int)>func2 = Functor();
    cout << func2(1, 2) << endl;

    // lambda表达式
    std::function<int(int, int)>func3 = [](const int a, const int b){return a + b;};
    cout << func3(1, 3) << endl;

    // 类的静态成员函数
    std::function<int(int, int)> func4 = &Plus::plusi;
    cout << func4(1, 2) << endl;

    //类的普通成员函数
    std::function<double(Plus, double, double)> func5 = &Plus::plusd;
    cout << func5(Plus(), 1.1, 2.2) << endl;

    return 0;
}

2.bind

std::bind函数定义在头文件中,它是一个函数模板,也是一个函数包装器(适配器),接受一个可调用对象,生成一个新的可调用对象来“适应”原对象的参数列表。一般而言,我们用它可以把一个原本接收N个参数的函数fn,通过绑定一些参数,返回一个接收M个(M与N大小没有关系)参数的新函数。而且,使用std::bind函数还可以实现参数顺序调整等。

bind的声明如下:

template

bind (Fn&& fn, Args&&... args);

template

bind (Fn&& fn, Args&&... args);

两个类一个有返回类型,一个没有返回类型。

可以将bind函数看作是一个通用的函数适配器,它接受一个可调用对象,生成一个新的可调用对 象来“适应”原对象的参数列表。 调用bind的一般形式:auto newCallable = bind(callable,arg_list);

其中,newCallable本身是一个可调用对象,arg_list是一个逗号分隔的参数列表,对应给定的callable的参数。当我们调用newCallable时,newCallable会调用callable,并传给它arg_list中 的参数。

arg_list中的参数可能包含形如_n的名字,其中n是一个整数,这些参数是“占位符”,也就是说你只要将占位符写上去了,写上去几个,那么你在调用该函数时就一定要写这个数量的参数。而且占位符placeholders::_1指原函数从左向右数的第一个参数,placeholders::_2指原函数从左向右数的第二个参数,以此类推……

#include<functional>
#include<iostream>
using namespace std;

int Plus(int a, int b)
{
    return a + b;
}

class Sub
{
public:
    int sub(int a, int b)
    {
        return a - b;
    }
};

int main()
{
    //表示绑定函数plus,函数的参数分别由调用func1的第一,二个参数指定
    auto func1 = std::bind(Plus, placeholders::_1, placeholders::_2);
    cout << func1(1, 2) << endl;

    //fun2绑定了Plus(1, 2)
    auto func2 = std::bind(Plus, 1, 2);
    cout << func2() << endl;
    
    Sub s;
    // 绑定成员函数
    std::function<int(int, int)> func3 = std::bind(&Sub::sub, s, placeholders::_1, placeholders::_2);
    cout << func3(1, 2) << endl;//1-2 = -1

    // 参数调换顺序
    std::function<int(int, int)> func4 = std::bind(&Sub::sub, s, placeholders::_2, placeholders::_1);
    cout << func4(1, 2) << endl;//2-1 = 1
    return 0;
}

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值