现代C++:C++14中的新语言特性和库特性

现代C++:C++14中的新语言特性和库特性

C++14主要是对C++11版本的修正与补充,所以新的语法和库特性也不多。

一.新语言特性

1.1变量模板

https://cppreference.cn/w/cpp/language/variable_template

我们之前在使用模板时仅支持对类和函数进行使用,C++14也支持了变量对模板的使用。我们来看一个使用变量模板求阶乘的例子:

cpp 复制代码
//控制变量精度
template<typename T>
constexpr T pi = T(3.1415926);

//实现计算阶乘
template<size_t N>
constexpr uint32_t tar = N * tar<N - 1>;

template<>
constexpr uint32_t tar<0> = 1;

int main()
{

	std::cout << typeid(pi<float>).name() << std::endl;
	std::cout << tar<10> << endl;
	return 0;
}

结果如下:

cpp 复制代码
float
3628800

像我们上篇文章介绍的类型检验,他不是有_v的版本吗,其实就是借助变量模板实现的,比如我们自己想要去检验一个变量类型是否为int,可以这么搞来简化模板的使用:

cpp 复制代码
template<typename T>
class is_Int {
public:
	static constexpr bool value = false;
};

template<>
class is_Int<int> {
public:
	static constexpr bool value = true;
};

template<typename T>
bool is_Int_v = is_Int<T>::value;

int main()
{
	std::cout << is_Int_v<decltype(10)> << endl;
	return 0;
}

1.2泛型 lambda 表达式

https://cppreference.cn/w/cpp/language/lambda

C++14 允许 lambda 表达式 使用 auto 作为参数类型,使其成为泛型 。和前面模板的语法高度类似,auto& 代表左值引用的形参,auto&& 代表万能引用的形参,auto&&... 代表可变模板参数的万能引用。其const和引用剔除规则与函数模板一致。

这个泛型lambda其实使用起来就是带模板的lambda,后续到C++20以后开始⽀持在捕捉列表和参数中间直接类似模板语法写模板参数,具体参考下面的语法。(最开始就这样设计可能更好?反而增加了学习成本),我们来看下面一个例子:

cpp 复制代码
int main()
{
	auto test = [](auto a, auto b) {
	 //typeid会发生和模板函数一样的类型剥离,导致const int& 发生两次类型剥离退化为int
		cout << "a: " << typeid(a).name() << endl;
		cout << "b: " << typeid(b).name() << endl;
		};

	int a = 1;//int
	int& b = a;//int
	const int& const c = a;//int 注意引用顶层const无效,因为引用本身就是无法被修改的
	int* p1 = &a;//int*
	const int* const p2 = &a;//顶层const被移除->const int*
	test(a, b);
	test(c, p1);
	test(p2, p1);
	std::cout << std::endl;
	//C++14 允许在 lambda 捕获中使⽤表达式初始化捕获的变量,这个变量可以是当前域定义的,也可以是没有定义的。
	int a1 = 100;
	float b1 = 1.0;
	auto test1 = [c = 10,d = a1 * 10](auto a, auto b) {
	//typeid会发生和模板函数一样的类型剥离,导致const int& 发生两次类型剥离退化为int
		cout << "a: " << typeid(a).name() << endl;
		cout << "b: " << typeid(b).name() << endl;
		cout << "c: " << typeid(c).name() << "\t" << c << endl;
		cout << "d: " << typeid(d).name() << "\t" << d << endl;
	};
	test1(a1, b1);
	std::cout << std::endl;
	//C++20开始支持使用写模板函数那样写lambda表达式
	auto fun = []<class T, class U>(const T & a, const U & b)
	{
		return a + b;
	};
	std::string x = "30";
	std::string y = "100";
	cout << fun(x, y) << endl;
	return 0;
}

结果如下:

cpp 复制代码
a: int
b: int
a: int
b: int * __ptr64
a: int const * __ptr64
b: int * __ptr64

a: int
b: float
c: int  10
d: int  1000

30100

1.3返回类型推导

参考文档为此链接中的返回类型推导部分:
https://cppreference.cn/w/cpp/language/function#Return_type_deduction_.28since_C.2B.2B14.29

像之前的C++11中,虽然允许我们使用auto作为返回值,但是此时需要我们写一个尾置返回类型才能使用。本质上是为了能够看清函数参数类型与名称的一种写法。下面是基本的用法例子:

cpp 复制代码
int x = 10;
int& y = x;
const int* z = &x;

auto func_x() {
	return x;
}

auto func_y() {
	return y;
}

auto func_z() {
	return z;
}
int main()
{
	decltype(func_x()) _x;//返回类型为int
	decltype(func_y()) _y;//int
	decltype(func_z()) _z;//const int*
	return 0;
}

但是C++14中,便支持了直接使用auto作为返回值。但是它遵循如下几个规则:

  • C++14 直接支持 auto 返回类型推导 。如果函数声明的声明说明符序列包含关键词 auto,那么尾随返回类型可以省略 ,且编译器将从未舍弃的返回语句中所用的操作数的类型推导出它。如果有多条返回语句,那么它们必须推导出相同的类型

上面的意思开头部分我们是能够理解的,就是auto推导返回类型遵循函数模板推导参数类型那一套规则。但是为什么会有一个未舍弃 的说法。这其实就是constexpr搞的鬼,我们来看下面的例子:

正常情况下这样子写是编不过的,因为多条if语句的返回值不同:

cpp 复制代码
// 报错,多返回语句需类型⼀致
auto f4(int x) {
	if (x > 0)
		return 1.0; // double
	else
		return 2; // int → 错误:类型不⼀致
}

但是如果可以编译期进行判断舍去部分if判断语句,保证剩余的if语句返回的类型均相同便可以编译通过:

cpp 复制代码
auto f4(int x) {
	//编译期间判断x为整型类型,则舍弃下面的return 2仅保留return1.0
	if constexpr(std::is_integral_v<decltype(x)>)
		return 1.0; // double
	else
		return 2; // int → 错误:类型不⼀致
}
  • 如果返回类型没有使用 decltype(auto) ,那么推导遵循模板实参推导 的规则进行。如果返回类型是 decltype(auto),那么返回类型是将返回语句中所用的操作数包裹到 decltype 中时所得到的类型。

这个其实实际上意思就是你给我什么类型我就精准的返回给你什么类型,我也不去掉&和const,参考这篇文章:
https://blog.csdn.net/xiuwoaiailixiya/article/details/156116500

中说的decltype(auto)

但是实际上我们一般不会闲的没事干省略返回类型,这样就会导致一个月后只有老天爷才能看懂这段代码的返回值了,可读性极差。一般是结合完美转发进行使用的在实际生产环境中:

cpp 复制代码
//完美转发参数包中的所有值,保持其左右值等属性整个过程中均不变的同时传给目标函数使用
template<typename F,typename... Args>
decltype(auto) func_forward(F&& func,Args&&... args)
{
	return std::forward<F>(func)(std::forward<Args>(args)...);
}

int main()
{
	auto type_x = func_forward(strlen, "114514");
	cout << typeid(type_x).name() << "\t" << type_x << endl;//打印结果:unsigned __int64(size_t)        6
	return 0;
}

1.4二进制字面量和数字分割符

这两个都没什么好说的,看文档和概念就能明白在说什么了。
二进制字面量

  • 十进制字面量:是一个非零十进制数字(1、2、3、4、5、6、7、8、9)后随零或多个十进制数字(0、1、2、3、4、5、6、7、8、9)
  • 八进制字面量:是数字零(0)后随零或多个八进制数字(0、1、2、3、4、5、6、7)
  • 十六进制字面量 :是字符序列 0x0X 后随一个或多个十六进制数字(0、1、2、3、4、5、6、7、8、9、a、A、b、B、c、C、d、D、e、E、f、F)
  • 二进制字面量 :是字符序列 0b0B 后随一个或多个二进制数字(0、1),二进制字面量是 C++14 开始支持的。

数字分割符

  • C++14 允许在数字字面量中使用单引号作为分隔符,提高可读性。(至于你想怎么分割,你觉得怎么好看怎么来)

一个简单的例子如下:

cpp 复制代码
int main()
{
	int x = 1'0000'000;//数字分割符
	bitset<32> b(0b1010101010);//二进制字面量
	cout << "x:" << x << "\t" << b.to_string() << endl;
	return 0;
}

结果如下:

cpp 复制代码
x:10000000      00000000000000000000001010101010

1.5带有默认非静态成员初始化器的聚合类

https://cppreference.cn/w/cpp/language/aggregate_initialization

这部分最主要的是方便了我们对聚合类通过列表的方式对其进行初始化,具体的相关概念如下:
聚合类的定义

聚合类是指满足以下条件的类(包括结构体):

  1. 没有用户提供的构造函数
  2. 没有私有或受保护的非静态数据成员
  3. 没有基类
  4. 没有虚函数

关键点

  1. 默认成员初始化器:当使用默认构造或值初始化时,成员会使用这些默认值
  2. 聚合初始化:仍然可以使用花括号初始化列表,未指定的成员会使用默认值
  3. 初始化顺序:初始化列表中的值按成员声明顺序应用于成员

聚合类的定义和初始化方式的变化

  • C++11 之前 ,聚合类不能有任何成员初始化器,但从 C++14 开始,这个限制被放宽了。
  • C++14 允许聚合类包含默认的非静态成员初始化器(default member initializers),这使得聚合类的使用更加灵活。
  • 在后续 C++17、C++20 中对嵌套类定义在不断放宽,并且初始化方式也进一步优化,具体看文档和下面代码样例。(但是也有缩紧的部分,具体看下面例子)
cpp 复制代码
struct PersonInfo {
    //C++11后到C++20前可以使用delete和default同样满足聚合初始化
    //但是C++20起不可以这样搞了,必须是严谨的用户没有定义默认构造
    //PersonInfo() = delete;
    //PersonInfo() = default;
    std::string name = "114514";
    std::string phone_num = "158904";
    int age = 20;
};

int main()
{
    PersonInfo p1 = { "knd", "158904", 20 };
    cout << p1.name << endl;
    cout << p1.phone_num << endl;
    cout << p1.age << endl;
    //C++14可以使用下面的方式进行聚合初始化
    PersonInfo p2{ "ena", "114514", 21 };
    cout << p2.name << endl;
    cout << p2.phone_num << endl;
    cout << p2.age << endl;
    // C++20中初始化聚合类的⽅式,解决C++14必须按顺序给值初始化的问题
    PersonInfo p3{ .name = "knd", .phone_num = {"158904"}, .age = 20 };
    cout << p3.name << endl;
    cout << p3.phone_num << endl;
    cout << p3.age << endl;
    // C++20中还⽀持嵌套类的初始化
    struct Inner {
        int a;
        int b;
    };
    struct Outer {
        Inner i;
        int c;
    };
    Outer o1{ .i{1,2}, .c = 3 };
    // 或
    Outer o2{ .i = {.a = 1, .b = 2}, .c = 3 };
    cout << o1.i.a << endl;
    cout << o1.i.b << endl;
    cout << o1.c << endl;
    cout << o2.i.a << endl;
    cout << o2.i.b << endl;
    cout << o2.c << endl;
    return 0;
}

结果如下:

cpp 复制代码
knd
158904
20
ena
114514
21
knd
158904
20
1
2
3
1
2
3

1.6字面量后缀

参考文档在此链接的字面量运算符部分:
https://cppreference.cn/w/cpp/language/user_literal
字面量后缀 是附加在字面量后面的标识符,用于明确指定该字面量的具体类型。这在避免歧义确保精度控制转换提升代码可读性方面至关重要。

cpp 复制代码
// C++98 就有的字面量后缀
// 整型和浮点数的字面量后缀
auto a = 10;      // int
auto b = 10u;     // unsigned int
auto c = 10l;     // long
auto d = 10ul;    // unsigned long
auto e = 10ll;    // long long
auto f = 10ull;   // unsigned long long
auto g = 3.14;    // double
auto h = 3.14f;   // float
auto i = 3.14l;   // long double

C++11 开始,允许整数、浮点数、字符和字符串字面量通过定义用户定义后缀 来生成用户定义类型的对象。这是通过重载 operator"" 实现的。它允许程序员为字面量(数字、字符、字符串)定义自己的后缀,从而将这些"裸"字面量自动转换为具有特定类型和语义的对象。
C++14/17/20 中库里面定义了一些实用的时间、字符串等字面量后缀

cpp 复制代码
//语法:ReturnType operator "" _YourSuffix(Parameters);
// ReturnType:你希望转换后的目标类型。
// _YourSuffix:关键:你自定义的后缀名。必须以下划线 _ 开头。
//              不以 _ 开头的(如 s, h, i)保留给标准库使用。
// Parameters:参数类型取决于你处理的是哪种字面量(整型、浮点、字符、字符串或原始形式)。

#include<string>
#include<string_view>

std::string operator "" _s(const char* str, size_t len) {
    return std::string(str, len);
}

std::string_view operator "" _sv(const char* str, size_t len) {
    return std::string_view(str, len);
}

float operator ""_e(const char* str) {
    return std::stof(std::string(str));
}

constexpr long double operator "" _km(unsigned long long int x) {
    return x * 1000.0;   // 将公里转换为米
}

constexpr long double operator "" _pi(long double x) {
    return x * 3.14159265358979323846L;
}

int main() {
    auto s1 = "hello"_s;
    auto s2 = "Hello\0World"_s;
    auto sv1 = "hello"_sv;
    auto distance = 5_km;    // 相当于 auto distance = 5000.0L;
    auto angle = 2.0_pi;     // 相当于 auto angle = 6.28318530717958647692L;
    auto x = 12.3_e;
    return 0;
}

C++14 起,标准库提供了大量开箱即用 的字面量,它们定义在内联命名空间 std::literals 中。

常见标准库字面量如下:

后缀 例子 转换后的类型 头文件
s "hello"s std::string <string>
sv "hello"sv std::string_view <string_view>
h 24h std::chrono::hours <chrono>
min 30min std::chrono::minutes <chrono>
s 10s std::chrono::seconds <chrono>
ms 100ms std::chrono::milliseconds <chrono>
us 100us std::chrono::microseconds <chrono>
ns 100ns std::chrono::nanoseconds <chrono>
i 2.0 + 3.0i std::complex<double> <complex>
if, i, il 1.0if std::complex<float> <complex>
cpp 复制代码
#include <string>
#include <string_view>
#include <chrono>
#include <complex>

using namespace std::literals; // 引入所有字面量后缀

int main() {
    // 1. 字符串和字符串视图
    std::string str = "hello"s;
    std::string_view sv = "hello"sv;
    std::cout << "string: " << str << ", view: " << sv << std::endl;

    // 2. 时间字面量 (C++14起)
    auto hours = 24h;
    auto minutes = 30min;
    auto seconds = 10s;
    auto ms = 100ms;
    auto us = 100us;
    auto ns = 100ns;

    std::cout << "Duration: " << hours.count() << "h, "
        << minutes.count() << "min, "
        << seconds.count() << "s, "
        << ms.count() << "ms, "
        << us.count() << "us, "
        << ns.count() << "ns" << std::endl;

    // 3. 复数字面量 (C++14起)
    std::complex<double> c1 = 2.0 + 3.0i;
    std::complex<float> c2 = 1.0if;   // if 后缀表示 float 类型
    // 也可以使用 i 或 il (long double)

    std::cout << "Complex: " << c1 << ", " << c2 << std::endl;

    return 0;
}

结果如下:

cpp 复制代码
string: hello, view: hello
Duration: 24h, 30min, 10s, 100ms, 100us, 100ns
Complex: (2,3), (0,1)

二.新库特性

2.1std::exchange

https://cppreference.cn/w/cpp/utility/exchange

  • std::exchange 是 C++14 标准库在 <utility> 头文件中引入的一个实用函数模板,它提供了一种简洁高效的方式来替换一个对象的值并返回其旧值
  • 相比我们自己实现替换而言,std::exchange 使用上更加简洁,并且 C++20 以后库里面将这个函数实现为 constexpr,效率更高了。

一种简单的实现思路如下,同时我们结合这个例子认识下std::exchange的食用方法:

cpp 复制代码
namespace my_std {
	template<typename T,typename U = T>
	T exchange(T& src, U&& new_value)
	{
		T tmp = std::move(src);
		src = std::forward<U>(new_value);
		return tmp;
	}
}

int main()
{
	std::string str("1111");
	std::string test_str("2222");
	cout << std::exchange<decltype(str)>(test_str, str) << endl;
	return 0;
}

结果如下:

cpp 复制代码
2222

2.2std::make_unique与(C++20)std::make_unique_for_overwrite

https://cppreference.cn/w/cpp/memory/unique_ptr/make_unique

在说这两个家伙之前,我们有必要先来区分下值初始化与默认初始化的区别,这正是这二者的区别所在:

2.2.1值初始化与默认初始化

值初始化规则(C++17起)

  • 如果 T 是 有用户提供的默认构造函数 的类类型:调用该默认构造函数(不先清零)。

  • 如果 T 是 没有用户提供的默认构造函数 的类类型(例如聚合体、内置类型组成的结体):先零初始化整个对象,再调用默认构造函数(如果有)。

  • 如果 T 是内置类型:零初始化(变为 0 / nullptr / false)。
    默认初始化规则

  • 如果 T 是 有用户提供的默认构造函数 的类类型:调用该默认构造函数。

  • 如果 T 是 没有用户提供的默认构造函数 的类类型:不做任何初始化(成员的值不确定,除非成员自己有默认成员初始化器)。

  • 如果 T 是内置类型:不做任何初始化(值不确定)。

对make_unique的认识

  • std::make_unique 是 C++14 引入的一个智能指针创建工具函数,用于安全地创建和管理 std::unique_ptr 对象。它是现代 C++ 中推荐的对象创建方式之一,类似于 C++11 的 std::make_shared
  • std::make_unique 相比直接构造 std::unique_ptr 对象而言更安全 一些。当构造函数或初始化过程中抛出异常时,std::make_unique 能确保已分配的资源被正确释放。直接使用 new 可能导致在智能指针接管前发生异常,造成内存泄漏 。所以现代 C++ 中更推荐使用 std::make_sharedstd::make_unique 这类工具函数创建智能指针对象。

我们来看一个例子来认识make_unique:

cpp 复制代码
class Data {
public:
	Data(int _year,int _month,int _day)
		:year(_year),
		month(_month),
		day(_day)
	{ }


	friend std::ostream& operator<<(std::ostream& os, const Data& data);
private:
	int year = 0;
	int month = 0;
	int day = 0;
};

std::ostream& operator<<(std::ostream& os, const Data& data)
{
	return os << data.year << "年" << data.month << "月" << data.day << "日";
}

int main()
{
	//make_unique对于int这些内置类型会进行值初始化,造成不必要性能开销
	unique_ptr<Data> d_ptr = make_unique<Data>(2026,5,22);
	//C++20:make_unique_for_overwrite进行默认初始化
	cout << *d_ptr << endl;
	return 0;
}

make_uniquemake_unique_for_overwrite的区别其实就是值初始化(make_unique)与默认初始化(make_unique_for_overwrite)的区别。

2.3std::integer_sequence

https://cppreference.cn/w/cpp/utility/integer_sequence

他其实是编译期表示整型序列的一个工具,一般是用来解包tuple参数包的,我们来看下面一个使用例子,直接说不能很明白它及它衍生出来的几个工具:

cpp 复制代码
template<typename T,T... Ints>
void print_sequence(int id, std::integer_sequence<T, Ints...> se)
{
	std::cout << "序号:" << id << ",大小为:" << se.size() << endl;
	//折叠表达式,需要编译器支持C++17
	//实际展开为假设为三个元素:(std::cout << a << " "), ((std::cout << b << " "), (std::cout << c << " ")) << endl;
	((std::cout << Ints << " "), ...) << endl;
}

template<typename Tuple,size_t... I>
void print_tuple_impl(const Tuple& t, std::index_sequence<I...>)
{
	//使用I...参数包从0~N-1打印所有参数包中的元素
	//实际展开为假设为三个元素:(std::cout << a << " "), ((std::cout << b << " "), (std::cout << c << " ")) << endl;
	((std::cout << std::get<I>(t) << " "), ...) << endl;
}
//打印tuple中的内容
template<typename... Args>
void print_tuple(const std::tuple<Args...>& t)
{
	//借助index_sequence_for获取t中的参数个数
	print_tuple_impl(t, std::index_sequence_for<Args...>{});
}

int main()
{
	print_sequence(1, std::integer_sequence<uint32_t, 1, 2, 3, 4, 5>());
	print_sequence(2, std::make_integer_sequence<size_t, 12>{});
	print_sequence(3, std::index_sequence<6,7,8,9,10>{});
	print_sequence(4, std::make_index_sequence<12>{});
	print_sequence(5, std::index_sequence_for<std::ios,int,double>{});
	std::tuple<int, std::string, double> t(10, "xiu114514", 10.0);
	print_tuple(t);
	return 0;
}

结果如下:

cpp 复制代码
序号:1,大小为:5
1 2 3 4 5
序号:2,大小为:12
0 1 2 3 4 5 6 7 8 9 10 11
序号:3,大小为:5
6 7 8 9 10
序号:4,大小为:12
0 1 2 3 4 5 6 7 8 9 10 11
序号:5,大小为:3
0 1 2
10 xiu114514 10

2.4std::qutoed

https://cppreference.cn/w/cpp/io/manip/quoted
std::quoted 是 C++14 引入的一个 I/O 操纵器 ,用于简化带引号字符串的输入输出操作。它定义在 <iomanip> 头文件中。

std::quoted 主要用于:

  • 输出时:自动为字符串添加引号
  • 输入时:自动去除字符串周围的引号
  • 处理转义字符:自动处理引号内的转义序列

当然我们也可以自定义包裹在字符串两边的符号 ,如果我们自定义的符号并不是转义字符,他和转义字符走的逻辑是一致的:

例如:你希望用 # 作为分隔符,转义字符使用 ^(不打算用 \)。但字符串内容本身可能包含 # 字符,比如 "hello #world"

若直接使用 quoted(str, '#', '^'),则输出时:

  • 外围添加 #,内部遇到 # 时会用 ^# 转义。

输入时也能正确还原 ^##

关键点std::quoted 要求此时你必须明确提供一个转义字符,遇到分隔符时就用该转义字符进行转义。

如果你完全不想用转义字符(即不提供任何转义机制),那么 std::quoted 无法直接处理------因为当字符串内部出现分隔符时,不转义会导致输入解析错乱(无法区分哪个是真正的结束符)。

下面我们来看一个例子,这个例子包含了基本使用和自定义分隔符及简单的序列化和反序列化示例:

cpp 复制代码
struct Config {
	std::string name;
	std::string password;
	std::string uid;
	int id;
};

std::string Serialization(const Config& config)
{
	std::ostringstream os;
	//cin输入时一般以\n或空格为分割符,这里使用换行或者空格都可以达到后续反序列化的目的
	//即使字段中有\n或空格,因为qutoted读到"时在读到下一个"不会停止读取,所以不会出现截断问题
	os << std::quoted(config.name) << " " \
		<< std::quoted(config.password) << " " \
		<< std::quoted(config.uid) << " " \
		<< config.id;
	return os.str();
}

Config Deserialization(const std::string& str)
{
	Config ret;
	std::istringstream is(str);
	is >> std::quoted(ret.name) \
		>> std::quoted(ret.password) \
		>> std::quoted(ret.uid) \
		>> ret.id;
	return ret;
}

int main()
{
	std::string text("Hello,\"Mr.Zhang!");
	//输出时自动添加双引号
	std::cout << "with quoted:" << std::quoted(text) << endl;//with quoted:"Hello,\"Mr.Zhang!"
	std::cout << "without quoted:" << text << endl;//without quoted:Hello,"Mr.Zhang!
	//输入时去除双引号
	std::string text1 = R"("hello")";
	std::stringstream ss(text1);
	ss >> std::quoted(text1);
	std::cout << std::endl;
	cout << text1 << endl;//打印hello
	//自定义分割符号
	std::string text3 = "Hello,%Mr.Zhang!%";
	std::cout << "with quoted:" << std::quoted(text3,'%') << endl;
	std::cout << "without quoted:" << text3 << endl;
	std::cout << std::endl;
	//序列化反序列化例子
	Config con{ "xi u","114514","sk-114514",20 };
	std::string ser = Serialization(con);
	std::cout << ser << std::endl;
	Config con1 = Deserialization(ser);
	std::cout << con1.name << " " << con1.password << " " << con1.uid << " " << con1.id;
	return 0;
}

结果如下:

cpp 复制代码
with quoted:"Hello,\"Mr.Zhang!"
without quoted:Hello,"Mr.Zhang!

hello
with quoted:%Hello,\%Mr.Zhang!\%%
without quoted:Hello,%Mr.Zhang!%

"xi u" "114514" "sk-114514" 20
xi u 114514 sk-114514 20

2.5std::shared_timed_mutex和(C++17)std::shared_mutex

理解共享锁与独占锁的关系

因为我们日常总是能碰到多读少写的情况,如果单纯仅使用我们之前学的互斥锁等其他锁多多少少会造成性能的损耗和时间上的浪费。有没有什么方式能够让读操作的线程能够共享锁,而写操作线程则是独占锁。共享锁与独占锁便是诞生于这种契机。

二者的机制如下:

共享锁可以被多个线程同时获取,但是当共享锁被获取时其他线程无法获取独占锁。

独占锁仅可以被一个线程单独获取,且独占锁被获取时其他线程无法获取独占锁与共享锁。

认识std::shared_timed_mutex和std::shared_mutex

https://cppreference.cn/w/cpp/thread/shared_timed_mutex
https://cppreference.cn/w/cpp/thread/shared_mutex

  • shared_timed_mutex是 C++14 提供的一种同步原语,能用于保护数据免受多个线程同时访问。与其他促进独占访问的互斥体类型相反,它拥有两个访问层次:

    • 共享:多个线程能共享同一互斥体的所有权。
    • 独占:仅一个线程能占有互斥体。
  • shared_mutex 是 C++17 提供的一个同步原语,可用于保护共享数据不被多个线程同时访问。与便于独占访问的其他互斥体类型不同,shared_mutex 拥有两个访问级别:

    • 共享:多个线程能共享同一互斥体的所有权。
    • 独占:仅一个线程能占有互斥。
  • 若一个线程已获取独占锁 (通过 locktry_lock),则无其他线程能获取该锁(包括共享的)。

  • 若一个线程已获取共享锁 (通过 lock_sharedtry_lock_shared),则无其他线程能获取独占锁,但可以获取共享锁。

  • 仅当任何线程均未获取独占锁时,共享锁能被多个线程获取;在一个线程内,同一时刻只能获取一个锁(共享或独占)。

  • shared_timed_mutexshared_mutex 的主要区别是:shared_timed_mutex 提供超时锁定 相关系列的接口:try_lock_for/try_lock_untiltry_lock_shared_for/try_lock_shared_until

  • 其次 C++14 还提供了一个 shared_lock 的 RAII 管理共享锁的类型。一般建议,共享锁时使用 shared_lock,独占锁时使用 unique_lock

  • 日常中如果没有超时控制的需求且支持 C++17,优先推荐 shared_mutex,因为它通常更轻量,不需要实现复杂的超时逻辑。

我们来看下面一个例子:

cpp 复制代码
//#define MY_COUT std::cout
// C++20⽀持,⽤于多线程同步的输出,保证顺序不乱
#define MY_COUT std::osyncstream(std::cout)

//using TimeMutex = std::shared_mutex;
using TimeMutex = std::shared_timed_mutex;
class TheardReadWrite {
public:
	//多个线程可以同时对数据进行读取
	//get和add是两种锁同时支持的操作
	unsigned int get()
	{
		//使用共享锁
		std::shared_lock<TimeMutex> lock(_mutex);
		return _value;
	}

	void add()
	{
		//对数据进行修改时只能有一个线程进入
		std::unique_lock<TimeMutex> lock(_mutex);
		_value++;
	}
	// 尝试获取独占锁来修改计数器
	bool try_add() {
		std::unique_lock<TimeMutex> lock(_mutex, std::try_to_lock);
		//owns_lock提供判断是否获取到锁的结果
		if (lock.owns_lock()) {
			++_value;
			return true;
		} 
		return false;
	} 
	//以下操作均为shared_timed_mutex才支持的操作
	// ⼀段时间内尝试获取独占锁来修改计数器
	bool try_increment_for(int milliseconds) {
		std::unique_lock<TimeMutex> lock(_mutex, std::chrono::milliseconds(milliseconds));
		//通过重载operator bool判断是否获取到锁
		if (lock) {
			++_value;
			return true;
		} 
		return false;
	}
private:
	mutable TimeMutex _mutex;
	unsigned int _value = 0;
};

const int writer_num = 1;
const int reader_num = 5;

int main()
{
	TheardReadWrite counter;
	auto writer = [&counter]() {
		//写线程休息1s进行写入,共进行10次
		for (int i = 0; i < 10; i++)
		{
			this_thread::sleep_for(std::chrono::seconds(1));
			MY_COUT << "写线程:" << this_thread::get_id() << ",开始写入。" << endl;
			counter.add();
		}
	};

	auto reader = [&counter]() {
		//读线程休息1.5s进行读取,共进行10次
		for (int i = 0; i < 10; i++)
		{
			this_thread::sleep_for(std::chrono::milliseconds(1500));
			MY_COUT << "读线程:" << this_thread::get_id() << ",读取到当前value为:" << counter.get() << endl;
		}
	};

	std::vector<std::thread> writer_threads;
	for (int i = 0; i < writer_num; i++)
	{
		writer_threads.emplace_back(writer);
	}
	std::vector<std::thread> reader_threads;
	for (int i = 0; i < reader_num; i++)
	{
		reader_threads.emplace_back(reader);
	}

	for (auto& wr : writer_threads)
	{
		wr.join();
	}
	for (auto& rr : reader_threads)
	{
		rr.join();
	}
	return 0;
}

一种情况的打印结果如下:

cpp 复制代码
写线程:29500,开始写入。
读线程:27872,读取到当前value为:1
读线程:8832,读取到当前value为:1
读线程:25656,读取到当前value为:1
读线程:13304,读取到当前value为:1
读线程:4292,读取到当前value为:1
写线程:29500,开始写入。
读线程:4292,读取到当前value为:2
读线程:27872,读取到当前value为:2
读线程:8832,读取到当前value为:2
读线程:13304,读取到当前value为:2
读线程:25656,读取到当前value为:2
写线程:29500,开始写入。
写线程:29500,开始写入。
读线程:13304,读取到当前value为:4
读线程:25656,读取到当前value为:4
读线程:8832,读取到当前value为:4
读线程:27872,读取到当前value为:4
读线程:4292,读取到当前value为:4
写线程:29500,开始写入。
读线程:25656,读取到当前value为:5
读线程:13304,读取到当前value为:5
读线程:4292,读取到当前value为:5
读线程:8832,读取到当前value为:5
读线程:27872,读取到当前value为:5
写线程:29500,开始写入。
写线程:29500,开始写入。
读线程:13304,读取到当前value为:7
读线程:25656,读取到当前value为:7
读线程:27872,读取到当前value为:7
读线程:8832,读取到当前value为:7
读线程:4292,读取到当前value为:7
写线程:29500,开始写入。
读线程:25656,读取到当前value为:8
读线程:8832,读取到当前value为:8
读线程:13304,读取到当前value为:8
读线程:27872,读取到当前value为:8
读线程:4292,读取到当前value为:8
写线程:29500,开始写入。
写线程:29500,开始写入。
读线程:8832,读取到当前value为:10
读线程:4292,读取到当前value为:10
读线程:27872,读取到当前value为:10
读线程:25656,读取到当前value为:10
读线程:13304,读取到当前value为:10
读线程:13304,读取到当前value为:10
读线程:4292,读取到当前value为:10
读线程:8832,读取到当前value为:10
读线程:25656,读取到当前value为:10
读线程:27872,读取到当前value为:10
读线程:4292,读取到当前value为:10
读线程:13304,读取到当前value为:10
读线程:25656,读取到当前value为:10
读线程:27872,读取到当前value为:10
读线程:8832,读取到当前value为:10
读线程:8832,读取到当前value为:10
读线程:13304,读取到当前value为:10
读线程:25656,读取到当前value为:10
读线程:4292,读取到当前value为:10
读线程:27872,读取到当前value为:10
相关推荐
biter down3 小时前
14:pytest-order 插件 顺序控制案例
开发语言·python·pytest
郝学胜-神的一滴3 小时前
Qt 高级开发 009: C++ Lambda 表达式
开发语言·c++·qt·软件构建
星栈独行3 小时前
我在 Rust 全栈项目里用 JWT 做无状态认证
开发语言·后端·rust·前端框架·开源·github·web
石山代码4 小时前
C++ 轻量级日志系统
开发语言·c++
小技与小术4 小时前
玩转Flask
开发语言·python·flask
SilentSamsara4 小时前
Python 性能优化:tracemalloc、profiling 与 C 扩展加速
开发语言·python·青少年编程·性能优化
冰小忆5 小时前
大驼峰命名规范和小驼峰命名规范的区别是什么?
开发语言·python
冉卓电子6 小时前
MPC5604B/C eMIOS 高级定时器全解
c语言
এ慕ོ冬℘゜6 小时前
JS 前端基础面试题
开发语言·前端·javascript
浩少7026 小时前
【无标题】
java·开发语言