• C++ | C++11新特性(下)


    目录

    前言

    一、可变参数模板

    1、关于解包的两种方式 

    2、emplace系类接口

    二、lambda

    1、初始lambda

    2、lambda的使用

    3、lambda的底层原理

    三、包装器

    1、初始function

    2、包装器的使用场景

    3、bind

    四、线程库

    1、线程库相关接口

    2、关于线程库中的一些细节问题 

    3、线程相关接口

    4、线程安全相关问题

    5、锁的分类

    6、lockguard

    7、条件变量


    前言

            前面我们介绍了C++11列表初始化、新的类功能以及右值引用等新特性,本文继续介绍关于可变参数模板以及lambda表达式等新语法;

    一、可变参数模板

            在C++11前,我们有普通固定数量模板参数,但对于可变参数,我们无从下手,而在C++11后,推出了可变模板参数;使形参可以接收0个至多个不同或同种类型的参数;以下为具体代码;

    1. template<class ...Args>
    2. void showList(Args... args)
    3. {
    4. // ...
    5. }

            其中,Args就是可变参数类型名,也可以取别的名字,args我们称这个为参数包,同样也可以取别的名字;该参数包可以接收0个甚至多个参数;关于这个参数包的解析方式,我们可以使用以下两种方式;

    1、关于解包的两种方式 

    1. // 方法一:递归取参
    2. void showList()
    3. {
    4. cout << endl;
    5. }
    6. template<class T, class ...Args>
    7. void showList(const T& t, Args... args)
    8. {
    9. cout << t << " ";
    10. showList(args...);
    11. }
    12. void test13()
    13. {
    14. showList();
    15. showList(1);
    16. showList(1, 'x');
    17. showList(1, 1.11, 'y');
    18. }
    1. // 方式二:利用数组自动推导元素个数
    2. template <class T>
    3. void PrintArg(T t)
    4. {
    5. cout << t << " ";
    6. }
    7. template <class ...Args>
    8. void ShowList(Args... args)
    9. {
    10. // 逗号表达式
    11. int arr[] = { 0, (PrintArg(args), 0)...};
    12. cout << endl;
    13. }
    14. void test13()
    15. {
    16. ShowList();
    17. ShowList(1);
    18. ShowList(1, 'x');
    19. ShowList(1, 1.11, 'y');
    20. }

    2、emplace系类接口

            emplace系列接口都为插入接口;其是采用了参数包的方式进行传参;STL中大部分容器都提供了这个插入接口;例如下面几个;

            那么emplace插入接口与普通的插入接口有什么区别呢?这里我用vector来举例;

    1. void test14()
    2. {
    3. list l1;
    4. MySpace::string str1("hello");
    5. // 无区别
    6. l1.push_back(str1); // 深拷贝
    7. l1.emplace_back(str1); // 深拷贝
    8. cout << endl << endl;
    9. // 无区别
    10. l1.push_back(move(str1)); // 移动构造
    11. l1.emplace_back(move(str1)); // 移动构造
    12. cout << endl << endl;
    13. // 有区别
    14. l1.push_back("11111"); // 拷贝构造 + 移动构造
    15. l1.emplace_back("11111"); // 直接构造
    16. }

    二、lambda

    1、初始lambda

            我们经常会写出类似以下代码;

    1. struct Goods
    2. {
    3. string _name; // 名字
    4. double _price; // 价格
    5. int _evaluate; // 评价
    6. Goods(const char* str, double price, int evaluate)
    7. :_name(str)
    8. , _price(price)
    9. , _evaluate(evaluate)
    10. {}
    11. };
    12. struct CmpByPriceLess
    13. {
    14. bool operator()(const Goods& x, const Goods& y)
    15. {
    16. return x._price < y._price;
    17. }
    18. };
    19. struct CmpByPriceGreater
    20. {
    21. bool operator()(const Goods& x, const Goods& y)
    22. {
    23. return x._price > y._price;
    24. }
    25. };
    26. void test1()
    27. {
    28. vector v = { { "苹果", 2.1, 5 }, { "香蕉", 3, 4 }, { "橙子", 2.2,
    29. 3 }, { "菠萝", 1.5, 4 } };
    30. // 价格升序
    31. sort(v.begin(), v.end(), CmpByPriceLess());
    32. // 价格降序
    33. sort(v.begin(), v.end(), CmpByPriceGreater());
    34. }

            使用sort的时候我们需要使用传入仿函数;你会不会有给这个仿函数传入何名称而感到烦恼呢?C++11中,推出了一种新玩法,便是我们的lambda;

    2、lambda的使用

    lambda的具体格式如下;

    [ capture-list ]( parameters ) mutable -> return-type { statement };

    看了上面的你或许还是有点懵,我们粗略的使用一下;

    1. void test1()
    2. {
    3. vector v = { { "苹果", 2.1, 5 }, { "香蕉", 3, 4 }, { "橙子", 2.2,
    4. 3 }, { "菠萝", 1.5, 4 } };
    5. // 价格升序
    6. //sort(v.begin(), v.end(), CmpByPriceLess());
    7. // 价格降序
    8. //sort(v.begin(), v.end(), CmpByPriceGreater());
    9. // 价格升序
    10. sort(v.begin(), v.end(), [](const Goods& x, const Goods& y)mutable->bool
    11. {
    12. return x._price < x._price;
    13. });
    14. // 价格降序
    15. sort(v.begin(), v.end(), [](const Goods& x, const Goods& y)mutable->bool
    16. {
    17. return x._price > x._price;
    18. });
    19. }

            首先我们介绍方括号中的是我们的捕捉列表,可以捕捉上下文中的变量供lambda函数使用;具体有以下几种捕捉方式;

    [var]:值捕捉;此时我们仅仅只是传值,lambda函数内修改值不会影响lambda外被捕捉的值;默认情况下,我们捕捉的值都是带const属性的,我们增肌mutable关键字可以使var可以修改(不改变lambda外被捕捉的值);

    [ = ]:全部值传递,捕捉上文中所有变量,且值传递给lambda函数;

    1. void test2()
    2. {
    3. int a = 3;
    4. int b = 5;
    5. // 值捕捉
    6. auto add = [a, b]()mutable->int
    7. {
    8. a++;
    9. return a + b;
    10. };
    11. // 全部值捕捉(若无参数,括号可省略;一般情况下,返回值也可以省略)
    12. // 如果不期望修改值传递进来的值,mutable也可以省略;
    13. auto sub = [=] { return a - b; };
    14. cout << add() << endl;
    15. cout << sub() << endl;
    16. cout << a << endl;
    17. }

    [ var& ]:引用传递,将某个值通过引用传递的方式传给lambda函数;我们在lambda函数内修改该值,由于是引用传递,也会影响该值本身;

    [ & ]:将上文的所有变量通过引用的方式进行捕捉;

    1. void test3()
    2. {
    3. int x = 3;
    4. int y = 4;
    5. // 若添加了mutable则圆括号不可省略
    6. auto f1 = [&x, y]()mutable {x = 1; y = 2; };
    7. f1();
    8. cout << x << " " << y << endl;
    9. // 将上文所有变量以引用的方式进行捕捉
    10. auto f2 = [&] {x = 10; y = 20; };
    11. cout << x << " " << y << endl;
    12. }

    [ this ]:捕获当前类的this指针;实际上之前的=在类内也会捕捉this指针,只不过是以值传递的方式;而&则是以引用的传递方式;

    1. class A
    2. {
    3. public:
    4. void func()
    5. {
    6. // 捕获当前类的this指针
    7. auto f1 = [this] {cout << _a << endl; };
    8. }
    9. private:
    10. int _a = 0;
    11. };

            我们再看圆括号,实际上,圆括号类似于函数中的圆括号,是用来进行接收参数的;mutable前面已经介绍过了, 对于普通值传递通常加上了const修饰,加了mutable以后,值传递就没有用const修饰了;箭头后面为返回值,返回值一般可有可无;花括号则是我们的函数体了;

    3、lambda的底层原理

            前面我们看了lambda的使用,那么lambda的底层原理又是什么呢?在此之前,我们回答一个问题,类似的lambda可以相互赋值吗?如下代码;

    1. // 是否正确?
    2. void test3()
    3. {
    4. auto f1 = [](int x, int y) {return x + y; };
    5. //auto f2 = [](int x, int y) {return x + y; };
    6. auto f2 = [](int x, int y) {return x - y; };
    7. f1 = f2;
    8. }

            实际上,不管我们是类似还是完全相同,我们都不能进行赋值,这是为什么呢?看起来不是完全相同吗?还有我们的lambda函数的本质到底是什么类型呢?我用下面一段代码来解释这些问题;

    1. struct Fun
    2. {
    3. int operator()(int x, int y)
    4. {
    5. return x + y;
    6. }
    7. };
    8. void test4()
    9. {
    10. auto f1 = [](int x, int y) { return x + y; };
    11. Fun f2;
    12. // 调用lambda函数
    13. f1(2, 5);
    14. // 调用仿函数
    15. f2(2, 5);
    16. }

            我们看到代码转汇编后的结果;

            我们发现两段代码转汇编后的结果几乎相同;没错,我们的lambda实际上就是仿函数,只不过编译器会自动帮我们生成类型,这个类名是lambda_ + UUID;UUID是唯一标识符;因此我们虽然看起来像相同的lambda函数,实际上,它们是类名不同的仿函数,既然类型不同,又如何相互赋值呢?前面的问题不就迎刃而解了么;

    三、包装器

    1、初始function

            我们的包装器,也叫做适配器;主要包装可调用对象;C++中,可调用对象有哪些呢?有我们C语言学的函数指针,有我们前面学的仿函数,还有我们刚刚学的lambda函数;我们可以对其进行同一的封装;如下代码所示;

    1. struct ADD
    2. {
    3. int operator()(int x, int y)
    4. {
    5. return x + y;
    6. }
    7. };
    8. int add(int x, int y)
    9. {
    10. return x + y;
    11. }
    12. void test5()
    13. {
    14. // 包装器封装仿函数
    15. function<int(int, int)> f1 = ADD();
    16. // 包装器封装函数指针
    17. int(*pf)(int, int) = add;
    18. function<int(int, int)> f1 = pf;
    19. // 包装器封装lambda函数
    20. function<int(int, int)> f1 = [](int x, int y) { return x + y; };
    21. }

            包装器出了可以封装上面的一些常规函数,还可以包装我们类中的成员函数;如下所示;

    1. class A
    2. {
    3. public:
    4. // 静态成员函数
    5. static int func1(int x, int y)
    6. {
    7. return x + y;
    8. }
    9. // 普通成员函数
    10. int func2(int x, int y)
    11. {
    12. return x + y;
    13. }
    14. private:
    15. };
    16. void test6()
    17. {
    18. // 封装静态成员函数(类名前可加&也可不加)
    19. //function f1 = A::func1;
    20. function<int(int, int)> f1 = &A::func1;
    21. // 封装普通成员函数(类型前必须加&,语法规定,且参数列表中声明类名)
    22. function<int(A, int, int)> f2 = &A::func2;
    23. }

    2、包装器的使用场景

            包装器的主要使用在一些需要对函数统一处理的场景,比如,我们有一个vector,其中存的是一些返回值,形参相同的可调用对象;但这些可调用对象可能是仿函数,可能是函数指针,也可能是lambda函数,这时它们并不是同一类,我们只能通过仿函数将其封装归为一类;

    1. struct functor
    2. {
    3. int operator()(int x, int y)
    4. {
    5. return x - y;
    6. }
    7. };
    8. int mul(int x, int y)
    9. {
    10. return x * y;
    11. }
    12. void test7()
    13. {
    14. vectorint(int x, int y)>> vfs;
    15. // lambda函数
    16. vfs.push_back([](int x, int y)-> int {return x + y; });
    17. // 仿函数
    18. functor ft;
    19. vfs.push_back(ft);
    20. // 函数指针
    21. int(*ptr)(int, int) = mul;
    22. vfs.push_back(ptr);
    23. }

    3、bind

            bind定义在头文件functional中,它更像一个函数模板的适配器,而上面的function是类模板的适配器;bind主要有两个功能,分别为调整参数顺序调整参数个数;下面我们一一进行展示;

    1. // 调整参数顺序
    2. void Func1(int x, int y)
    3. {
    4. cout << x << " " << y << endl;
    5. }
    6. void test8()
    7. {
    8. // 调整参数顺序
    9. Func1(10, 20);
    10. // placeholder为命名空间,_1,_2....._n都为调整的参数顺序
    11. auto f1 = bind(Func1, placeholders::_2, placeholders::_1);
    12. // 写法二
    13. function<void(int, int)> f2 = bind(Func1, placeholders::_2, placeholders::_1);
    14. f1(10, 20);
    15. }
    1. // 调整参数个数
    2. class Cal
    3. {
    4. public:
    5. Cal(double rate = 2.5)
    6. :_rate(rate)
    7. {}
    8. double cal(int x, int y)
    9. {
    10. return (x + y) * _rate;
    11. }
    12. private:
    13. double _rate;
    14. };
    15. void test9()
    16. {
    17. int x = 3;
    18. int y = 6;
    19. Cal c1;
    20. cout << c1.cal(x, y) << endl;
    21. // 调整参数个数
    22. auto func2 = bind(&Cal::cal, c1, placeholders::_1, placeholders::_2);
    23. cout << func2(x, y) << endl;
    24. }

            实际上,调整参数个数就是在我们的bind中显示的传入该参数;

    四、线程库

            在C++11后,推出了一种面向对象的线程操作手段;对比于以前不仅仅是有面向对象这一好处,实际上,还解决了跨平台的问题;我们linux下线程操作的接口与我们window下线程操作接口是不同的,因此在C++11以前,我们对于有线程相关操作的程序拥有跨平台性,我们必须通过条件编译实现两套实现方案;但是在C++11后,我们可以通过使用我们的线程库实现跨平台(实际上底层还是条件编译,只是人家写好了);

    1、线程库相关接口

            线程库给我们提供了如下相关接口;

            如果你曾有过Linux或Windows下多线程开发经历,上述接口可能看一眼就会使用了;首先我们来看构造函数;

            析构函数我们无需关心,会自动调用; 我们的赋值重载只有右值版本,左值版本被删除了;

            其他一些接口使用也非常简单,都是一些无参公有成员函数,具体功能如下图所示;

    2、关于线程库中的一些细节问题 

            细节一:这里的get_id返回的线程id并不是与Linux下相同是一个整型,而是一个结构化数据;具体如下所示;

    1. // vs下查看
    2. typedef struct
    3. {
    4. /* thread identifier for Win32 */
    5. void *_Hnd; /* Win32 HANDLE */
    6. unsigned int _Id;
    7. } _Thrd_imp_t;

            细节二:对于创建线程时的第一个参数,可以是函数指针,可以是仿函数,可以是lambda函数;

    1. void Func1()
    2. {
    3. cout << "thread 1" << endl;
    4. }
    5. struct Func2
    6. {
    7. void operator()()
    8. {
    9. cout << "thread 2" << endl;
    10. }
    11. };
    12. int main()
    13. {
    14. // 函数指针
    15. thread t1(Func1);
    16. // 传仿函数对象
    17. Func2 f2;
    18. thread t2(f2);
    19. // 传lambda函数
    20. thread t3([] { cout << "thread 3" << endl; });
    21. t1.join();
    22. t2.join();
    23. t3.join();
    24. return 0;
    25. }

            细节三:关于线程函数的传参,线程函数若想传引用,则必须调用ref函数;并且当我们传引用与传指针时,若我们调用的detach,将线程分离时,我们需要注意该引用/指针指向的对象若为栈上的对象,有可能会出现越界访问的现象;即该对象所在线程若结束,或出该对象作用域,该对象会被销毁,而子线程仍然访问会引起越界访问的现象; 

    3、线程相关接口

            除了thread类的成员函数还有以下这些来自this_thread这个命名空间中的函数;

    4、线程安全相关问题

            同样我们C++11封装的线程库使用时也会存在线程安全问题;下面代码便是一段有线程安全的代码;

    1. static int sum = 0;
    2. void transaction(int num)
    3. {
    4. for (int i = 0; i < num; i++)
    5. {
    6. sum++;
    7. }
    8. }
    9. int main()
    10. {
    11. // 创建线程
    12. //template
    13. //explicit thread(Fn && fn, Args&&... args);
    14. // 参数一fn通常是一个函数指针,表示该线程调用的函数
    15. // 参数二是可变模板参数,会自动解析,并传给我们参数函数指针所指向的函数
    16. thread t1(transaction, 100000);
    17. thread t2(transaction, 200000);
    18. // 线程等待(必须进行线程等待,除非子线程执行函数中进行了detach)
    19. t1.join();
    20. t2.join();
    21. cout << sum << endl;
    22. return 0;
    23. }

            明明是同一段代码,差距竟然如此之大; 我们原本意料的结果是300000;却有如上各种不同的结果,通常我们会采用加锁的方式进行处理;C++11也为我们的锁也进行了封装;具体如下;

    5、锁的分类

            C++11具体提供了如下四种锁,我们主要讲解mutex,普通互斥锁,因为会普通互斥锁,其他锁理解起来也就很简单了;

            互斥锁主要有以下几个接口;

            lock为上锁(阻塞调用),unlock为解锁;try_lock为尝试申请锁,若未申请到则返回false,是一种非阻塞调用; 我们通过这些接口可以对上述代码加以线程安全;

    1. static int sum = 0;
    2. mutex mx; // 创建锁变量
    3. void transaction(int num)
    4. {
    5. // 上锁
    6. mx.lock();
    7. for (int i = 0; i < num; i++)
    8. {
    9. sum++;
    10. }
    11. mx.unlock(); // 解锁
    12. }
    13. int main()
    14. {
    15. thread t1(transaction, 100000);
    16. thread t2(transaction, 200000);
    17. t1.join();
    18. t2.join();
    19. cout << sum << endl;
    20. return 0;
    21. }

    6、lockguard

            lockguard是mutex使用RAII机制对锁进行了封装,通过这种机制,我们可以使锁在某个局部作用域生效,实际原理是使用类的构造函数与析构函数实现的;因为构造函数在对象定义时调用,在出作用域时调用析构函数,我们可以在构造函数lock申请锁,在析构函数unlock解锁;下面是我模拟实现的一个lockguard;

    1. #pragma once
    2. #include
    3. template<class Lock>
    4. class Lockguard
    5. {
    6. public:
    7. Lockguard(std::Lock& mt)
    8. :_mt(mt)
    9. {
    10. _mt.lock();
    11. }
    12. ~Lockguard()
    13. {
    14. _mt.unlock();
    15. }
    16. private:
    17. std::Lock& _mt;
    18. };

            实际上库里也有这样的类;分别叫做lock_gurad和unique_lock;lock_guard与我们上述实现的几乎相同;而unique_lock仅仅只是给我们多增加了一些加锁解锁的接口;

            下面,我们用lock_guard再次升级一下我们之前写的代码; 

    1. static int sum = 0;
    2. mutex mx; // 创建锁变量
    3. void transaction(int num)
    4. {
    5. // 出作用域自动销毁
    6. lock_guard lg(mx);
    7. for (int i = 0; i < num; i++)
    8. {
    9. sum++;
    10. }
    11. }
    12. int main()
    13. {
    14. thread t1(transaction, 100000);
    15. thread t2(transaction, 200000);
    16. t1.join();
    17. t2.join();
    18. cout << sum << endl;
    19. return 0;
    20. }

    7、条件变量

            C++封装的条件变量与Linux下的条件变量使用差不多,只是进行了封装,提供了跨平台性;具体接口如下图所示;

            关于条件变量的使用我们用一道题进行展示;

    使用两个线程,一个打印奇数,一个打印偶数,交替打印至100;

  • 相关阅读:
    v-show与v-if控制图片显隐的本质
    Jmeter-Windows环境配置
    swagger(API接口文档利器)
    快来白漂动漫头像~Python调用百度AI接口,1行代码免费转换200张
    基于Mediapipe的对象分类任务,CPU平台毫秒级别延迟
    【PAT甲级】1001 A+B Format
    jq工具及其常用用法
    SpringBoot中15个常用启动扩展点,你用过几个?
    HTML——5.表单、框架、颜色
    堆友:阿里巴巴文生图工具又出新功能(局部重绘)
  • 原文地址:https://blog.csdn.net/Nice_W/article/details/132152478