C++11『lambda表达式 ‖ 线程库 ‖ 包装器』

✨个人主页: 北 海
🎉所属专栏: C++修行之路
🎃操作环境: Visual Studio 2022 版本 17.6.5

成就一亿技术人


文章目录

  • 🌇前言
  • 🏙️正文
    • 1.lambda表达式
      • 1.1.仿函数的使用
      • 1.2.lambda表达式的语法
      • 1.3.lambda表达式的使用
      • 1.4.lambda表达式的原理
      • 1.4.lambda表达式的优点及适用场景
    • 2.线程库
      • 2.1.thread 线程类
        • 2.1.1.this_thread 命名空间
      • 2.2.mutex 互斥锁类
        • 2.2.1.并行与串行的对比
        • 2.2.2.其他锁类型
        • 2.2.3.RAII 风格的锁
      • 2.3.condition_variable 条件变量类
        • 2.3.1.交替打印数字
      • 2.4.atomic 原子操作类
    • 3.包装器
      • 3.1.function 包装器
      • 3.2.bind 绑定
  • 🌆总结


🌇前言

自从C++98以来,C++11无疑是一个相当成功的版本更新。它引入了许多重要的语言特性和标准库增强,为C++编程带来了重大的改进和便利。C++11的发布标志着C++语言的现代化和进步,为程序员提供了更多工具和选项来编写高效、可维护和现代的代码


🏙️正文

1.lambda表达式

lambda 表达式 源于数学中的 λ 演算,λ 演算是一种 基于函数的形式化系统,它由数学家 阿隆佐邱奇 提出,用于研究抽象计算和函数定义。对于编程领域来说,可以使用 lambda 表达式 快速构建函数对象,作为函数中的参数

1.1.仿函数的使用

仿函数C++ 中的概念,指借助 类+operator()重载 创建的函数对象,仿函数 的使用场景如下

创建一个 vector,通过 sort 函数进行排序,至于结果为升序还是降序,可以通过 仿函数 控制

#include <iostream>
#include <unordered_map>
#include <iostream>
#include <vector>
#include <algorithm>using namespace std;struct cmpLess
{bool operator()(int n1, int n2){return n1 < n2;}
};struct cmpGreater
{bool operator()(int n1, int n2){return n1 > n2;}
};int main()
{vector<int> arr = { 8,5,6,7,3,1,1,3 };sort(arr.begin(), arr.end(), cmpLess()); // 升序cout << "升序: ";for (auto e : arr)cout << e << " ";cout << endl;sort(arr.begin(), arr.end(), cmpGreater()); // 降序cout << "降序: ";for (auto e : arr)cout << e << " ";cout << endl;return 0;
}

注:sort 如果不传递函数对象,默认排序结果为升序

结果为正确排序,但这种先创建一个仿函数对象,再调用的传统写法有点麻烦了,如果是直接使用 lambda 表达式 创建函数对象,整体逻辑会清楚很多

使用 lambda 表达式 修改后的代码如下,最大的改变就是 可以直接在传参时直接编写函数对象的代码逻辑

#include <iostream>
#include <unordered_map>
#include <iostream>
#include <vector>
#include <algorithm>using namespace std;int main()
{vector<int> arr = { 8,5,6,7,3,1,1,3 };sort(arr.begin(), arr.end(), [](int n1, int n2) { return n1 < n2; }); // 升序cout << "升序: ";for (auto e : arr)cout << e << " ";cout << endl;sort(arr.begin(), arr.end(), [](int n1, int n2) { return n1 > n2; }); // 降序cout << "降序: ";for (auto e : arr)cout << e << " ";cout << endl;return 0;
}

最终结果也是正常的

有了 lambda 表达式 之后,程序员不必再通过 仿函数 构建函数对象,并且可以在一定程度上提高代码的可阅读性,比如一眼就可以看出回调函数是在干什么

接下来看看如何理解 lambda 表达式 语法

1.2.lambda表达式的语法

lambda 表达式 分为以下几部分:

  • [ ] 捕捉列表
  • ( ) 参数列表
  • mutable 关键字
  • ->returntype 返回值类型
  • { } 函数体

[ ]( ) mutable ->returntype { }

其中,( ) 参数列表、mutable->returntype 都可以省略

  • 省略 ( )参数列表 表示当前是一个无参函数对象
  • 省略 mutable关键字 表示保持捕捉列表中参数的常量属性
  • 省略 ->returntype返回值类型 表示具体的返回值类型由函数体决定,编译器会自动推导出返回值类型

注意:

  • 捕捉列表 和 函数体 不可省略
  • 如果使用了 mutable关键字 或者 ->returntype 返回值,就不能省略 ( )参数列表,即使为空
  • 虽然返回值类型编译器可以推导,但最好还是注明返回值类型

也就是说,最基本的 lambda表达式 只需书写 [ ]{ } 即可表示,比如这样

int main()
{// 最简单的 lambda表达式[]{};return 0;
}

此时的 lambda表达式 相当于一个 参数为空、返回值为空、函数体为空 的匿名函数对象

void func()
{}

主要区别在于 lambda 表达式 构建出来的是一个 匿名函数对象,而 func 是一个 有名函数对象,可以直接调用

1.3.lambda表达式的使用

lambda 表达式 构建出的是一个 匿名函数对象,匿名函数对象也可以调用,不过需要在创建后立即调用,否则就会因为越出作用域而被销毁(匿名对象生命周期只有一行

下面通过 lambda 表达式 构建一个简单的 两整数相加 函数对象并调用

int main()
{int ret = [](int x, int y)->int { return x + y; }(1, 2);cout << ret << endl;return 0;
}

直接使用 lambda 表达式 构建出的 匿名函数对象 比较抽象,一般都是将此 匿名函数对象 作为参数传递(比如 sort),如果需要显式调用,最好是将创建出来的 匿名函数对象 赋给一个 有名函数对象,调用时逻辑会清晰很多

使用 auto 推导 匿名函数对象 的类型,然后创建 add 函数对象

int main()
{auto add = [](int x, int y)->int { return x + y; };int ret = add(1, 2);cout << ret << endl;return 0;
}

lambda 表达式 还有很多玩法,接下来逐一介绍,顺便学习其他组成部分


利用 lambda 表达式 构建一个交换两个元素的 函数对象

最经典的写法是 函数参数设为引用类型,传入两个元素,在函数体内完成交换

int main()
{int x = 1;int y = 2;cout << "交换前" << endl;cout << "\tx: " << x << endl << "\ty: " << y << endl;auto swap = [](int& rx, int& ry)->void{auto tmp = rx;rx = ry;ry = tmp;};swap(x, y);cout << "交换后" << endl;cout << "\tx: " << x << endl << "\ty: " << y << endl;return 0;
}

这种经典写法毋庸置疑,肯定能完成两数交换的任务

除此之外,还可以借助 lambda表达式 中的 捕捉列表 捕获外部变量进行交换

int main()
{int x = 1;int y = 2;cout << "交换前" << endl;cout << "\tx: " << x << endl << "\ty: " << y << endl;auto swap = [x, y]() ->void{auto tmp = x;x = y;y = tmp;};swap();cout << "交换后" << endl;cout << "\tx: " << x << endl << "\ty: " << y << endl;return 0;
}

因为现在 函数对象 是直接捕获外部变量进行操作,调用函数对象时,无需传参

代码写完,编译器立马给出了报错:xy 不可修改

这是因为 捕捉列表 中的参数是一个值类型(传值捕捉),此时的捕获的是外部变量的内容,然后赋值到 x、y 中,捕捉列表 中的参数默认具有 常量属性,不能直接修改,但可以添加 mutable 关键字 取消常性

int main()
{int x = 1;int y = 2;cout << "交换前" << endl;cout << "\tx: " << x << endl << "\ty: " << y << endl;auto swap = [x, y]()mutable ->void{auto tmp = x;x = y;y = tmp;};swap();cout << "交换后" << endl;cout << "\tx: " << x << endl << "\ty: " << y << endl;return 0;
}

但是程序运行结果不尽人意,外部的 x、y 并没有被交换,证明此时 捕捉列表 中的参数 x、y 是独立的值(类似函数中的值传递)

想让外部的 x、y 被真正捕获,需要使用 引用捕捉

int main()
{int x = 1;int y = 2;cout << "交换前" << endl;cout << "\tx: " << x << endl << "\ty: " << y << endl;// 引用捕捉auto swap = [&x, &y]() ->void{auto tmp = x;x = y;y = tmp;};swap();cout << "交换后" << endl;cout << "\tx: " << x << endl << "\ty: " << y << endl;return 0;
}

现在 x、y 被成功交换了

注意: 捕捉列表中的 &x 表示引用捕捉外部的 x 变量,并非取地址(特例)

所以说 mutable 关键字不常用,因为它取消的是值类型的常性,即使修改了,对外部也没有什么意义,如果想修改,直接使用 引用捕捉 就好了


捕捉列表 支持 混合捕捉,同时使用 引用捕捉 + 传值捕捉

int main()
{int x = 1;int y = 2;cout << "调用前" << endl;cout << "\tx: " << x << endl << "\ty: " << y << endl;// 混合捕捉auto func = [&x, y]()mutable ->void{x = 100;y = 200;};func();cout << "调用后" << endl;cout << "\tx: " << x << endl << "\ty: " << y << endl;return 0;
}

x 被修改了,而 y 没有


除了 混合捕捉 外,捕捉列表 还支持 全部引用捕捉全部传值捕捉

全部引用捕捉

int main()
{int x, y, z, a, b, c;x = y = z = 0;a = b = c = 1;string str = "Hello lambda!";cout << "&str: " << &str << endl << endl;auto func = [&]()->void{cout << x << " " << y << " " << z << " " << endl;cout << a << " " << b << " " << c << " " << endl;cout << str << endl;cout << "&str: " << &str << endl << endl;};func();return 0;
}

无需指定 捕捉列表 中的参数,& 可以一键 引用捕捉 外部所有变量

注:只能捕捉已经定义或声明的变量

全部传值捕捉

int main()
{int x, y, z, a, b, c;x = y = z = 0;a = b = c = 1;string str = "Hello lambda!";cout << "&str: " << &str << endl << endl;auto func = [=]()->void{cout << x << " " << y << " " << z << " " << endl;cout << a << " " << b << " " << c << " " << endl;cout << str << endl;cout << "&str: " << &str << endl << endl;};func();return 0;
}

全部传值捕捉 也能一键捕捉外部变量,不过此时捕获的是外部变量的值,并非变量本身,无法对其进行修改(可以通过 mutable关键字 取消常性)

注意: [=] 表示全部传值捕捉,[] 表示不进行捕捉,两者不等价

捕捉列表 的使用非常灵活,比如 [&, x] 表示 x 使用 传值捕捉,其他变量使用 引用捕捉[=, &str] 表示 str 使用 引用捕捉,其他变量使用 传值捕捉

捕捉列表 就像一个 “大师球”,可以直接捕捉到外部的变量,在需要大量使用外部变量的场景中很实用,有效避免了繁琐的参数传递与接收

有没有 全部引用捕捉 + 全部传值捕捉

当然没有,这是相互矛盾的,一个变量不可能同时进行 引用传递值传递,即便传递成功了,编译器在使用时也不知道使用哪一个,存在二义性,所以不被允许

注意: 关于 捕获列表 有以下几点注意事项

  1. 捕捉列表不允许变量重复传递,否则就会导致编译错误
  2. 在块作用域以外的 lambda 函数捕捉列表必须为空
  3. 在块作用域中的 lambda 函数不仅能捕捉父作用域中局部变量,也能捕捉到爷爷作用域中的局部变量


lambda表达式 还可以完美用作 线程回调函数,比如接下来使用 C++11 中的 thread 线程类,创建一个线程,并使用 lambda 表达式 创建一个线程回调函数对象

int main()
{// 创建线程,并打印线程idthread t([] { cout << "thread running... " << this_thread::get_id() << endl; });t.join();return 0;
}

总之 lambda 表达式 在实际开发中非常好用,关于 thread类的相关知识放到后面讲解,接下来先看看 lambda 表达式 的实现原理

1.4.lambda表达式的原理

lambda 表达式 生成的函数对象有多大呢?

是像 普通的函数对象指针 一样占 4/8 字节,还是像 仿函数 一样占 1 字节,通过 sizeof 计算大小就可以一探究竟

// 普通函数
int add(int x, int y)
{return x + y;
}// 仿函数
class addFunc
{
public:int operator()(int x, int y){return x + y;}
};int main()
{auto typeA = add;addFunc typeB;auto typeC = [](int x, int y)->int { return x + y; };cout << "普通函数: " << sizeof(typeA) << endl;cout << "仿函数: " << sizeof(typeB) << endl;cout << "lambda表达式: " << sizeof(typeC) << endl;return 0;
}

结果显示,lambda 表达式 生成的函数对象与 仿函数 生成的函数对象大小是一样的,都是 1字节


仿函数 生成的函数对象大小为 1字节是因为其生成了一个空类,实际调用时是通过 operator() 重载实现的,比如上面的 addFunc 类,空类因为没有成员变量,所以大小只为 1字节

由此可以推断 lambda 表达式 本质上也是生成了一个空类,分别查看使用 仿函数lambda 表达式 时的汇编代码

可以看到,这两段汇编代码的内容是一模一样的,都是先 call 一个函数(operator() 重载函数),然后再执行主体逻辑(两数相加),只不过使用 仿函数 需要自己编写一个 空类,而 使用 lambda 表达式 时由编译器生成一个 空类,为了避免这个自动生成的 空类 引发冲突,会将这个 空类 命名为 lambda_uuid

uuid通用唯一标识码,可以生成一个重复率极低的辨识信息,避免类名冲突,这也意味着即便是两个功能完全一样的 lambda 表达式,也无法进行赋值,因为 lambda_uuid 肯定不一样

所以在编译器看来,lambda 表达式 本质上就是一个 仿函数

1.4.lambda表达式的优点及适用场景

lambda 表达式 作为一种轻量级的匿名函数表示方式,具备以下优点:

  1. 简洁性: 对于简单的函数操作,无需再手动创建函数、调用,只需要编写一个 lambda 表达式生成函数对象
  2. 方便些: lambda 表达式具有 捕捉列表,可以轻松捕获外部的变量,避免繁琐的参数传递与接收
  3. 函数编程支持: lambda 表达式可以作为函数的参数、返回值或存储在数据结构中
  4. 内联定义: lambda 表达式Lambda表达式可以作为函数的参数、返回值或存储在数据结构中
  5. 简化代码: 对于一些简单的操作,使用 lambda 表达式可以减少代码的行数,提高代码的可读性

总的来说,lambda 表达式 可以替代一些代码量少的函数,使用起来十分方便,如果 lambda 表达式 编写出来的代码过于复杂时,可以考虑转为普通函数,确保代码的清晰性和可读性


2.线程库

关于 线程 相关操作,Linux 选择使用的是 POSIX 标准,而 Windows 没有选择 POSIX 标准,反而是自己搞了一套 API 和系统调用,称为 Win32 API,意味着 LinuxWindows 存在标准差异,直接导致能在 Linux 中运行的程序未必能在 Windows 中运行

C++11 之前,编写多线程相关代码如果保证兼容性,就需要借助 条件编译,分别实现两份代码,根据不同平台编译不同的代码(非常麻烦)

// 确保平台兼容性
#ifdef __WIN_32__CreateThread // Windows 中创建线程的接口// ...
#elsepthread_create // Linux 中创建线程的接口// ...
#endif

C++11 中,加入了 线程库 这个标准,其中包含了 线程、互斥锁、条件变量 等常用线程操作,并且无需依赖第三方库,也就意味着使用 线程库 编写的代码既能在 Linux 中运行,也能在 Windows 中运行,保障了代码的可移植性,除此之外,线程库 还新加入了 原子相关操作

2.1.thread 线程类

thread 线程类的概况如下

首先看看 thread 类中的 线程 id

Linux 中的 线程 id 表示每个轻量级进程 TCB 的起始地址,用一个 unsigned long int 表示,理解起来比较费劲;在 thread 类中,直接创建了一个 id 类,也就是这里的 thread::id,这个类用于标识 线程,同时在类中重载了一系列 operator 函数,用于两个 thread::id 对象的比较

线程创建后,系统会为其分配一个类型为 thread::id 的标识符,也就是该线程的唯一标识符

获取当前线程的 id,并进行比较

int main()
{thread::id id1 = std::this_thread::get_id();thread::id id2 = std::this_thread::get_id();cout << "id1: " << id1 << " " << "id2: " << id2 << endl;if (id1 == id2)cout << "id 相同" << endl;elsecout << "id 不同" << endl;return 0;
}

注意: thread::id 是一个类,不支持初始化或赋值,用于获取线程 id

至于 thread::native_handle_type 代表一个底层线程的本地(native)句柄或标识符,本地句柄通常是由操作系统提供的,用于标识和管理线程的底层资源

在绝大多数情况下,使用 C++ 标准库提供的高级线程抽象是足够的,而无需直接访问线程的本地句柄。直接使用底层线程句柄通常是为了执行与平台相关的线程操作,这可能包括与操作系统相关的调度、优先级、特定的线程控制等。这样的操作通常是为了满足对底层线程管理的特殊需求,而不是一般性的 C++ 线程编程。

总结就是 thread::native_handle_type 一般用不上,现阶段不必关心


接下来看看 构造函数 部分

创建 线程类 对象,支持:

  • 创建一个参数为空的默认线程对象
  • 通过可变参数模板传入回调函数和参数,其中 Fn 表示回调函数对象,Args 是传给回调函数的参数包(可以为空)
  • 移动构造,根据线程对象(右值)来构造线程对象

注意: thread 类不支持 拷贝构造,因为线程对象拥有自己的独立栈等线程资源,所以这里的 拷贝构造 使用 delete 关键字删除了

使用 thread 类需要包含 thread 这个头文件

#include <iostream>
#include <thread>using namespace std;int main()
{// 参数为空的默认线程对象thread t1; // 传入回调函数及参数thread t2([](int x, int y)->void { while(true)cout << "x + y = " << x + y << endl; }, 1, 2);// 只传入回调函数thread t3([]()->void {while(true)cout << "thread running..." << endl; });//t1.join(); // t1 线程状态为空,不能 join 等待t2.join();t3.join();// 无法拷贝构造//thread t4(t3);return 0;
}

线程回调函数不止可以使用 lambda 表达式,还可以传入 函数指针 或者 函数对象

通过调试可以看到 t2t3 线程正在运行中,而 t1 因为没有指定回调函数,所以也就没有完全创建,自然也就没有在运行

其中 1739230925964 分别为 主线程、次线程 t2 和 次线程 t3,而 846026080ntdll.dll 类型的线程,用于为应用程序加载其他动态库,程序运行大概半分钟后,这两个线程就会自动消失,因为当前处于调试状态,并且程序运行时间较短,所以才会看到这个两个系统级线程

注意: 线程如果没有完全创建,是不能 join 等待的,并且线程不支持拷贝操作

同样的,thread 只支持 移动赋值,不支持 传值赋值

部分构造函数后跟的 noexcept 关键字表示当前函数不会抛出 异常,详细知识放到 『异常』 文章中讲解

当线程对象生命周期结束时,会调用 析构函数 销毁对象


thread 类还提供了一批线程相关接口,比如 获取 id、等待、分离、交换

除了 joinableswap,其他功能在 pthread 库中都已经使用过了

  • get_id 对应 pthread_self
  • join 对应 pthread_join
  • detach 对应 pthread_detach

简单使用如下

int main()
{// 创建线程thread t([]()->void { cout << "thread running..." << endl; });// 获取线程 idthread::id id = t.get_id();// 线程剥离// t.detach();cout << "线程 " << id << " 已经创建了" << endl;// 等待线程退出t.join();return 0;
}

注意: 分离线程后,主线程运行结束,整个程序也会随着终止,会导致正在运行中的次线程终止

joinable 是非阻塞版的线程等待函数,等待成功返回 true,否则返回 false

swap 则是将两个线程的资源进行交换(线程回调函数、线程状态等)

注意: swap 并不会交换 thread::id,因为这是线程唯一标识符

至于最后两个函数不常用,这里就不介绍了

这些都是线程常见操作,有了 Linux 多线程编程的基础,学习起来会轻松很多,接下来编写一个成员:创建一批线程,并分别打印十次自己的 id

int main()
{vector<thread> vts(5); // 5 个次线程(未完全创建)for (int i = 0; i < 5; i++){// 移动构造vts[i] = thread([]()->void{for (int i = 0; i < 10; i++){// 如何获取 id ?cout << "我是线程 " << " 我正在运行..." << endl;}});}// 等待线程退出for (auto& t : vts)t.join();return 0;
}

此时面临一个尴尬的问题:如何在回调函数中获取线程 id

  • 线程 id 目前之前通过线程对象调用 get_id 函数获取
  • 传入线程吗?不行,因为此时线程还没有完全创建,线程 id0
  • 传入线程对象?不行,线程还没有完全创建,传入的对象也无法使用,也能通过捕获列表进行引用捕捉,不过同样无法使用

如此一来,想要在 线程回调函数 内获取 线程 id 还不是一件容易的事,好在 C++11 中还提供了一个 this_thread 命名空间,其中提供了获取 线程 id 等函数,可以自由调用

2.1.1.this_thread 命名空间

this_thread 是一个命名空间,其中包含了 获取线程 id、线程休眠、线程时间片 相关函数

有了 this_thread 命名空间之后,就可以轻松获取 线程 id

int main()
{vector<thread> vts(5); // 5 个次线程(未完全创建)for (int i = 0; i < 5; i++){// 移动构造vts[i] = thread([]()->void{for (int i = 0; i < 10; i++){// 获取 idauto id = this_thread::get_id();cout << "我是线程 " << id << " 我正在运行..." << endl;}});}// 等待线程退出for (auto& t : vts)t.join();return 0;
}

可以看到,正常获取到了每个线程的 线程 id

注:这里打印错乱很正常,因为显示器也是临界资源,多线程并发访问时,也是需要加锁保护的

this_thread 只是一个命名空间,是如何做到正确调用 get_id 函数并获取线程 id 的?
this_threadstd 中的一个子命名空间,其中包含了一些与线程有关的操作,比如 get_id,当线程调用 this_thread::get_id 时,实际调用的就是该线程的 thread::get_id,所以才能做到谁调用,就获取谁的线程 id


除此之外,this_thread 命名空间中还提供了 线程休眠 的接口:sleep_untilsleep_for

sleep_util 表示休眠一个 绝对时间,比如线程运行后,休眠至明天 6::00 才接着运行;sleep_for 则是让线程休眠一个 相对时间,比如休眠 3 秒后继续运行,休眠 绝对时间 用的比较少,这里来看看如何休眠 相对时间

相对时间 有很多种:时、分、秒、毫秒、微秒…,这些单位包含于 chrono 类中

比如分别让上面程序中的线程每隔 200 毫秒休眠一次,修改代码如下

int main()
{vector<thread> vts(5); // 5 个次线程(未完全创建)for (int i = 0; i < 5; i++){// 移动构造vts[i] = thread([]()->void{for (int i = 0; i < 10; i++){// 获取 idauto id = this_thread::get_id();cout << "我是线程 " << id << " 我正在运行..." << endl;// 休眠 200 毫秒this_thread::sleep_for(chrono::milliseconds(200));}});}// 等待线程退出for (auto& t : vts)t.join();return 0;
}

也可以让线程休眠其他单位时间


最后在 this_thread 命名空间中还存在一个特殊的函数:yield

这里的 yield 表示 让步、放弃,带入多线程环境中就表示 主动让出当前的时间片

yield 主要用于 无锁编程(尽量减少使用锁),而无锁编程的实现基于 原子操作 CAS,关于原子的详细知识放到后面讲解

原子操作 CAS 是一个不断重复尝试的过程,如果尝试的时间过久,就会影响整体效率,因为此时是在做无用功,而 yield 可以主动让出当前线程的时间片,避免大量重复,把 CPU 资源让出去,从而提高整体效率

2.2.mutex 互斥锁类

多线程编程需要确保 线程安全 问题

首先要明白 线程拥有自己独立的栈结构,但对于全局变量等 临界资源,是直接被多个线程共享的

如果想给线程回调函数传递 左值引用 类型的参数,需要使用 ref 引用包装器函数进行包装传递

比如通过以下代码证明 线程独立栈 的存在

int g_val = 0;void Func(int n)
{cout << "&g_val: " << &g_val << " &n: " << &n << endl << endl;
}int main()
{int n = 10;thread t1(Func, n);thread t2(Func, n);t1.join();t2.join();return 0;
}

可以看到,全局变量 g_val 的地址是一样,而局部变量 n 的地址相差很远,证明这两个局部变量不处于同一个栈区中,而是分别存在线程的 独立栈

如果多个线程同时对同一个 临界资源 进行操作

  • 操作次数较少时,近似原子
  • 操作次数多时,有线程安全问题

这里同时对 g_val 进行 n++ 操作

n = 100 时,结果还算正常(正确结果为 200

int g_val = 0;void Func(int n)
{while (n--)g_val++;
}int main()
{int n = 100;thread t1(Func, n);thread t2(Func, n);t1.join();t2.join();cout << "g_val: " << g_val << endl;return 0;
}

但如果将 n 改为 20000,程序就出问题了(正确结果为 40000

n = 20000;

并且几乎每一次运行结果都不一样,这就是由于 线程安全 问题带来的 不确定性 导致的

关于线程安全的更多知识详见 Linux多线程【线程互斥与同步】


确保 线程安全 的手段之一就是 加锁 保护,C++11 中就有一个 mutex 类,其中包含了 互斥锁 的各种常用操作

比如创建一个 mutex 互斥锁 对象,当然 互斥锁也是不支持拷贝的mutex 互斥锁 类也没有提供移动语义相关的构造函数,因为锁资源一般是不允许被剥夺的


互斥锁 对象的构造很简单,使用也很简单,常用的操作有:加锁、尝试加锁、解锁

  • lock 对应 pthread_mutex_lock
  • try_lock 对应 pthread_mutex_trylock
  • unlock 对应 pthread_mutex_unlock

这些操作使用起来十分简单,对上面的程序进行加锁保护

注:使用 mutex 类需要包含 mutex 这个头文件

int g_val = 0;// 互斥锁对象
mutex mtx;void Func(int n)
{while (n--){mtx.lock();g_val++;mtx.unlock();}
}int main()
{int n = 20000;thread t1(Func, n);thread t2(Func, n);t1.join();t2.join();cout << "g_val: " << g_val << endl;return 0;
}

此时无论数据量有多大,最终的结果都是符合预期的

注意: 这里的两个线程只需要一把锁,并且要保证两个线程看到的是同一把锁

2.2.1.并行与串行的对比

互斥锁 的加锁、解锁位置也是有讲究的,比如只把 g_val++ 这个操作加锁,此时程序就是 并行化 运行,线程 A 与 线程 B 都可以进入循环,但两者需要在循环中竞争 锁资源,只有抢到 锁资源 的线程才能进行 g_val++,两个线程同时竞争,相当于同时进行操作

也可以把整个 while 循环加锁,程序就会变成 串行化,线程 A 或者 线程 B 抢到 锁资源 后,就会不断进行 g_val++,直到循环结束,才会把 锁资源 让出

理论上来说,并行化 要比 串行化 快,实际结果可以通过代码呈现

int main()
{int n = 20000;size_t begin = clock();thread t1(Func, n);thread t2(Func, n);t1.join();t2.join();size_t end = clock();cout << "g_val: " << g_val << endl;cout << "time: " << end - begin << " ms" << endl;return 0;
}

首先来看看在 n = 20000 的情况下,并行化 耗时

注:测试性能需要在 release 模式下进行

耗时 4ms,似乎还挺快,接下来看看 串行化 耗时

串行化 只花了 2ms,比 并行化 还要快

为什么?
因为现在的程序比较简单,while 循环内只需要进行 g_val++ 就行了,并行化中频繁加锁、解锁的开销要远大于串行化单纯的进行 while 循环

如果循环中的操作变得复杂,那么 并行化 是要比 串行化 快的,所以加锁时选择 并行化 还是 串行化,需要结合具体的场景进行判断


这里为了让两个线程看到的是同一把锁,将 mutex 对象定义成了一个 全局对象,其实也可以定义为 局部对象,配合 lambda 表达式 的捕捉列表捕获 mutex 对象

int main()
{int n = 20000;int val = 0;mutex mtx; // 局部锁对象size_t begin = clock();thread t1([&, n]()mutable->void{mtx.lock();while (n--)val++;mtx.unlock();});thread t2([&, n]()mutable->void{mtx.lock();while (n--)val++;mtx.unlock();});t1.join();t2.join();size_t end = clock();cout << "val: " << val << endl;cout << "time: " << end - begin << " ms" << endl;return 0;
}

注意: n 是传值捕捉,如果相对其进行修改,需要使用 mutable 关键字取消常性

2.2.2.其他锁类型

除了最常用的 mutex 互斥锁C++11 中还提供了其他几种版本

recursive_mutex 递归互斥锁,这把锁主要用来 递归加锁 的场景中,可以看作 mutex 互斥锁 的递归升级版,专门用在递归加锁的场景中

比如在下面的代码中,使用普通的 mutex 互斥锁 会导致 死锁问题,最终程序异常终止

// 普通互斥锁
mutex mtx;void func(int n)
{if (n == 0)return;mtx.lock();n--;func(n);mtx.unlock();
}int main()
{int n = 1000;thread t1(func, n);thread t2(func, n);t1.join();t2.join();return 0;
}

为什么会出现 死锁
因为当前在进入递归函数前,申请了锁资源,进入递归函数后(还没有释放锁资源),再次申请锁资源,此时就会出现 锁在我手里,但我还申请不到 的现象,也就是 死锁

解决这个 死锁 问题的关键在于 自己在持有锁资源的情况下,不必再申请,此时就要用到 recursive_mutex 递归互斥锁

// 递归互斥锁
recursive_mutex mtx;

使用 recursive_mutex 递归互斥锁 后,程序正常运行


timed_mutex 时间互斥锁,这把锁中新增了 定时解锁 的功能,可以在程序运行指定时间后,自动解锁(如果还没有解锁的话)

其中的 try_lock_for 是按照 相对时间 进行自动解锁,而 try_lock_until 则是按照 绝对时间 进行自动解锁

比如在下面的程序中,使用 timed_mutex 时间互斥锁,设置为 3 秒后自动解锁,线程获取锁资源后,睡眠 5 秒,即便睡眠时间还没有到,其他线程也可以在 3 秒后获取锁资源,同样进入睡眠

// 时间互斥锁
timed_mutex mtx;void func()
{// 3秒后自动解锁mtx.try_lock_for(chrono::seconds(3));// 睡眠5秒for (int i = 1; i <= 5; i++){this_thread::sleep_for(chrono::seconds(1));cout << "线程 " << this_thread::get_id() << " 已经睡眠了 " << i << " 秒" << endl;}mtx.unlock();
}int main()
{thread t1(func);thread t2(func);t1.join();t2.join();return 0;
}


至于最后一个 recursive_timed_mutex 递归时间互斥锁,就是对 timed_mutex 时间互斥锁 做了 递归 方面的升级,使其在面对 递归 场景时,不会出现 死锁

2.2.3.RAII 风格的锁

手动加锁、解锁可能会面临 死锁 问题,比如在引入 异常处理 后,如果在 临界区 内出现了异常,程序会直接跳转至 catch 中捕获异常,这就导致 锁资源 没有被释放,其他线程申请锁资源时,就会出现 死锁 问题

// 死锁
mutex mtx;void func()
{for (int i = 0; i < 2; i++){try{mtx.lock();if (i % 2 == 0)throw exception("抛出异常");mtx.unlock();}catch (const std::exception& msg){cout << msg.what() << endl;}}
}int main()
{thread t1(func);thread t2(func);t1.join();t2.join();return 0;
}

这里引发 死锁问题 的关键在于 线程在出现异常后,直接跳转至 catch 代码块中,并且没有释放锁资源

解决方法有两个:

  1. catch 代码块中手动释放锁资源(不推荐)
  2. 使用 RAII 风格的锁(推荐)

RAII 风格就是 资源获取就是初始化 ,也就是利用对象出了作用域会自动调用析构函数这个特性,来 自动释放锁资源

编写一个 LockGuard

// RAII 风格
template<class locktype>
class LockGuard
{
public:LockGuard(locktype& mtx):_mtx(mtx){// 加锁_mtx.lock();}~LockGuard(){// 解锁_mtx.unlock();}private:locktype& _mtx;
};

注意:

  1. 需要使用模板,因为互斥锁有多个版本
  2. 成员变量 _mtx 需要使用引用类型,因为所有的锁都不支持拷贝

使用引用类型作为类中的成员变量时,需要在 初始化列表 中进行初始化,以下三种类型需要在初始化列表进行初始化:

  1. 引用类型
  2. const 修饰
  3. 没有默认构造函数的类型

修改之前的代码,不再手动加锁、解锁

void func()
{for (int i = 0; i < 2; i++){try{LockGuard<mutex> lock(mtx);if (i % 2 == 0)throw exception("抛出异常");}catch (const std::exception& msg){cout << msg.what() << endl;}}
}

此时再次运行,可以发现程序正常运行,证明锁资源被自动释放了


其实库中已经提供了 RAII 风格的类了,分别是 lock_guardunique_lock

其中 lock_guard 和我们自己实现的 LockGuard 几乎一样,功能十分简单(构造时加锁,析构时解锁)

unique_lock 在此基础上增加了一些功能,比如 加锁、解锁、赋值、交换 等,因为在某些场景中,需要在临界区内对锁资源进行操作,此时就比较适合使用 unique_lock


在使用 互斥锁 时,推荐使用 lock_guard 或者 unique_lock 进行 自动加锁、解锁,避免 死锁问题

2.3.condition_variable 条件变量类

线程安全 不仅需要 互斥锁,还需要 条件变量条件变量 主要用来同步各线程间的信息(线程同步),同时可以避免 死锁问题,因为如果线程条件不满足,它就会主动将 锁资源 让出,让其他线程先运行

C++11 提供了一个 condition_variable 条件变量类,其中包含了 构造、析构、等待、唤醒 相关接口

条件变量 也是不支持拷贝的,在 wait 等待时,有两种方式:

  1. 传统等待,传入一个 unique_lock 对象
  2. 带仿函数的等待,传入一个 unique_lock 对象,以及一个返回值为 bool 的函数对象,可以根据函数对象的返回值判断是否需要等待

为什么要在条件变量 wait 时传入一个 unique_lock 对象?
因为条件变量本身不是线程安全的,同时在条件变量进入等待状态时,需要有释放锁资源的能力,否则无法将锁资源让出;当条件满足时,条件变量要有申请锁资源的能力,以确保后续操作的线程安全,所以把互斥锁传给条件变量合情合理

注:使用条件变量需要包含 condition_variable 头文件

int main()
{mutex mtx;condition_variable cond;// unique_lock 对象unique_lock<mutex> lock(mtx);// 传统等待cond.wait(lock);// 带函数对象的等待cond.wait(lock, []()->bool { return true; });return 0;
}

注意: 函数对象返回 true 表示条件为真,不需要等待,返回 false 表示需要等待

至于 wait_forwait_until 就是带时间限制的等待,这里不再细谈

notify_one 表示随机唤醒一个正在等待中的线程,notify_all 表示唤醒所有正在等待中的线程,如果唤醒时,没有线程在等待,那就什么都不会发生

条件变量 的使用看似简单,关键在于如何结合具体场景进行设计

2.3.1.交替打印数字

题目要求
给你两个线程 T1T1,要求 T1 打印奇数,T2 打印偶数,数字范围为 [1, 10],两个线程必须交替打印

两个线程交替打印,并且打印的是同一个值,所以需要使用 互斥锁 保护,由于题目要求 T1 打印奇数,T2 打印偶数,可以使用 条件变量 来判断条件是否满足,只有满足才能打印,具体实现代码如下

int main()
{mutex mtx;condition_variable cond;int n = 10;int x = 1; // 从 1 开始// 创建线程thread T1([&, n]()->void {while (x <= n){unique_lock<mutex> lock(mtx);// 避免非法情况if (x == n && n % 2 == 0)break;// 不为奇数就等待while (x % 2 != 1)cond.wait(lock); 直接这样写也是可以的//cond.wait(lock, [&]()->bool { return x % 2 == 1; });cout << "T1: " << x++ << endl;// 唤醒其他线程cond.notify_one();}});thread T2([&, n]()->void{while (x <= n){unique_lock<mutex> lock(mtx);// 避免非法情况if (x == n && n % 2 == 1)break;// 不为偶数,就等待while (x % 2 != 0)cond.wait(lock); 这样写也是可以的//cond.wait(lock, [&]()->bool {return x % 2 == 0; });cout << "T2: " << x++ << endl;// 唤醒其他线程cond.notify_one();}});T1.join();T2.join();return 0;
}

如何确保两个线程交替打印?
某个线程在打印后,条件必定不满足,只能 wait 等待,在这之前会唤醒另一个线程进行打印,因为数字范围全是正数,即只有奇数和偶数两种状态,所以两个线程可以相互配合、相互唤醒,从而达到交替打印的效果

如何确保打印时不会出现非法情况?
判断待打印的数字是否符合范围,如果不符合就不进行打印,直接 break 结束循环,因为这里是 RAII 风格的锁,所以不必担心死锁问题

2.4.atomic 原子操作类

在学习 atomic 原子操作类 之前,需要先看看什么是 原子操作

原子操作 是一种 “可靠” 的操作,只允许存在 成功失败 两种状态,比如对变量的修改,要么修改成功,要么修改失败,不会存在修改一半被切走的状态(被别人影响)

要想实现 原子操作 就得确保硬件支持 CAS(compare and swap)硬件同步原语CAS 简单来说就是 操作前先保存旧值,准备进行操作时,取操作数的值与旧值进行比较,如果相同就进行操作,否则就更新旧值,准备重新操作

结合具体的场景理解,假设现在有一个单链表 list线程A 在进行尾插时,线程B 也进行了尾插,并且插入过程比 线程A 快,此时得益于 CAS线程A 发现需要连接的节点变了,也就不再进行插入,而是更新尾节点信息,重新尾插

也就是说,基于 CAS原子操作 需要确保待操作数没有发生改变,如果被其他线程更改了,就不能进行之前的操作,而是需要更新信息后重新操作

类似的代码实现如下(基于无锁队列实现的链表)

EnQueue(Q, data) //进队列
{//准备新加入的结点数据n = new node();n->value = data;n->next = NULL;do {p = Q->tail; //取链表尾指针的快照} while( CAS(p->next, NULL, n) != TRUE); //while条件注释:如果没有把结点链在尾指针上,再试CAS(Q->tail, p, n); //置尾结点 tail = n;
}

如果只是单纯的进行 i++ 操作,CAS 逻辑可以写成这样

int i = 0;
int old = i; // 保存旧值// 如果 CAS 函数在对 old 和 i 进行比较时,发现两者不相等
// 就会返回 `false`,进入循环更新 `old` 旧值,准备下一次 CAS 判断
// 直到两者相等,才会进行操作,确保整个过程是原子的
while (!CAS(&i, old, old+1))
{old = i;
}// 进行操作
// ...

关于 CAS 的更多详细信息可以看看 陈皓 大佬的这篇文章:《无锁队列的实现》


CAS 操作可以自己手搓,也可以使用库中提供的,比如 C++11 中的 atomic 原子操作类,其中提供了一系列 原子操作,比如 加、减、位运算

借助 atomic 原子操作 类,就可以在不使用锁的情况下,确保整型变量 g_val 的线程安全

注:使用 atomic 原子操作类需要包含 atomic 这个头文件

// 定义为原子变量
atomic<int> g_val = 0;void Func(int n)
{while (n--)g_val++;
}int main()
{int n = 20000;thread t1(Func, n);thread t2(Func, n);t1.join();t2.join();cout << "g_val: " << g_val << endl;return 0;
}

除了整型 int 之外,atomic 还支持定义以下类型为 原子变量


atomic 定义的原子变量类型与普通变量类型并不匹配,比如使用 printf 进行打印时,就无法匹配 %d 这个格式

int main()
{// 定义为原子变量atomic<int> val = 0;printf("%d\n", val);return 0;
}

此时可以借助 atomic 类中的 load 函数,加载该原子类型的普通类型值

此时可以正常匹配

// ...
printf("%d\n", val.load());
// ...

除了 load 之外,还可以使用 store 获取其中的值

// ...
int tmp = 0;
val.store(tmp);
printf("%d\n", tmp);
// ...

线程库中还有一个 future 类,用于 异步编程和数据共享,并不是很常用,这里就不作介绍,使用细节可以看看这篇文章 《C++11中std::future的使用》


3.包装器

包装器 属于 适配器 的一种,正如 栈和队列 可以适配各种符合条件的容器实现一样,包装器 也可以适配各种类型相符的函数对象,有了 包装器 之后,对于相似类型的多个函数的调用会变得十分方便

3.1.function 包装器

现在我们已经学习了多种可调用的函数对象类型

  • 普通函数
  • 仿函数
  • lambda 表达式

假设这三种函数对象类型的返回值、参数均一致,用于实现不同的功能,如何将它们用同一个类型来表示?

// 普通函数
void func(int n)
{cout << "void func(int n): " << n << endl;
}// 仿函数
struct Func
{
public:void operator()(int n){cout << "void operator()(int n): " << n << endl;}
};// lambda 表达式
auto lambda = [](int n)->void{cout << "[](int n)->void: " << n << endl;};

如果 C 语言中的指针学的还可以的话,可以试试使用 函数指针 来表示这三个函数对象的类型

遗憾的是,无法直接使用 函数指针 指向 仿函数对象,也无法指向 类对象

int main()
{void(*pf)(int); // 返回值为 void,参数为 int 的函数指针pf = func;pf(10);//Func f;//pf = f(); // 无法赋值pf = lambda;pf(20);return 0;
}


C++11 中,增加了 function 包装器 这个语法,专门用来包装函数对象,function 包装器 是基于 可变参数模板 实现的,原型如下

template <class Ret, class... Args>
class function<Ret(Args...)>;

其中 Ret 表示函数返回值,Args 是上文中提到的可变参数包,表示传给函数的参数,function 模板类通过 模板特化 指明了包装的函数对象类型

有了 function 包装器 后,可以轻松包装之前的三个函数对象

注:使用 function 包装器需要包含 functional 头文件

int main()
{// 包装器function<void(int)> f;f = func;f(10);f = Func();f(20);f = lambda;f(30);return 0;
}

包装器 可以结合 哈希表 使用,提前准备一批任务,根据用户发出的不同指令来调用不同的任务,比如下面这个程序,完美地在 指令函数 之间建立了映射关系

int main()
{// 包装了返回值为 void,参数为 void 的函数类型unordered_map<string, function<void(void)>> hash;hash["下载请求"] = []()->void { cout << "正在进行下载任务..." << endl; };hash["SQL查询"] = []()->void { cout << "正在进行SQL查询..." << endl; };hash["日志记录"] = []()->void { cout << "正在记录日志信息..." << endl; };string comm; // 指令while (cin >> comm){if (!hash.count(comm))cout << "该指令不存在,请重新输入" << endl;elsehash[comm](); // 调用函数}return 0;
}

根据给出的指令,调用对应的函数

function 包装器 还可以用在刷题中,比如下面这道题目中,就可以使用 包装器运算符具体操作 之间建立映射关系,使用起来十分方便

150. 逆波兰表达式求值

class Solution 
{
public:int evalRPN(vector<string>& tokens) {// 解题思路:操作数入栈,遇到操作符,取两个数计算后,入栈// 建立映射关系unordered_map<string, function<int(int, int)>> hash = {{"+", [](int x, int y)->int { return x + y; } },{"-", [](int x, int y)->int { return x - y; } },{"*", [](int x, int y)->int { return x * y; } },{"/", [](int x, int y)->int { return x / y; } },};stack<int> s;for(auto str : tokens){if(str != "+" && str != "-" && str != "*" && str != "/")s.push(stoi(str));else{// 注意:先获取 y,再获取 xint y = s.top();s.pop();int x = s.top();s.pop();s.push(hash[str](x, y));}}return s.top();}
};

关于这道题的详细题解可以看看这篇文章 《C++题解 | 逆波兰表达式相关》


function 包装器 除了可以包装常规函数对象外,还可用于包装 类内成员函数

包装 静态成员函数 很简单,指明归属于哪个类就行了

class Test
{
public:Test(int n = 0):_n(n){}static void funcA(int val){cout << "static void funcA(int val): " << val << endl;}void funcB(int val){cout << "void funcB(int val): " << val * _n << endl;}private:int _n = 10;
};int main()
{// 包装静态函数function<void(int)> f = Test::funcA;//function<void(int)> f = &Test::funcA; // 这么写也是可以的f(10);return 0;
}

如果包装 非静态成员函数 就有点麻烦了,因为 非静态成员函数 需要借助 对象 或者 对象指针 来进行调用

解决方法是:构建 function 包装器时,指定第一个参数为类,并且包装时需要取地址 &

使用时则需要传入一个 对象,此时传入 匿名对象 或者 普通对象 都行

// 包装非静态函数
function<void(Test, int)> f = &Test::funcB;// 传入匿名对象
f(Test(10), 10);// 传入普通对象
Test t(10);
f(t, 10);

关于包装时的参数设置问题

为什么不能设置为 类的指针,这样能减少对象传递时的开销
因为设置如果设置为指针,后续在进行调用时,就需要传地址,如果是普通对象还好说,可以取到地址,但如果是匿名对象(右值)是无法取地址的,也就无法调用函数了

那能否设置成 类的左值引用 呢?

不行,如果是左值还好,但右值无法被左值引用接收

参数设置为 const 指针 或者 右值引用 又会导致 左值 无法正常传递,所以这里最理想的方案就是单纯设置为 普通类类型,既能接受 左值,也能接受 右值

将参数写成 && 不是会触发引用折叠机制吗,这样不就既能接收左值,也能接收右值了?
不行,引用折叠(万能引用)是指模板推导类型的行为,普通函数是没有这个概念,如果普通函数既想接收左值,又想接收右值,只能重载出两个参数不同的版本了

3.2.bind 绑定

bind 绑定 是一个函数模板,它就像一个函数包装器(适配器),接受一个可调用对象,生成一个新的可调用对象来“适应”原对象的参数列表

bind 绑定 可以修改参数传递时的位置以及参数个数,生成一个可调用对象,实际调用时根据 修改 规则进行实际的函数调用,具体原型如下

template <class Fn, class... Args>
bind (Fn&& fn, Args&&... args);

fn 是传递的 函数对象args 是传给函数的 可变参数包,这里使用了 万能引用(引用折叠),使其在进行模板类型推导时,既能引用左值,也能引用右值


使用 bind绑定 改变参数传递顺序

注:placeholders 是一个命名空间,其中的 _1_2_N 称为占位符,分别表示函数中的第1、第2、第N个参数,直接使用就行了

void Func(int a, int b)
{cout << "void Func(int a, int b): " << a << " " << b << endl;
}int main()
{// 正常调用Func(10, 20);// 绑定生成一个可调用对象auto RFunc = bind(Func, placeholders::_2, placeholders::_1);RFunc(10, 20);return 0;
}

经过 bind 绑定 后,同样的参数传递,出现了不同的调用结果

bind 的底层也是仿函数,生成一个对应的类,根据用户指定的规则,去调用函数,比如这里经过绑定后,实际调用时,RFunc 中实际在调用 Func 传递的参数为 20 10

除了使用 auto 自动推导 bind 生成的可调用对象类型外,还可以使用 包装器 来包装出类型

// 使用包装器包装出类型
function<void(int, int)> RFunc = bind(Func, placeholders::_2, placeholders::_1);

bind 绑定 改变参数传递顺序很少使用,只需要简单了解即可

注意: 在使用 bind 绑定改变参数传递顺序时,参与交换的参数类型,至少需要支持隐式类型转换,否则是无法交换传递的


bind 绑定 还可以用来指定参数个数,比如对上面的函数 Func 进行绑定,将参数 1 始终绑定为 100,后续进行调用时,只需要传递一个参数

int main()
{// 使用包装器包装出类型auto RFunc = bind(Func, 100, placeholders::_1);RFunc(20);RFunc(10, 20);return 0;}

此时如果坚持传递参数,会优先使用绑定的参数,再从函数参数列表中,从左到右选择参数进行传递,直到参数数量符合,比如这里第二次调用虽然传递了 1020,但实际调用 Func 时,RFunc 会先传递之前绑定的值 100 作为参数1传递,而 10 会作为参数2传递,至于 20 会被丢弃


注意: 无论绑定的是哪一个参数,占位符始终都是从 _1 开始,并且连续设置

绑定普通参数显得没意思,bind 绑定 参数个数用在 类的成员函数 上才舒服,比如对之前 function 包装器 包装 类的成员函数 代码进行优化,直接把 类对象 这个参数绑定,调用时就不需要手动传递 对象

class Test
{
public:Test(int n = 0):_n(n){}static void funcA(int val){cout << "static void funcA(int val): " << val << endl;}void funcB(int val){cout << "void funcB(int val): " << val * _n << endl;}void funcC(){}private:int _n = 10;
};int main()
{function<void(int)> RFuncB = bind(&Test::funcB, Test(10), placeholders::_1);RFuncB(10);return 0;
}

除了可以绑定类对象外,也可以直接绑定 val 这个参数,亦或是两者都绑定

// 绑定对象
function<void(Test, int)> f1 = bind(&Test::funcB, placeholders::_1, 10);
f1(Test(), 0);// 两者都绑定
function<void(int)> f2 = bind(&Test::funcB, Test(10), 20);
f2(0);

注意: 虽然参数已经绑定了,但实际调用时,仍然需要传递对应函数的参数,否则无法进行函数匹配调用,当然实际传入的参数是绑定的值,这里传参只是为了进行匹配;并且如果不对类对象进行绑定,需要更改包装器中的类型,调用时也需要传入参数进行匹配


🌆总结

在这C++11系列的收尾文章中,我们深入研究了lambda表达式,为函数对象提供了快速构建的方法。接着,我们学习了标准线程库,包括线程、互斥锁、条件变量等,为跨平台的多线程编程提供了强大工具。最后,通过包装器和绑定工具,我们获得了统一函数对象类型的新手段,使得代码更灵活、可读性更强,为现代C++编程提供了丰富的工具和技巧


星辰大海

相关文章推荐

C++ 进阶知识

C++11『右值引用与移动语义』

C++11『基础新特性』

C++ 哈希的应用【布隆过滤器】

C++ 哈希的应用【位图】

C++【哈希表的完善及封装】

C++【哈希表的模拟实现】

C++【初识哈希】

C++【一棵红黑树封装 set 和 map】

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/202551.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

快速在WIN11中本地部署chatGLM3

具体请看智谱仓库github&#xff1a;GitHub - THUDM/ChatGLM3: ChatGLM3 series: Open Bilingual Chat LLMs | 开源双语对话语言模型 或者Huggingface:https://huggingface.co/THUDM/chatglm3-6b 1. 利用Anaconda建立一个虚拟环境&#xff1a; conda create -n chatglm3 pyt…

Redis打包事务,分批提交

一、需求背景 接手一个老项目&#xff0c;在项目启动的时候&#xff0c;需要将xxx省整个省的所有区域数据数据、以及系统字典配置逐条保存在Redis缓存里面&#xff0c;这样查询的时候会更快; 区域数据字典数据一共大概20000多条,&#xff0c;前同事直接使用 list.forEach…

分布式链路追踪入门篇-基础原理与快速应用

为什么需要链路追踪&#xff1f; 我们程序员在日常工作中&#xff0c;最常做事情之一就是修bug了。如果程序只是运行在单机上&#xff0c;我们最常用的方式就是在程序上打日志&#xff0c;然后程序运行的过程中将日志输出到文件上&#xff0c;然后我们根据日志去推断程序是哪一…

TCL脚本语言光速入门教程,一篇就够了(超全查表)

目录 引子&#xff1a;初见TCL 基本命令 置换命令 普通置换 变量置换 命令置换 反斜杠置换 其他置换 脚步命令 eval命令 source命令 语言命令 简单变量 数组变量 重构变量及其操作 补充概念 全局变量和局部变量 小结 最近突然遇到了要用TCL脚本语言操作的需求…

C/C++小写字母的判断 2022年3月电子学会中小学生软件编程(C/C++)等级考试一级真题答案解析

目录 C/C小写字母的判断 一、题目要求 1、编程实现 2、输入输出 二、算法分析 三、程序编写 四、程序说明 五、运行结果 六、考点分析 C/C小写字母的判断 2022年3月 C/C编程等级考试一级编程题 一、题目要求 1、编程实现 输入一个字符&#xff0c;判断是否是英文小…

Qt/QML编程学习之心得:一个Qt工程的学习笔记(九)

1、.pro文件 加CONFIG += c++11,才可以使用Lamda表达式(一般用于connect的内嵌槽函数) 2、QWidget 这是Qt新增加的一个类,基类,窗口类,QMainWindow和QDialog都继承与它。 3、Main函数 QApplication a应用程序对象,有且仅有一个 a.exec() 进行消息循环、阻塞 MyWi…

设计模式-解析器-笔记

“领域规则”模式 在特定领域中&#xff0c;某些变化虽然频繁&#xff0c;但可以抽象为某种规则。这时候&#xff0c;结合特定领域&#xff0c;将稳日抽象为语法规则&#xff0c;从而给出在该领域下的一般性解决方案。 典型模式&#xff1a;Interpreter 动机(Motivation) 在…

【Axure教程】用中继器制作卡片多条件搜索效果

卡片设计通过提供清晰的信息结构、可视化吸引力、易扩展性和强大的交互性&#xff0c;为用户界面设计带来了许多优势&#xff0c;使得用户能够更轻松地浏览、理解和互动。 那今天就教大家如何用中继器制作卡片的模板&#xff0c;以及完成多条件搜索的效果&#xff0c;我们会以…

云原生入门系列(背景和驱动力)

做任何一件事&#xff0c;或者学习、应用一个领域的技术&#xff0c;莫过于先要想好阶段的目标和理解、学习它的意义是什么&#xff1f;解决了什么问题&#xff1f; 这部分&#xff0c;就尝试来探讨下这个阶段需要理解并达成的目标以及践行云原生的意义在哪里。 1.历程 任何阶…

【开源】基于Vue.js的衣物搭配系统的设计和实现

项目编号&#xff1a; S 016 &#xff0c;文末获取源码。 \color{red}{项目编号&#xff1a;S016&#xff0c;文末获取源码。} 项目编号&#xff1a;S016&#xff0c;文末获取源码。 目录 一、摘要1.1 项目介绍1.2 项目录屏 二、研究内容2.1 衣物档案模块2.2 衣物搭配模块2.3 衣…

解锁潜力:创建支持Actions接口调用的高级GPTs

如何创建带有Actions接口调用的GPTs 在本篇博客中&#xff0c;我们将介绍如何创建一个带有Actions接口调用的GPTs &#xff0c;以及如何进行配置和使用。我们将以 https://chat.openai.com/g/g-GMrQhe7ka-gptssearch 为例&#xff0c;演示整个过程。 Ps: 数据来源&#xff1a…

全网最全c++中的system详解

这篇文章是二发&#xff0c;做了些微调&#xff0c;感兴趣的朋友可以看原文&#xff1a;C中的system_一只32汪的博客-CSDN博客 1&#xff0c;简介 system()函数是在C制作中十分常用&#xff0c;有用的一个函数。 其效果类似于系统中"cmd"控制台和"bat"文件…

【nlp】2.8 注意力机制拓展

注意力机制拓展 1 注意力机制原理1.1 注意力机制示意图1.2 Attention计算过程1.3 Attention计算逻辑1.4 有无attention模型对比1.4.1 无attention机制的模型1.4.2 有attention机制的模型1 注意力机制原理 1.1 注意力机制示意图 Attention机制的工作原理并不复杂,我们可以用下…

使用持久卷部署 WordPress 和 MySQL

&#x1f5d3;️实验环境 OS名称Microsoft Windows 11 家庭中文版系统类型x64-based PCDocker版本Docker version 24.0.6, build ed223bcminikube版本v1.32.0 &#x1f587;️创建 kustomization.yaml 你可以通过 kustomization.yaml 中的生成器创建一个 Secret存储密码或密…

DBeaver安装与使用教程(超详细安装与使用教程),好用免费的数据库管理工具

&#x1f3c6;好的学习、工作从选对一个对于自己好用的软件开始。 点击目录跳转至相应目录的内容&#xff0c;更方便观看 &#x1f3c6;目录 &#x1f3c6;一、DBeaver介绍1.它支持任何具有一个JDBC驱动程序数据库&#xff0c;也可以处理任何的外部数据源。2.跨平台使用、支持…

python-opencv划痕检测-续

python-opencv划痕检测-续 这次划痕检测&#xff0c;是上一次划痕检测的续集。 处理的图像如下&#xff1a; 这次划痕检测&#xff0c;我们经过如下几步: 第一步&#xff1a;读取灰度图像 第二步&#xff1a;进行均值滤波 第三步&#xff1a;进行图像差分 第四步&#xff1…

java的包装类

目录 1. 包装类 1.1 基本数据类型和对应的包装类 1.2 装箱和拆箱 1.3 自动装箱和自动拆箱 1. 包装类 在Java中&#xff0c;由于基本类型不是继承自Object&#xff0c;为了在泛型代码中可以支持基本类型&#xff0c;Java给每个基本类型都对应了 一个包装类型。 若想了解…

抽象工厂设计模式是什么?什么是 Abstract Factory 抽象工厂设计模式?Python 抽象工厂设计模式示例代码

什么是 Abstract Factory 抽象工厂设计模式&#xff1f; 抽象工厂设计模式是一种创建型设计模式&#xff0c;旨在提供一个创建一系列相关或相互依赖对象的接口&#xff0c;而无需指定其具体类。它允许客户端使用抽象的接口创建一组相关对象&#xff0c;而无需关注实际的对象实…

[超详细]基于YOLO&OpenCV的人流量统计监测系统(源码&部署教程)

1.图片识别 2.视频识别 [YOLOv7]基于YOLO&#xff06;Deepsort的人流量统计系统(源码&#xff06;部署教程)_哔哩哔哩_bilibili 3.Deepsort目标追踪 &#xff08;1&#xff09;获取原始视频帧 &#xff08;2&#xff09;利用目标检测器对视频帧中的目标进行检测 &#xff08…

学习量化交易如何入门?

Python 量化入门很简单&#xff0c;只需 3 步就能快速上手! 题主在程序方向没有相关经验&#xff0c;今天就从量化行业的通用语言-Python 着手&#xff0c;教大家如何快速入门。 一、准备工作 在开始 Python 编程之前&#xff0c;首先需要确保你的计算机上安装了合适的 Pytho…