当前位置:首页 » 《关于电脑》 » 正文

【C++】C++11(lambda、可变参数模板、包装器、线程库)

14 人参与  2024年12月19日 10:02  分类 : 《关于电脑》  评论

点击全文阅读


  ?个人主页:秦jh_-CSDN博客
? 系列专栏:https://blog.csdn.net/qinjh_/category_12575764.html?spm=1001.2014.3001.5482

 9efbcbc3d25747719da38c01b3fa9b4f.gif​ 

目录

前言

lambda表达式

C++98中的一个例子

lambda表达式语法

 函数对象与lambda表达式

新的类功能 

默认成员函数 

强制生成默认函数的关键字default

禁止生成默认函数的关键字delete 

可变参数模板 

递归函数方式展开参数包 

逗号表达式展开参数包 

STL容器中的empalce相关接口函数: 

 包装器

 function包装器

使用方法

 包装成员函数的函数指针:

bind

线程库

thread类的简单介绍 

线程函数参数

lock_guard

 unique_lock

 原子性操作库(atomic)

条件变量


前言

    ? hello! 各位铁子们大家好哇。

             今日更新了C++11的相关内容
    ? 欢迎大家关注?点赞?收藏⭐️留言?

lambda表达式

C++98中的一个例子

待排序元素为自定义类型,需要用户定义排序时的比较规则 。每次为了实现一个algorithm算法, 都要重新去写一个类,如果每次比较的逻辑不一样,还要去实现多个类。因此,在C++11语法中出现了Lambda表达式。

lambda表达式语法

lambda表达式书写格式:[capture-list] (parameters) mutable -> return-type { statement }

1. lambda表达式各部分说明:

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

lambda表达式实际上可以理解为无名函数,该函数无法直接调用,如果想要直接调用,可借助auto将其赋值给一个变量。 

lambda表达式的定义是一个局部的匿名对象。lambda编译时,编译器会生成对应仿函数。

lambda的本质是仿函数。  

 

默认捕捉过来的对象是const修饰的,并且是传值捕捉。所以如果要修改,就要加上mutable。传值捕捉的对象如果要修改,就要加上mutable。 加上mutable后,就可以修改了。但a,b是值拷贝,所以修改局部域里的a,b,不影响外面的a,b。

因为这里是引用方式捕捉,所以不用mutable也可以修改捕捉对象。 调用swap3后,修改里面的a,b,会影响外面的a,b。

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

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

注意:

父作用域指包含lambda函数的语句块语法上捕捉列表可由多个捕捉项组成,并以逗号分割。捕捉列表不允许变量重复传递,否则就会导致编译错误。比如:[=, a]

上图是值捕捉和引用捕捉的例子。

上图都是混合捕捉。图二是a,b传引用捕捉,d,e传值捕捉。 

 函数对象与lambda表达式

函数对象,又称为仿函数,即可以像函数一样使用的对象,就是在类中重载了operator()运算符的 类对象。

函数对象在定义对象时给出初始值即可,lambda表达式通过捕获列表可以直接将该变量捕获到。

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

新的类功能 

默认成员函数 

原来C++类中,有6个默认成员函数:

 构造函数析构函数 拷贝构造函数拷贝赋值重载 取地址重载 const 取地址重载 

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

注意:

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

强制生成默认函数的关键字default

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

禁止生成默认函数的关键字delete 

 

 如果想要限制某些默认函数的生成,在C++98中,是将该函数设置成private,并且只进行声明。在C++11中,只需在该函数声明加上=delete即可,编译器不生成对应函数的默认版本,称=delete修饰的函数为删除函数。

可变参数模板 

 下面是一个基本可变参数的函数模板

// Args是一个模板参数包,args是一个函数形参参数包// 声明一个参数包Args...args,这个参数包中可以包含0到任意个模板参数。template <class ...Args> void ShowList(Args... args) {}

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

 如果想知道参数包的参数个数,可以用sizeof。不过需要注意写法sizeof...(args)。

语法不支持使用args[i]这样的方式获取可变参数。

递归函数方式展开参数包 

我们无法直接获取参数包args中的每个参数, 只能通过展开参数包的方式来获取参数包中的每个参数。

当传三个参数时,参数包有三个参数,其中第一个参数会匹配val,剩下两个参数到了另一个参数包中,然后进行递归,最后再写上一个递归终止函数。

逗号表达式展开参数包 

template <class T> void PrintArg(T t) {     cout << t << " "; } //展开函数template <class ...Args> void ShowList(Args... args) { int arr[] = { (PrintArg(args), 0)... }; cout << endl; }

这里不使用逗号表达式,做出如下图的修改 

 

PrintArg只接受一个参数,当参数包有三个参数时, 构造arr数组时,会调用三次PrintArg,返回三个0。arr数组就会开3个int大小的空间        。

STL容器中的empalce相关接口函数: 

 

emplace系列的接口,支持模板的可变参数,并且万能引用。 

上图中push_back和emplace_back二者没啥区别。实际中,emplace_back不会这样传,如果直接传pair,效率跟push_back是一样的。实际是传pair的参数包,emplace不会将这些参数包构造pair对象,而是会一直往下传,直到new出节点的时候,直接拿出参数包进行初始化。

 

上面是emplace的简单实现。传入参数包时,会将参数包一直往下传,直到new节点时,根据_data的类型进行初始化,直接构造。 

总结:push_back只能先构造再拷贝构造,emplace可以兼容push_back的功能,还能接收参数包进行直接构造。

效率比较结论:

 包装器

 function包装器

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

 包装器不是定义可调用对象,而是包装可调用对象。

现今所学的可调用对象有函数指针、仿函数对象、lambda。但他们有各有缺点:

因此,使用function包装起来,这样方便统一类型。

模板参数说明:

Ret: 被调用函数的返回类型

Args…:被调用函数的形参

 function的构造

function还重载了operator() 

使用方法

#include<functional>int f(int a, int b){return a + b;}struct Functor{public:int operator() (int a, int b){return a + b;}};//不是定义可调用对象,而是包装可调用对象int main(){function<int(int, int)> fc1;//function<int(int, int)> fc2(f);function<int(int, int)> fc2 = f; //单参数构造隐式类型转换  //函数指针function<int(int, int)> fc3 = Functor();  //仿函数对象function<int(int, int)> fc4 = [](int x, int y) {return x + y; }; //lambda cout << fc2(1, 2) << endl;cout << fc2.operator()(1, 2) << endl;cout << fc3(1, 2) << endl;cout << fc4(1, 2) << endl;return 0;}

 function其实就是个仿函数,它是将现有的东西传给它,然后存起来,再通过operator()把参数传给可调用对象去调用。

 下面通过一道题了解它的用法:

. - 力扣(LeetCode). - 备战技术面试?力扣提供海量技术面试资源,帮助你高效提升编程技能,轻松拿下世界 IT 名企 Dream Offer。icon-default.png?t=O83Ahttps://leetcode-cn.com/problems/evaluate-reverse-polish-notation/submissions/

class Solution {public:    int evalRPN(vector<string>& tokens) {        stack<int> st;        map<string,function<int(int,int)>> opFuncMap={            {"+",[](int a,int b){return a+b;}},            {"-",[](int a,int b){return a-b;}},            {"*",[](int a,int b){return a*b;}},            {"/",[](int a,int b){return a/b;}}        };        for(auto& str:tokens)        {            if(opFuncMap.count(str)) //操作符            {                function<int(int,int)> func=opFuncMap[str];                int right=st.top();                st.pop();                int left=st.top();                st.pop();                st.push(func(left,right));            }            else            {                st.push(stoi(str));            }        }        return st.top();    }};

 上方是参考代码。

 包装成员函数的函数指针:

上面这种方法需要定义有名对象,很麻烦。下面是另一种方式:

这种方法可以用匿名对象。实际上,调用非静态的成员函数不是直接把参数传给过去。成员函数调用的时候,不能直接传this指针。

operator()底层:如果是指针,就用对象的指针去调用plusd,如果是对象,就用对象去调用。所以里面的参数不是直接传过去的,如果是成员函数,就用该对象去调用。

bind

std::bind函数定义在头文件中,是一个函数模板,它就像一个函数包装器(适配器),接受一个可调用对象(callable object),生成一个新的可调用对象来“适应”原对象的参数列表。一般而言,我们用它可以把一个原本接收N个参数的函数fn,通过绑定一些参数,返回一个接收M个(M可以大于N,但这么做没什么意义)参数的新函数。同时,使用std::bind函数还可以实现参数顺序调整等操作。 

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

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

arg_list中的参数可能包含形如_n的名字,其中n是一个整数,这些参数是“占位符”,表示 newCallable的参数,它们占据了传递给newCallable的参数的“位置”。数值n表示生成的可调用对 象中参数的位置:_1为newCallable的第一个参数,_2为第二个参数,以此类推。


  

上图是用bind调整参数顺序。 _n表示对应的第n个参数,如果没展开命名空间,就要显示写出来。下面是调整的原理。

调整参数个数:

 

注意,sub有三个参数,其中一个是隐藏的this指针。 这里和前面一样,可以传成员函数指针,或者对象。实际不会这样用。

实际使用如上,像固定的参数,可以直接绑死。这样就能调整参数个数了。

下面是另一个使用例子:

如果不想每次都传固定的参数,如名称,就可以绑定该参数。如下图:

绑定还可以绑中间的参数,如下图:


bind的返回值可以用function来接收。 

如果有一个方法对应的函数参数个数不是两个,这时就可以使用bind。


 下面介绍下模板中Ret的作用:

 Ret指的是可调用对象返回值的类型。这个地方用显式实例化的方式,控制可调用对象的返回值的类型。

线程库

thread类的简单介绍 

在C++11之前,涉及到多线程问题,都是和平台相关的,比如windows和linux下各有自己的接口,这使得代码的可移植性比较差。C++11中最重要的特性就是对线程进行支持了,使得C++在 并行编程时不需要依赖第三方库,而且在原子操作中还引入了原子类的概念。要使用标准库中的线程,必须包含< thread >头文件。 

注意:

线程是操作系统中的一个概念,线程对象可以关联一个线程,用来控制线程以及获取线程的状态。当创建一个线程对象后,没有提供线程函数,该对象实际没有对应任何线程。 

get_id()的返回值类型为id类型,id类型实际为std::thread命名空间下封装的一个类,该类中包含了一个结构体。

当创建一个线程对象后,并且给线程关联线程函数,该线程就被启动,与主线程一起运行。 线程函数一般情况下可按照以下三种方式提供:

函数指针lambda表达式函数对象

thread类是防拷贝的,不允许拷贝构造以及赋值,但是可以移动构造和移动赋值,即将一个 线程对象关联线程的状态转移给其他线程对象,转移期间不意向线程的执行。

 可以通过jionable()函数判断线程是否是有效的,如果是以下任意情况,则线程无效:

采用无参构造函数构造的线程对象线程对象的状态已经转移给其他线程对象线程已经调用jion或者detach结束

主线程有线程对象,很容易获取线程id。从线程没有线程对象,那该怎么获取从线程的线程id呢?

这里还有一个配套的类,封装在this_thread的命名空间里面。 里面的get_id接口就可以获取。

运行上面代码,结果不一定等于3000。这时候就得加锁了。

如上图,这个锁可以放在for循环里面,也可以放在外面,但是放在里面效率会更低一些。

每个线程都有独立的栈,for循环里的i都是各自的。

放到外面时,就是一个线程先加完了,另一个再加。因为循环++的过程是很快的,放到外面就相当于变成串行的了。放到里面的话线程就会一直来回切换,效率就是变低。

线程函数参数

 

如果不想用全局变量,可以传递给线程函数。

线程函数的参数是以值拷贝的方式拷贝到线程栈空间中的,因此:即使线程参数为引用类型,在线程中修改后也不能修改外部实参,因为其实际引用的是线程栈中的拷贝,而不是外部实参。 

如果想要通过形参改变外部实参时,必须借助std::ref()函数,强行把中间的拷贝变成引用拷贝。

注意:如果是类成员函数作为线程参数时,必须将this作为线程函数参数。

下面是用lambda的写法,

lock_guard

我们平时锁的时候是有风险的,如下图,如果中间部分抛异常了,就不会解锁了。解锁没有执行,就会发生死锁。

下面是一种解决方法,模拟lockguard的实现

 

这样就不怕抛异常了。程序正常结束,就调用析构函数。如果抛异常,生命周期也结束。  

 

如果有两个循环,第二个循环我们并不想锁。这时就可以用花括号来增加一个局部域,控制lock对象的生命周期。 

库里面的lock_guard是模板的,不仅可以管理互斥锁,别的锁也可以管理。它是自动的加锁和解锁的。

 unique_lock

unique_lock和lock_guard的功能类似。 但是unique_lock不仅可以支持自动的锁和解锁,还可以支持手动的。 如果在锁的中途想解锁就可以用unique_lock。

 原子性操作库(atomic)

多线程最主要的问题是共享数据带来的问题(即线程安全)。如果共享数据都是只读的,那么没问 题,因为只读操作不会影响到数据,更不会涉及对数据的修改,所以所有线程都会获得同样的数 据。但是,当一个或多个线程要修改共享数据时,就会产生很多潜在的麻烦。 

虽然加锁可以解决,但是加锁有一个缺陷就是:其他线程会被阻塞,会影响程序运行的效率,而且锁如果控制不好,还容易造成死锁。 

因此C++11中引入了原子操作。所谓原子操作:即不可被中断的一个或一系列操作,C++11引入 的原子操作类型,使得线程间数据的同步变得非常高效。 

 

 如上图,如果不用atomic,就可能会发生线程安全的问题。

用了atomic就可以解决。

注意:原子类型通常属于"资源型"数据,多个线程只能访问单个原子类型的拷贝,因此在C++11 中,原子类型只能从其模板参数中进行构造,不允许原子类型进行拷贝构造、移动构造以及 operator=等,为了防止意外,标准库已经将atmoic模板类中的拷贝构造、移动构造、赋值运算符重载默认删除掉了。 

条件变量

下面通过一个程序来介绍,支持两个线程交替打印,一个打印奇数,一个打印偶数。记得包头文件<condition_variable>

int main(){std::mutex mtx;condition_variable c;int n = 100;bool flag = true;thread t2([&]() {int j = 1;while (j < n){unique_lock<mutex> lock(mtx);// 只要flag == true t2一直阻塞// 只要flag == false t2不会阻塞while (flag)c.wait(lock);cout << j << endl;j += 2; // 奇数flag = true;c.notify_one();}});// 第一个打印的是t1打印0thread t1([&]() {int i = 0;while (i < n){unique_lock<mutex> lock(mtx);// flag == false t1一直阻塞// flag == true t1不会阻塞while (!flag)c.wait(lock);cout << i << endl;flag = false;i += 2; // 偶数c.notify_one();}});t1.join();t2.join();return 0;}

wait的功能:让当前线程阻塞,直到被唤醒。在进入阻塞的瞬间,会把锁进行解锁,允许在这个锁上阻塞的其他线程继续走。如果被唤醒,就会解阻塞,并且获得这个锁。 

notify就是唤醒线程,one是唤醒一个,all是唤醒全部。


点击全文阅读


本文链接:http://zhangshiyu.com/post/203432.html

<< 上一篇 下一篇 >>

  • 评论(0)
  • 赞助本站

◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。

关于我们 | 我要投稿 | 免责申明

Copyright © 2020-2022 ZhangShiYu.com Rights Reserved.豫ICP备2022013469号-1