C++11
C++$11$ 是 C++ 语言的一个重大更新标准,它于 $2011$ 年发布,因此被命名。这个标准为 C++ 语言引入了许多新的语言特性和库改进,使得程序设计更加简洁、高效,并且更易于维护。
捕获异常
异常处理是一种处理程序运行过程中出现的错误或异常情况的机制,它使得程序能够在发生错误时采取适当的措施,而不是直接崩溃。
- 异常是通过
throw
关键字抛出的,通常用于在代码中某个地方检测到错误时,将异常抛出到调用者
throw SomeException(); // 抛出一个类型为 SomeException 的异常
- 抛出的异常类型可以是任何类型的对象,通常是继承自
std::exception
的类型,也可以是自定义类型。 - 捕获异常,
throw
可以抛出任意类型的异常,使用catch
捕捉到异常后执行不同的处理逻辑
void f() {
try {
// 可能会抛出异常的代码
} catch (ExceptionType1 &e1) {
// 捕获并处理异常类型 ExceptionType1
} catch (ExceptionType2 &e2) {
// 捕获并处理异常类型 ExceptionType2
} catch (...) {
// 捕获所有其他类型的异常
}
}
- 当捕获到异常后,不会继续向下执行,而是回到
f()
再寻找catch()
匹配的异常参数,执行其中的语句 - 可以限定函数抛出异常的类型
void f() throw(int, double ...) {} 限定抛出括号内的类型异常
void f() throw(){} 不允许抛出异常
void f() noexcept {} C++11不允许抛出异常
noexcept()
还可以传入参数,参数是一个常量表达式,true
或者false
,不抛异常或者抛异常
$volatile$ 修饰
$volatile$ 关键字修饰的变量,告诉编译器这个变量是一个经常变化的值,编译时不能做优化处理,一般用于多线程
volatile
是一个告诉编译器不要优化变量的关键字。它保证每次访问变量时,编译器都会从内存中读取最新的值。volatile
并不保证线程安全,也不提供原子性操作或内存顺序控制。如果需要多线程同步,使用std::atomic
或其他同步机制
$auto$ 关键字
用于自动类型推导,声明的变量必须初始化
- 使用
auto
声明的变量必须要进行初始化,以让编译器推导出它的实际类型,在 编译时 将auto
占位符替换为真正的类型。 - 当
auto
变量 不是指针或者引用类型时,推导的结果 不会 保留const
和volatile
属性 - 当
auto
变量 是指针或者引用类型时,推导的结果 会 保留const
和volatile
属性 - 不能使用
auto
的场景- 作为函数参数时。因为函数只有在被调用的时候形参才会被初始化。
- 类的非静态成员变量初始化。因为这个变量是属于对象不是属于类的,该成员变量只有对象被创建出来的时候才会初始化。
- 类的非常量静态成员变量。静态变量不允许在类内初始化
class A {
auto x = 0; // error
static auto y; // error 静态成员要在类外初始化
static const auto z = 0; // ok, 常量必须声明的时候初始化
};
$decltype(expr)$
也是自动类型推导,与 $auto$ 的区别是声明变量的时候不需要一定初始化,会根据 $exptr$ 来推导
decltype
的推导是在编译期完成的,它只是用于表达式类型的推导,并不会计算表达式的结果- $expr$ 是 普通变量 或 普通表达式 或者 类表达式,在这种情况下,使用
decltype
推导出来的类型和表达式的类型是一致的 - $expr$ 里面是 函数调用,使用
decltype
推导出的类型和函数返回值一致- 函数返回的,只有类类型可以携带
const
和volatitle
限定符,除此之外忽略掉这两个限定符
- 函数返回的,只有类类型可以携带
- $expr$ 是表达式,表达式的结果是一个
左值
,或者被()
包围,使用decltype
推导出的是表达式类型的引用 decltype
在泛型编程中的应用,因为在泛型编程有大量的未定义类型c++11
返回值类型后置(追踪返回类型)
auto func(参数1,参数2...) -> decltype(expr)`
auto 会推导成 decltype(epxr) 推导出的类型
$nullptr$
C++
是强类型语言,不允许不同指针类型之间隐氏转换。C
语言的空指针类型是NULL = void * (0)
,在C++
中,int *p = (void *)0
是不允许的- 所以在
C++
中NULL
的值是整形0
,而这又会带来歧义,所以C++11
专门推出了一个空指针类型nullptr
$lambda$ 表达式
lambda 表达式(或称匿名函数),在函数内部定义和使用临时函数变得更加简洁和高效。$Lambda$ 表达式可以让你在不定义单独函数的情况下,直接在代码中创建并使用函数对象
- 语法
[capture](params) opt -> ret {
body;
};
[] 为空,表示不捕捉任何外部变量
[&] 表示用引用的方式捕获外部变量
[=] 表示用值拷贝的方式捕获外部变量
[=, &foo] 按值拷贝方式捕获外部变量,并按照引用捕获外部变量foo
[bar] 按值捕获捕获外部变量 bar,同时不捕获其他外部变量
[&bar] 按引用捕获捕获外部变量 bar,同时不捕获其他外部变量
[this] 捕获当前类中的 this指针,在匿名中只能使用this指针所指向对象的成员
opt
mutable 在匿名函数内部就可以修改拷贝进来的值
exception 指定函数抛出的异常,如抛出整数类型的异常,可以使用 throw()
- $lambda$ 表达式的本质
- $lambda$ 表达式的类型在
C++11
中会被看成带operator()
的类,即仿函数 - 按照 $C++$ 标准,$lambda$ 表达式的 $operator()$ 是默认 $const$ 的,一个 $const$ 成员函数是无法修改成员变量的值的。
mutable
选项的作用就在于取消const
属性
- $lambda$ 表达式的类型在
$constexpr$ 修饰
用于指示一个表达式或函数在编译时能够求值。它告诉编译器,这个函数或变量可以在编译期进行求值,从而提高程序的效率,特别是对于常量计算和复杂的编译时逻辑。
- 编译器期进行语法分析,语义分析,优化,代码生成,链接。并不会计算结果,但被
constexpr
修饰后,就可以在编译器被计算,前提是基于常量的 - 常量表达式 常量或者用常量计算的表达式
const
修饰常量。修饰变量只读。左值引用的是一个临时变量是不合法的,加上const
变成常量左值就可以了- 常量表达式在编译阶段就确定值,运行时不会再计算,提高效率
constexpr
修饰常量
struct T {int x;};
constexpr int x = 1;
constexpr int y = x + 1;
constexpr T t{y + 1};
constexpr
修饰函数 $-$ 常量表达式函数 类的构造函数- 修饰普通函数/类成员函数 函数必须有返回值,并且返回的必须是常量表达式
- 并且不能出现非常量表达式之外的语句,逻辑运算。(
using
指令之类除外) - $c++11$ 会对$constexpr$ 有诸多限制,会在 $c++14$ 以后逐渐放开
constexpr int f() {
constexpr int x = 1;
return x + 1;
}
- 修饰模板函数 如果传进来的参数
t
是常量表达式,则constexpr
生效,否则失效
template<class T>
constexpr T display(T t) {
return t;
}
- 修饰构造函数 构造函数的函数体必须为空,并且必须采用初始化列表的方式为各个成员赋值
struct A {
constexpr A() : x(10) {}
int x;
};
$11$ 中的 $using$
C++11 引入了 $using$ 关键字,扩展了其在语言中的应用,允许更简洁和灵活的代码。$using$ 关键字主要有以下几种用途:类型别名、简化命名空间使用、继承构造函数、别名模板
- 类型别名
using i64 = long long;
using func = int(*)(int, int);
- 给模板取别名
template<class T>
using vec = vector<T>;
- 委托构造函数 在一个构造函数中调用其他的构造函数
class A {
public:
A() : A(10) {}
A(int x) {
cout << x << "\n";
}
}a;
- 继承构造函数 在子类中使用父类的构造函数
class Base {
public:
Base() {}
Base(int value) {x = value;}
int x;
};
class Son : public Base {
public:
using Base::Base; // 继承父类的所有构造函数
};
- 在
C++
中,当子类有了父类的同名函数(非虚函数),父类的该同名函数(包括重载)都会被隐藏,可以使用using
显示的指出
$enable\_if$
一种条件编译机制,通常用于启用或禁用某些模板函数或类的特定实例化。它通过静态断言来判断模板参数是否满足特定条件,从而决定是否启用对应的代码
- 例如,仅当某个类型满足特定条件时才启用某个函数模板。
- 模板原型
namespace std {
template<bool B, typename T = void>
struct enable_if {};
template<typename T>
struct enable_if<true, T> {
using type = T;
};
}
- 它的基本原理是:当模板条件
B
为true
时,std::enable_if<true, T>
会定义一个成员类型type
,类型为T
。当模板条件B
为false
时,std::enable_if<false, T>
不会定义任何成员类型,因此不能被使用。 - 通常,
std::enable_if
被用于模板函数和类的重载,通过SFINAE
技术选择合适的模板版本。它的用法一般是这样:
template<typename T>
typename std::enable_if<条件, 返回类型>::type 函数名();
- 使用
template<class T>
typename std::enable_if<std::is_integral<T>::value, void>::type // 如果是int类型,type类型是void
print(T x) {
std::cout << x << "\n";
}
template<class T>
typename std::enable_if<std::is_floating_point<T>::value, float>::type
print(T x) {
std::cout << x << "\n";
return x;
}
int main() {
print(123);
std::cout << print(12.3);
return 0;
}
$tuple$
std::tuple
是一个容器类模板,允许你存储不同类型的元素,并通过统一的接口来访问这些元素。与传统的std::pair
只存储两个元素不同,std::tuple
可以存储任意数量的元素,且每个元素可以是不同类型。std::tuple
是C++11
标准库中非常强大的工具,通常用于实现多种类型的聚合数据结构
- 它的大小和元素类型在编译时就已经确定,不可动态更改。
- 用法
std::tuple<int, char, std::string> t(1, '2', "345");
std::get<>
用于访问tuple
中指定索引位置的元素。可以通过索引(编译时确定)或者类型(编译时确定)来访问元素
std::get<0>(t);
tuple_size<T>::value
是一个模板结构体,用于获取tuple
的大小(即元素的个数)。它的value
成员表示tuple
中元素的数量
std::tuple_size<decltype(t)>::value
std::tuple_element<T, N>::type
允许访问tuple
中某个索引位置的元素类型
using type = std::tuple_element<0, decltype(t)>::type;
std::apply
是一个将tuple
中的元素展开并传递给可调用对象(如函数、lambda
表达式等)的函数。
auto f = [](auto x, auto &y, auto &z) { // 14才支持泛型lambda
std::cout << x << ' ' << y << ' ' << z << "\n";
};
std::apply(f, t);
$std::initializer\_list$
一个轻量级的类模板,用来包装初始化列表。它可以看作是一个指向
const T
类型元素的数组指针,但其内部元素的数量是固定的,而且元素本身是常量的(即元素是只读的)
- 初始化容器或数组
- 常常用于函数参数,可以接受不同数量的相同类型的值
void solve(std::initializer_list<int> list) {}
- 使用
initializer_list
可以通过{}
初始化一组元素,使得代码更加简洁和易读 - 本质上是一个只读数组,使用它时并不会对内存进行复制(它的元素是常量的),因此是轻量级的
- 三个成员接口
size(), begin(), end()
可调用对象包装器,绑定器
可调用对象包装器和绑定器是 C++ 中非常重要的概念,它们涉及到函数指针、函数对象、以及如何将函数与参数绑定,从而实现更灵活的函数调用机制。这些概念在现代 C++ 中通过
std::function
、std::bind
、以及lambda
表达式等特性得到了广泛应用。
- 可调用对象:函数指针、仿函数、可被转换成函数指针的类对象、类的成员指针
- 函数指针
void f(int) {}
using func = void(*)(int);
func ptr = f; // ptr可调用对象
- 一个具有
operator()
成员函数的类对象(仿函数)
class A {
public:
void operator() {}
} a; // a可调用对象
- 一个可被转换成函数指针的类对象
class A {
public:
/*
将类对象转换成funcptr类型的函数指针
类对象被转换成funcptr类型指针的时候就执行了hello函数
只能执行静态函数。
*/
operator funcptr() {
return hello;
}
static void hello(int) {
cout << "hello\n";
}
}a; // a可调用对象
int main() {
a(1);
return 0;
}
- 类的成员指针
using funcptr = void(*)(int);
class A {
public:
static void hello(int) {}
void world(int) {}
int x;
};
int main() {
/* 类的函数指针
funcptr ptr1 = A::world; // error
属于类的函数指针类型是 void (*) (int)
属于对象的函数指针类型是 void (A::*)(int)
非静态成员是属于对象的,与普通函数指针类型不一致
*/
funcptr ptr2 = A::hello; // ok
// 非静态成员函数是属于对象的,不能用普通函数指针
// 要指向非静态成员函数需要使用成员函数指针
using nfuncptr = void(A::*)(int);
nfuncptr ptr3 = A::world; // ok
// 类的成员指针
using f = int A::*;
f p = &A::x; // p 并没有指向对象实例,而是指向 A 类中的 x 成员变量相对于类的偏移量
A a;
(a.*ptr3)(3);
a.*p = 3; // 成员指针 p 与对象 a 一起,计算出成员 x 在 a 对象中的具体位置。
cout << a.x << "\n";
}
- 可调用对象包装器
function<返回值类型(参数类型列表)> diy_name = 可调用对象
- 可调用对象绑定器 对可调用对象绑定参数,绑定后的结果可以使用
function
进行保存,并延长调用到我们任何需要的时候,通俗来讲,主要作用- 将可调用对象和参数绑定成一个仿函数
- 将多元(参数为
n
,n > 1
) 可调用对象转化为一元或者(n - 1)
元可调用对象,即绑定部分参数 - $bind$(可调用对象地址,绑定的参数/占位符)
- $bind$(类函数/成员地址,类实例对象地址,绑定的参数/占位符)
- 绑定实例对象和实例对象地址有区别,一个相当于值,一个相当于引用
- 占位符:
placeholders::_1
是一个占位符,代表这个位置在函数调用时被传入的 第一个 参数所代替,还有placeholders_2
,3...
void add(int x, int y) {
cout << x + y << "\n";
}
int main() {
// 使用绑定器绑定可调用对象,并调用得到的仿函数
bind(add, 1, 2)();
bind(add, placeholders::_1, 2)(1); // 3
bind(add, 1, placeholders::_1)(2); // 3
bind(add, 1, placeholders::_2)(10, 2); // 3 以绑定的为准,形参就没用了
return 0;
}
- 打包成员函数
class Add {
public:
void add(int x, int y) {
cout << x + y << "\n";
}
int x;
};
int main() {
Add a;
// 成员函数绑定
auto f = bind(Add::add, &a, 520, placeholders::_1);
function<void(int, int)> func1 = f;
f(1);
// 成员变量绑定
auto ff = bind(&Add::x, &a);
function<int&(void)> func2 = ff;
ff() = 666;
cout << ff() << "\n";
return 0;
}
右值引用
右值引用是 C++11 引入的一个重要概念,它通过引入
&&
语法来区分右值和左值。右值引用为移动语义和完美转发提供了支持,从而使得 C++ 的性能和灵活性得到了显著提升
- 左值存储在内存中,有明确存储地址(可取地址)的数据
- 右值是可以提供数值的数据(不可取地址)
- 纯右值:字面量
- 将亡值:与右值引用相关的表达式,比如
T &&
类型的函数返回值,std::move()
的返回值等
- 右值引用的作用:
- 延长即将被释放的变量的生命周期
- 转移资源
T t = X
或者T && t = x
编译器会判断X
的是不是临时变量,若是临时变量则调用移动构造函数转移资源到t
。没有移动构造函数则调用拷贝构造。- 如没有移动构造,则要求临时变量不能取地址。此时右值引用只会临时对象的延长生命周期,不会调用移动构造函数
struct A {
A(){}
A(A && other) noexcept { std::cout << "move\n";}
}a, b = std::move(a);
a 被move转换成右值引用,用于初始化 b,调用移动移动构造进行资源转移。
&&
的特性 (引用折叠)auto &&
或者T &&
是未定义引用类型,需要推导。const T &&
不需要推导,直接就是一个右值引用- 传入
右值
推导T &&
或者auto &&
是一个右值引用
类型 - 传入非右值右值引用(没有使用完美转发),左值,左值引用,常量右值引用常量,左值引用
推导
T &&或者
auto &&得到是一个
左值引用类型,有
const加上
const` - 引用折叠是在
T && t
接收参数时发生的,不是传入参数的发生的。
- 转移和完美转发
std::move()
将左值转为右值引用std::forward<T>(t)
完美转发,防止变量的类型在进行函数传递时发生改变。
template<class T>
void f(T && t) {
if constexpr (std::is_same<T, int>::value) {
std::cout << "int&&\n";
} else if constexpr (std::is_same<T, int &>::value) {
std::cout << "int&\n";
} else {
}
}
推导规则:传入右值,例如 5, T 会被推导成int类型,变成 int && t。此时是右值引用类型
传入左值,例如 x, T 会被推导成int &类型,变成 int & &&,引用折叠后变成 int &类型,左值引用。
int && x = 5;
f(x);
传递的右值引用,按引用折叠规则,推导出来应该是右值引用,为什么结果确是左值引用呢?
传递的是右值引用,但是此时有名字的右值引用经过传递会被看成左值引用。推导出来所以成了左值引用,
使用完美转发可以让传递时参数类型不发生变化,推导出来保持原来的类型。
f(forward<decltype(x)>(x));
- 右值引用并不会 自动调用移动构造函数,它只会 绑定临时对象。
- 移动构造函数只有在 显式使用
std::move
或者 进行移动操作(如将对象传递给容器,返回值优化等)时才会调用。 - 右值引用 的作用是为 移动语义 提供支持,允许在适当的时候移动资源,而不是每次绑定时就执行移动操作
引用折叠
- 类型转换时有一个是左值引用,推导结果就是左值引用,否则(两个都是右值引用)结果为右值引用
- 引用折叠发生在当你进行类型转换
(如 static_cast)
时,多个引用类型结合时会根据规则折叠成合适的类型。static_cast<T&&>
是对引用类型进行转换时,可能会发生引用折叠。。
template<class T>
void Forward(T && t) { // 这里也会折叠
static_cast<T &&>(t); // 现在会根据T推导出来的类型与 &&(右值引用) 发生引用折叠
}
万能引用的消失
- 这是一个万能引用
template<class T>
void f(T && t) {}
- 这不是一个万能引用,而是一个右值引用
template<class T>
struct F {
F(T && t) {}
}
- 模板参数
T
是从传递给类模板实例化的参数类型推导出来的。这种推导是在类模板的构造函数调用时发生的 - 顶层的
T
类型已经被推导出来了,不再是万能引用。而是一个实际的类型。例如int
。T &&
->int &&
- 如果你希望
F
的构造函数可以同时接受左值和右值,则需要让T &&
成为万能引用。万能引用的关键条件是模板参数T
必须是未固定的(待推导的)
template<class T>
struct F {
template<class U>
F(U && t) {}
}
参数包
参数包可以将多个参数打包到一起, 可以用来传递多个不确定的数量的参数。
- 模板 $<>$ 中的参数分为 类型 参数和 数值 参数。
- 类型参数包
// calss T, class U, ... 等多个模板的类型参数打包到一起成为Args, 但是class Args只是一个模板类型参数,多个参数加上...
template<class...Args>
void foo(Args...args) {} // 类型 变量名
// f<int, double, char>(1, 2.3, '4');
// 可以根据形参来推导模板参数的类型,省略模板参数
f(1, 2.3, '4')
- 数值参数包
template<int...args> // 模板的参数都是已经确定了类型的数值,但具体参数个数不确定
void foo() {}
f<1, 2, 3, 4>();
可变参数模板
void foo() {}
template<class T, class...Arg>
void foo(T t, Arg...args) {
cout << t << " ";
foo(args...);
}
- 递归调用,每次递归会将参数分解为
t
和args...
,直到最后参数args...
为空,但模板foo
必然有一个参数t
,因此需要有一个空的同名函数来作为递归出口foo(){}
- 传入多少参数,就会实例化多少模板函数,递归调用
- 在
C++17
中可以直接使用折叠表达式输出
处理日期和时间的 $chrono$ 库
duration
类 表示一段时间间隔,用来记录时间长度,可以表示几秒,几分钟,几个小时的时间间隔
分子 分母
template<std::intmax_t Num, std::intmax_t Denom = 1>
classs ration;
时间周期次数,时间周期(s)
template<class Rep, class Period = std::ratio<1>>
class duration;
- 小时,分钟,秒,毫秒
// 1hours
std::chrono::duration<int, std::ratio<3600>> hour(1);
std::chrono::hours h(1);
// 1min
std::chrono::duration<int, std::ratio<60>> min(1);
std::chrono::minutes m(1);
// 1sec
std::chrono::duration<int> sec(1);
std::chrono::seconds se(1);
// 1msec
std::chrono::duration<int, std::ratio<1, 1000>> msec(1);
std::chrono::millisecond mse(1);
$this\_thread$ 命令空间
get_id
获得当前线程的 $id$sleep_for
阻塞线程 多长时间
template<class Rep, class Period>
void sleep_for(std::chrono::duration<Rep, Period> &rel_time);
sleep_until
阻塞线程 到某个时间点
template<class Clock, class Duration>
void sleep_until(const std::chrono::time_point<Clock, Duration> &abs_time);
yield()
线程调用了yidld()
方法后会主动放弃cpu
资源,线程立马变为就绪态,再争取cpu
时间片
$C++$ 线程库
std::thread
线程thread() noexcept
创建一个空线程thread(thread && other) noexcept
移动构造- 有参构造
template<class Function, class...Args>
explicit thread(Function && f, Args && ...args); 万能引用类型
注意:传递给thread后,thread会将参数转发成引用类型,然后内部会将转发后的参数拷贝一份再传给线程函数。所以是值传递。
如果不想被拷贝,需要传递引用,需要使用std::ref()
- 删除拷贝构造
thread(const thread &) = delete
join()
阻塞主线程,直至调用join
的thread
完成任务才解除阻塞- 注意:线程的执行速度和中断打印的速度不对称,线程在cpu中执行,cpu的速度非常快,打印在中断是$io$操作,速度受限,输出的效率低,会导致输出的数据混乱/
detach()
在线程分离之后,主线程退出也会一并销毁创建出的所有子线程,在主线程退出之前,它可以脱离主线程继续独立的运行,任务执行完毕之后,这个子线程会自动释放自己占用的系统资源joinable
判断主线程和子线程是否处于关联状态static unsigned hardware_concurrency() noexcept;
获取计算机的cpu
核心数。根据这个结果在程序中创建出数量相等的线程,每个线程独自占有一个CPU核心,这些线程就不用分时复用CPU时间片,此时程序的并发效率是最高的call_once
多线程操作过程中,std::call_once()
内部的回调函数只会被执行一次。可以防止对象的重复初始化
// 定义于头文件 <mutex>
template<class Callable, class... Args>
void call_once( std::once_flag& flag, Callable&& f, Args&&... args );
线程同步和互斥锁
std::mutex
独占的互斥锁lock()
加锁,加锁不成功,则阻塞try_lock()
尝试加锁,如果加锁不成功,不会阻塞,可以处理其他逻辑unlock()
解锁
std::timed_mutex
带超时的互斥锁,不能递归的使用
// std::timed_mutex比std::_mutex多出的两个成员函数
template <class Rep, class Period>
bool try_lock_for (const chrono::duration<Rep,Period>& rel_time);
//函数是当线程获取不到互斥锁资源的时候,让线程阻塞一定的时间长度
template <class Clock, class Duration>
bool try_lock_until (const chrono::time_point<Clock,Duration>& abs_time);
//函数是当线程获取不到互斥锁资源的时候,让线程阻塞到某一个指定的时间点
//关于两个函数的返回值:当得到互斥锁的所有权之后,函数会马上解除阻塞,返回true,如果阻塞的时长用完或者到达指定的时间点之后,函数也会解除阻塞,返回false
std::recursive_mutex
不带超时的递归互斥锁,允许同一个线程多次加锁recursive_timed_mutex
带超时的递归互斥锁std::unique_lock<std::mutex>
或者std::lock_guard<std::mutex>
可以自动加锁和解锁,利用 {} 来限制作用域
条件变量 用于阻塞某一类线程,例如生产者线程或者消费者线程。但无法实现线程对共享资源的互斥访问(同步),需要和互斥锁搭配使用。
condtion_variable
需要配合std::unique_lock<std::mutex>
进行阻塞线程的wait
操作condtion_variable_any
可以配合任何带有lock()
和unlock()
语义的mutex
搭配使用。
//阻塞函数,如果上锁会自动解锁
void wait(unique_lock<mutex> &lck);
//pred 是一个回调函数,如果pred的返回值是true,调用wait方法的线程不阻塞,否则被阻塞
template<class Predicate>
void wait(unique_lock<mutex> &lck, Predicate pred);
wait
阻塞时间段
template<class Rep, class Period>
cv_status wair_for(unique_lock<mutex> &lck,
const chrono::duration<Rep, Period> &rel_time);
template<class Rep, class Period, class Predicate>
cv_status wair_for(unique_lock<mutex> &lck,
const chrono::duration<Rep, Period> &rel_time,
Predicate pred);
wait
阻塞时间点
template<class Clock, class Duration>
cv_status wair_until(unique_lock<mutex> &lck,
const chrono::time_point<Clock, Duration> &abs_time);
template<class Clock, class Duration, class Predicate>
cv_status wair_for(unique_lock<mutex> &lck,
const chrono::time_point<Clock, Duration> &abs_time,
Predicate pred);
- 通知函数 被阻塞的线程信息记录在条件变量中
// 唤醒被当前条件变量阻塞的一个线程
void notify_one() noexcept;
// 唤醒被当前条件变量阻塞的所有线程
void notify_all() noexcept;
多线程异步操作
- 线程异步:线程获取需要某个资源,但资源正在生产或者被其他线程占用。该线程不需要阻塞等待,可以做其他事情。
- 例同步:线程
A
想要得到另外一个线程B
的处理结果,线程A
需要阻塞到线程B
结束,返回结果- 例异步:线程
A
想要得到另外一个线程B
的处理结果,线程A
不需要阻塞到线程B
返回结果,期间可以继续处理别的事情。
如何在一个线程中获得另一个线程的执行结果
$std::futrue$ 用于存储线程的返回值。$future$是不能拷贝的
// 定义于头文件 <future>
template<class T> class future;
template<class T> class future<T&>;
//尖括号中的 T& 是 future 类模板对引用类型 T& 的特化声明,表示 future 类的行为或实现将会根据传入类型是否是引用类型 (T&) 进行不同的处理
template<> class future<void>;
/*
是一个模板特化,表示 future 类的一个特化版本,用于处理 void 类型。
这个特化版本用于表示没有返回值的异步操作
*/
- 构造函数
future() noexcept;
future(future && other) noexcept;
future(const future &other) = delete;
- 常用成员函数
future& operator=(future && other) noexcept;
future& operator=(const future &other) = delete;
T get();
T& get();
void get();
// get是一个阻塞函数,当子线程的数据就绪后解除阻塞就能得到传出的数值了
- 因为
future对
象内部存储的是异步线程任务执行完毕后的结果,是在调用之后的将来得到的,因此可以通过调用wait()
方法,阻塞当前线程,等待这个子线程的任务执行完毕,任务执行完毕当前线程的阻塞也就解除了。
template< class Rep, class Period >
std::future_status wait_for(const std::chrono::duration<Rep,Period> &timeout_duration ) const;
template< class Clock, class Duration >
std::future_status wait_until( const std::chrono::time_point<Clock,Duration> &timeout_time ) const;
```
* 当`wait_until()`和`wait_for()`函数返回之后,并不能确定子线程当前的状态,因此我们需要判断函数的返回值,这样就能知道子线程当前的状态了
* `future_status::deferred` 子线程中的任务函仍未启动
* `future_status::ready` 子线程中的任务已经执行完毕,结果已就绪
* `future_status::timeout` 子线程中的任务正在执行中,指定等待时长已用完
$std::promise$
// 定义于头文件 <future>
// R 是 promise 内部 future 模板类的类型 future<R>
template< class R > class promise;
template< class R > class promise<R&>;
template<> class promise<void>;
- 构造函数
promise();
promise( promise&& other ) noexcept; // 转移other内部futuee的资源
promise( const promise& other ) = delete; // 内部future不能拷贝,所以promise也不能拷贝
- 公共成员函数
std::future<T> get_future();
// A线程获取B线程的返回值结果,当B的set_value()结束后,数据就绪。A的get()解除阻塞
void set_value( const R& value );
void set_value( R&& value );
void set_value( R& value );
void set_value();
// 存储要传出的 value 值,但是不立即令状态就绪。在当前线程退出时,子线程资源被销毁,再令状态就绪
void set_value_at_thread_exit( const R& value );
void set_value_at_thread_exit( R&& value );
void set_value_at_thread_exit( R& value );
void set_value_at_thread_exit();
std::packaged_task
其实就是对子线程要执行的任务函数进行了包装,和可调用对象包装器的使用方法相同,包装完毕之后直接将包装得到的任务对象传递给线程对象就可以了
- 这个类可以将内部包装的函数和future类绑定到一起,以便进行后续的异步调用,它和std::promise有点类似,std::promise内部保存一个共享状态的值,而std::packaged_task保存的是一个函数。
// 定义于头文件 <future>
template< class > class packaged_task;
template< class R, class ...Args >
class packaged_task<R(Args...)>;
- 构造函数
packaged_task() noexcept;
template <class F>
explicit packaged_task( F&& f );
packaged_task( const packaged_task& ) = delete;
packaged_task( packaged_task&& rhs ) noexcept;
- 常用成员函数
std::future<R> get_future();
- 与
prosime
不同的是,prosime
会被传递到线程执行的函数参数中, 而packaged_task
需要将线程执行函数传递进来。
int add(int x, int y) {
return x + y;
}
int main() {
std::packaged_task<int(int, int)> task(add);
std::thread t(std::ref(task), 1, 2);
std::future<int> fu = task.get_future();
t.join();
std::cout << fu.get() << "\n";
return 0;
}
std::asyns
函数
- 通过这函数可以直接启动一个子线程并在这个子线程中执行对应的任务函数,异步任务执行完成返回的结果也是存储到一个
future
对象中,当需要获取异步任务的结果时,只需要调用future
类的get()
方法即可,如果不关注异步任务的结果,只是简单地等待任务完成的话,可以调用future
类的wait()
或者wait_for()
方法。
template<class Function, class... Args>
std::future<std::result_of_t<std::decay_t<Function>(std::decay_t<Args>...)>>
async(Function&& f, Args&&... args);
// ②
template<class Function, class... Args>
std::future<std::result_of_t<std::decay_t<Function>(std::decay_t<Args>...)>>
async(std::launch policy, Function&& f, Args&&... args);
- 函数①:直接调用传递到函数体内部的可调用对象,返回一个
future对
象 - 函数②:通过指定的策略调用传递到函数内部的可调用对象,返回一个
future
对象 - 函数参数:
f
:可调用对象,这个对象在子线程中被作为任务函数使用Args
:传递给f
的参数(实参)policy
:可调用对象f
的执行策略std::lanch::async
策略:调用async
函数时就创建新的线程执行任务函数std::lanch::deferred
策略:调用async
函数时不执行任务函数,直到调用对应的future
的get()
或者wait()
方法时才会执行任务函数。(不会创建新的线程)
std::result_of<T>
用于在模板函数中推导返回值的类型