文章目录
C++20新特性概览C++20的重要性侧面说明:正面说明:
模块(Modules)优点例子创建模块引用模块
范围库(Ranges)Ranges 是什么 ?好处:相关概念例子
概念库(Concepts)作用C++20以前C++20之后例子
协程(Coroutines)协程概念相关关键字用处生成器
并发库(Concurrency)原子智能指针智能指针(shared_ptr)线程安全吗?如何将智能指针变成线程安全?
自动合并, 可中断的线程示例
同步库(Synchronization)信号量(Semaphore)锁存器(Latches)屏障(Barriers)std::atomic 等待和通知接口std::atomic_ref
Lambda 表达式的更新1. [=, this] 需要显式捕获`this`变量2. 模板形式的 Lambda 表达式简化容器内部类型推断简化完美转发类型推断
3.支持初始化捕捉
指定初始化(Designated Initializers)船型操作符 <=>**一般情况**: 自动生成所有的比较操作符(6个)**高级情况**:
范围 for 循环语句支持初始化语句非类型模板形参支持字符串示例
C++属性符[[likely]], [[unlikely]][[nodiscard(reason)]]
日历(Calendar)功能时区(Timezone)功能std::span特性测试宏常量表达式(`constexpr`) 的更新constexpr的意义constexpr 修饰函数constexpr string & vector
consteval 函数constinit用 using 引用 enum类型示例
格式化库(std::format)示例
增加数学常量std::source_location示例
位运算一些小更新
C++20新特性概览
C++20出来已经一年多了,但是可以全面支持C++20新特性的编译器却没有多少。这从侧面反映了C++20的变化之大。然而,最为广大C++开发的程序员却没有做好迎接新特性的准备。一方面是由于C++的内容知识之多,难度之大,非一般程序员可以掌握,另一方面得益于C++强大的兼容性。30年前写的代码在今天编译器上依旧可以生成和稳定运行,这就话可不是白说的。但是C++20新特性确实可以简化代码,甚至从根本上改变了我们组织代码的方式。
Luciano Ramalho在其深受python程序员喜爱的书籍《流畅的Python》中写道:
人们总是倾向于寻求自己熟悉的东西。受到其他语言的影响,你大概能猜到 Python 会支持正则表达式,然后就会去查阅文档。但是如果你从来没见过元组拆包(tuple unpacking),也没听过描述符(descriptor)这个概念,那么估计你也不会特地去搜索它们,然后就永远失去了使用这些 Python 独有的特性的机会。
这也同样适合C++的程序员,如果你如果不去了解C++新特性,那你就失去了很多优雅而简便的代码编写。相比较其他语言,C++标准向我们承诺了更多:
the zero-overhead principle
If you don’t use the feature, you pay nothing. If you do use it, you pay no more than you would if you coded it by hand.——Bjarne Stroustrup
放心吧,让我们拥抱新特性,你会体验到新特性的快乐!
C++20的重要性
侧面说明:
C++的参考手册
我们可以看到C++11作为C++2.0版本,增加了很多内容,达到了12项。而C++17和C++20却只有7,8项。你可能觉得C++20和C++17增加的差不多,不够称之为大版本。如果细看就会发现C++20增加了三个独立的库:功能特性测试宏,概念库,范围库。这是C++17远远达不到的。C++20也正是因为有概念库,范围库而大放光彩。
C++标准的页数
我们同样可以发现相似的结论:C++11作为C++2.0版本,标准页数增加了600多页,这差不多是C++03的总页数。C++14页数几乎没变。C++17因引入了文件系统库这个基础性的功能库,外加上类型推断能力的增强和any新特性的引入,增加了不少页。C++20增加的页数和C++17增加的页数相差不大,但是由于C++标准的内容太多了,C++组委会更改了每页的大小,由美国的信纸大小改为A4大小。造成了页数增加看起来相差不大,其实内容确变化很多。
C++吸收的优秀库
format -> fmt https://github.com/fmtlib/fmt
Range ->range https//github.com/ericniebler/range-v3
Coroutines-> libgo https://github.com/yyzybb537/libgo)
-> openmp https://github.com/llvm-mirror/openmp
…
C++20看到网上非常优秀的库而带来的新特性,就把它们增加到C++的新标准中,以方便了我们使用。但C++标准只是一个标准,具体的实现可能并不是C++标准的责任,标准库可能借鉴这些优秀的库,但不会完全照抄。
正面说明:
要想说明正面说明,必须了解C++20增加了什么。这幅图大致几乎囊括了所有的新特性:
四大模块
Modules彻底改变了我们组织源码文件的方式,不需要在分.h和.cpp文件
Concepts改变了我们对模板类型的限制,我们不需要再去思考用语法技巧和语言特性去对模板类型做静态限制(编译期检查)了,这让我们很方便构造在编译期表现出大部分限制和规范(constraints)的模板
Ranges改变了我们使用算法或者算法组合的方式,为我们使用算法的提供了无限的可能
Coroutines让我们可以用同步的代码写出异步的效果
并发支持
上图框出部分极大的反应了C++对并发的支持,C++20增加了很多在其他语言看起来应有的东西:自动合并可中断的线程,信号量,锁存器,屏障,可等待通知的原子类型,原子引用,线程安全的原子智能指针。加上之前就有的各种并发支持:各种锁(唯一锁,等待锁,递归锁),线程,条件变量,异步,任务包。至此,C++语言对并发的支持可以说是最完善的了,远远超过其他语言,并且是十分高效的。
Lambda
Lambda表达式开始支持泛型,这让Lambda的使用更加灵活,更加简便,也更好技巧性。
常量修饰符(constexpr, constinit, consteval)
常量修饰符的改变,让编译期的能力大大增增强。编译期的大大增强就是可以在编译器进行内存分配。从之前的类型推断、类型检查、递归计算到现在可以进行动态分配内存。编译期能力更加突飞猛进。
基于以上四点,我更愿意将C++20称之为C++的3.0版本。虽然这些特性还有待完善,但是这是跨入C++另一个阶层的重要一步。
下面就来具体了解一下C++20新增的内容
模块(Modules)
优点
代替头文件声明实现仍然可分离, 但非必要两模块内可以拥有相同名字预处理宏只在模块内有效模块只处理一次不需要防卫式声明模块引入顺序无关紧要所有头文件都是可导入的
modules的出现彻底改变了我们组织代码文件的形式,我们不需要分为.h和.cpp文件并保证编译的一次性(pragma once或防卫式头)。其他特性基本上比较常规。
而优点中我最喜欢那个宏隔离,那个实在太棒了,可以让我们写出非入侵型代码。
例子
创建模块
// module.cpp
import
export module MyModule;
export void MyFunc()
{
std::cout << "This is my function" << std::endl;
}
引用模块
// main.cpp
import MyModule;
int main(int argc, char** argv)
{
MyFunc();
}
范围库(Ranges)
Ranges 是什么 ?
Range 代表一组元素, 或者一组元素中的一段类似 begin/end 这样的迭代器对
我们知道,迭代器是一种抽象出来的访问器,它隔离了容器类型和元素类型,并提供遍历能力。因此,我们就可以把数据和算法分离开来,数据的存储使用容器,数据的处理使用算法,两者用迭代器进行实现配合,实现了很高的复用性。在实际的应用中,我们一般需要传入一对迭代器,来表示算法所作用的范围。而这个范围在删除或增加元素的时候,迭代器会失效,或者算法的范围会变小。但是呢,可能接下来会将变化后的范围传给下一个算法,这个时候就不是很方便了。当然,利用C++的灵活性,我们也可以在函数外或使用引用在参数中重新获取最新的迭代器范围,或者建立一个tuple来返回最新的迭代器范围。但是这些做法不具备类型安全,抽象性不高,泛化能力弱。所以将一对迭代器在进行一层抽象——Ranges 也就随之而生,让我们更加便利,高效,强有力并只注重业务逻辑地利用算法。
好处:
消除迭代器对的误搭配
使变换/过滤等串联操作成为可能
惰性求值
向组装函数一样组装算法=>创造了非凡可能
注释:C++11后,我们可以自由组装函数,主要依赖于仿函数和函数适配器
仿函数=>重载操作符()的结构体或类函数适配器=>组装函数,主要有以下几个:
bind1st bind2nd not1 not2unary_function binary_functionmem_fun_ref men_fun ptr_fun eg: f = 3*x g=y+2 => f(g(x))
compose1(bind2nd(multipies<>(),3),bind1st(plus<>(), 2))
相关概念
Range: 拥有迭代器对的概念=>所有支持begin/end的容器都是有效的range基于Range的算法:接受迭代器对的算法都有range重载版本投影:交给算法之前的处理View: 过滤转化range: 延迟计算,不拥有数据,也不改写数据Range工厂: 按要求构造view管道操作符:可以用管道操作符连接视图变换,进行管道计算
例子
没有管道操作符
vector data { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
auto result {
transform(reverse(drop(transform(filter(data,isOdd),doubleNum),2)),to_string) /* "20" "16" "12" */
};
使用管道操作符
vector data { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
auto result { data
| views::filter([](const auto& value) { return value % 2 == 0; })/* 2 4 6 8 10 */
| views::transform([](const auto& value) { return value * 2.0; })/* 4 8 12 16 20 */
| views::drop(2) /* 12 16 20 */
| views::reverse /* 20 16 12 */
| views::transform([](int i) { return to_string(i); }) /* "20" "16" "12" */
};
序列工厂
auto result { view::iota(10) // 产生一个无限的序列
| views::filter([](const auto& value) { return value % 2 == 0; }) // 过滤奇数
| views::take(10) // 只取前10个
};
使用了管道操作符,代码的可读性和灵活性都得到了极大的提升:不需要在外部定义转化或过滤数据的函数,直接使用Lambda表达式。管道操作符也很好的为我们指明了数据的流向,让数据的处理流程更加清楚。最重要的是管道含义,不必产生数据处理过程中产生的中间数组,直接一个数据一个数据的处理,并且惰性计算,比如例子中的result在定义时并没有进行计算,而是等到使用时才进行计算。
概念库(Concepts)
作用
对模板类、模板函数的模板参数进行约束编译期进行约束帮助编译错误信息
C++20以前
我们可以利用一下技巧进行模板参数的限制:
type supprottraitstd::enable_if std::enable_if_tstd::is_type std::is_same std::is_interagefunction overload
我们在使用这些技巧进行模板参数的限制,代码看来有点疑惑,不够优雅。
C++20之后
concept是一个十分强大的工具:
放的位置比较随意,易于理解,还可以进行复合和组合,以表示复杂的限制。
我们再也不去考虑用语言特性去实施限制啦,它让我们的限制易于理解,并可轻易做到。
例子
定义
template
concept Incrementable = requires(T x) {x++; ++x;};
使用
template
void Foo(T t);
template
void Foo(T t);
template
void Foo(T t) requires Incrementable
void Foo(Incrementable auto t); // 函数参数限制
复合
template
concept C = requires (T& x, T& y) {
{ x.swap(y) } noexcept;
{ x.size() } -> std::convertible_to
...
};
组合
template
requires Incrementable
void Foo(T t);
template
concept C = Incrementable
void Foo(C auto t);
标准库将提供一些有用的concept
same, derived_from, convertible_to, integral, constructible, … sortable, mergeable, permutable, …
协程(Coroutines)
协程概念
简单点讲,就是一个可以记住自身状态,可随时挂起和执行的函数。具体区别另有博客介绍。
相关关键字
co_wait: 挂起协程, 等待其它计算完成co_return: 从协程中返回co_yield: 弹出一个值, 挂起协程, 下一次调用继续协程的运行
哈哈哈,这里是不是又想起了Python,确实很像python,见下面用法,你会惊叹更像。
用处
生成器异步I/O延迟计算事件驱动的程序
但是呢,C++标准库还没有提供协程帮助类的小原件,比如:生成器。那就对比python写一个关于协程的生成器呗!
生成器
python生成器
def mygen(startValue, nums):
now = startValue
endValue = now+nums
while(now < endValue):
yield now
now += 1
def main():
gen = mygen(10, 10)
num_list = [i for i in gen]
print(num_list)
main()
C++协程
generator
{
for (int i{ startValue }; i < startValue + numberOfValues; ++i)
{
co_yield i;
}
}
int main()
{
auto gen{ GetSequenceGenerator(10, 5) };
for (const auto& value : gen)
{
cout << value << endl;
}
}
其他人实现的
generator
{
while(true) co_yield start++;
}
generator
{
if(nums <= 0) co_return;
for(auto e : src) {
co_yield e;
if(num-- == 0) break;
}
}
和很显然第二个写法抽象性更高,表达能力也强一些,自然复用能力更好。
并发库(Concurrency)
原子智能指针
智能指针(shared_ptr)线程安全吗?
是: 引用计数控制单元线程安全, 保证对象只被释放一次否: 对于数据的读写没有线程安全
这个回答十分经典,但是解释起来却十分麻烦。
首先要区分三个东西:
管理对象:托管给shared_ptr的内存
引用计数:计算有多少个指针之前shared_ptr所管理的对象
shared_ptr: 包含一个指向引用计数的指针和一个指向管理对象的指针。(暂时这么描述把,虽然不准确)
这三个东西我们要分别去讨论他们的线程安全性,先给出结论:
管理对象:跟所管理的对象有关。(如果管理对象设计出来就是线程安全的,那么就是线程安全的。如果不是,那就是不安全的)
引用计数:线程安全(见C++标准文档)
shared_ptr:我们要讨论的对象,答案见上。
现在我们知道什么是shared_ptr的线程安全了么,那就是第三个,与其他无关!!!
好,现在我们现在来回答什么是shared_ptr的读?什么是shared_ptr的写?
shared_ptr中的两个指针都不变的操作为读,那就是用shared_ptr的const成员函数,比如get,*, ->,被复制给其他智能指针对象等;shared_ptr中的两个指针任何一个发生变化的操作称之为写,这个写就要分多种情况了。这就不区分了。
现在你可能已经十分清楚shared_ptr的线程安全性:shared_ptr的不安全也就是在于这两个指针的变化不是原子操作。
其实shared_ptr的线程安全性在标准文档已经有给出,shared_ptr与内置类型有相同的线程安全性。哈哈哈,不过自己分析也是很有意思的。
如何将智能指针变成线程安全?
使用 mutex 控制智能指针的访问
使用全局非成员原子操作函数访问, 诸如: std::atomic_load(), atomic_store(), …
缺点: 容易出错, 忘记使用这些操作
C++20提供了支持:atomic
内部原理可能使用了mutex全局非成员原子操作函数标记为不推荐使用
自动合并, 可中断的线程
C++20增加了可以自动join,可以随时中断的线程jthread。选择增加新类型jthread,而不是更改thread,体现了开放封闭原则。更重要的是满足C++之父的不增加运行开销的设计哲学。
std::jthread
支持中断析构函数中自动 join可以中断线程执行stop_token 中断机制std::stop_token
中断的实际请求者用来查询线程是否中断可以和condition_variable_any配合使用 std::stop_source
中断源用来请求线程停止运行stop_resources 和 stop_tokens 都可以查询到停止请求 std::stop_callback
中断的回调函数如果对应的stop_token 被要求终止, 将会触发回调函数
示例
std::queue
std::mutex mut;
std::condition_variable cv;
void worker(std::stop_token stoken) {
//注册回调函数
std::stop_callback cb(stoken, []() {
cv.notify_all(); //当中断后唤醒另一个线程
});
while (true)
{
Job currentJob;
{
std::unique_lock lck(mut); // 上锁
cv.wait(lck, [stoken]() { // 等待唤醒
return jobs.size() > 0 || stoken.stop_requested();
});
if (stoken.stop_requested())
{
break;
}
currentJob = jobs.front(); // 拿到要工作的函数或数据
jobs.pop(); // 从任务队列里删除
... // 消费具体任务
} // 结束保护区
// 任务单元完成后打印
std::cout << std::this_thread::get_id() << " " << "Doing job " << currentJob.jobId() << "\n";
}
}
void manager() {
std::stop_source ssource; //创建中断源
// 创建线程
std::jthread worker1(worker, ssource.get_token());
std::jthread worker2(worker, ssource.get_token());
for (int i = 0; i < 5; i++)
{
{
std::unique_lock lck(mut);
jobs.push(i);
cv.notify_one(); // 唤醒工作线程
}
std::this_thread::sleep_for(1s);
}
// 停止所用线程
ssource.request_stop();
// 自动join
// worker1.join()
// worker2.join()
}
同步库(Synchronization)
同步库主要用于线程的同步,我们知道线程同步有两种方式经典方式:
轮询:这就是你一直问你要同步的人,你做好了么?实现while循环。坏处很显然。通知:通知其他人你已经做好了。这种方式就是做好了就暂停运行(几乎不占用CPU)等待通知,没完成的在完成后是发出通知。通知可能是修改一个变量,也可能是一个函数调用等。
面对复杂的场景,线程同步的方式千变万化。为了应对各种情况,在程序库中有多种抽象和机制,蛋蛋库提供的同步量就有多种,比如:锁,条件变量,计数器,回调函数,锁存器,屏障…
C++11只提供了几种方式,C++20大大增强了同步的各种方式。
信号量(Semaphore)
十分轻量级的同步原语
可用来实现任何其他同步概念, 如: mutex, latches, barriers, …
两种类型:
多元信号量(counting semaphore): 建模非负值资源计数二元信号量(binary semaphore): 只有一个插孔, 两种状态, 最适合实现mutex
锁存器(Latches)
线程的同步点:线程将阻塞在这个位置, 直到到达的线程个数达标才放行, 放行之后不再关闭
锁存器只会作用一次
屏障(Barriers)
作用于多个阶段:相当于锁存器(Latches)起多次作用
每个阶段中:
一个参与者运行至屏障点时被阻塞,需要等待其他参与者都到达屏障点, 当到达线程数达标之后阶段完成的回调将被执行线程计数器被重置开启下一阶段线程得以继续执行
std::atomic 等待和通知接口
等待/阻塞在原子对象直到其值发生改变, 通过通知函数发送通知比轮训(polling)来的更高效方法
wait()notify_one()notify_all()
std::atomic_ref
atomic 引用通过引用访问变为原子操作, 被引用对象可以为非原子类型
Lambda 表达式的更新
1. [=, this] 需要显式捕获this变量
C++20 之前 [=] 隐式捕获thisC++20 开始 需要显式捕获this: [=, this]
这个我个人觉得C++组委会觉得对于this的捕获其实更像是引用捕获,而不是值捕获。
2. 模板形式的 Lambda 表达式
C++20支持在Lambda表达式中使用模板语法,其使用形式如下:
[]template
[]template
[]template
模板可以让程序泛化,而不必在乎类型,这是思考的基本出发点。但是我们却忘了模板还有一个最最基础的作用,那就是类型推断。比如函数模板,不用指定类型,使用最佳匹配。C++17更是增强了模板类型推断的能力,让模板更加省事好用。模板形式的Lambda表达是借助模板推断能力,让代码简洁,不用再去想类型推断的事情了。下次其他人问你,C++类型自动推断有几种方法,三种:auto,decltype,template T。具体好处见以下:
简化容器内部类型推断
C++20以前:
auto func = [](auto vec){
// using T = typename decltype(vec)::value_type;
using T = std::decay_t
T copy{x};
T::static_function();
using Iterator = typename T::iterator;
}
C++20之后:
auto func = []
T copy{x};
T::static_function();
using Iterator = typename T::iterator;
}
简化完美转发类型推断
C++20以前:
auto func = [](auto&& ...args) {
return foo(std::forward
}
C++20之后:
auto func = []
return foo(std::forward(args)...);
}
3.支持初始化捕捉
C++20以前:
template
auto delay_invoke(F f, Args... args){
return [f, args...]{
return std::invoke(f, args...);
}
}
C++20之后:
template
auto delay_invoke(F f, Args... args){
return [f = std::move(f), args = std::move(args)...](){
return std::invoke(f, args...);
}
}
指定初始化(Designated Initializers)
struct Data {
int anInt = 0;
std::string aString;
};
Data d{ .aString = "Hello" };
在很多时候,我们可能由于成员过多,不记得构造函数的元素循序,进行构造是必须再次查看对应关系才能进行初始化。现在你只要知道你想初始化的条目即可完成正确的构造。帅不帅。
船型操作符 <=>
正规名称: 三路比较运算符三路比较结果如下:
- (a <=> b) < 0 // 如果 a < b 则为 true
- (a <=> b) > 0 // 如果 a > b 则为 true
- (a <=> b) == 0 // 如果 a 与 b 相等或者等价 则为 true
一般情况: 自动生成所有的比较操作符(6个)
用法:auto X::operator<=>(const Y&) = default;例子:
#include
class Point {
int x; int y;
public:
auto operator<=>(const Point&) const = default; // 比较操作符自动生成
}; // 等价于以下代码
// 与上述代码等价形式
class Point {
int x; int y;
public:
friend bool operator==(const Point& a, const Point& b){
return a.x==b.x && a.y==b.y;
}
friend bool operator< (const Point& a, const Point& b){
return a.x < b.x || (a.x == b.x && a.y < b.y);
}
friend bool operator!=(const Point& a, const Point& b) {
return !(a==b);
}
friend bool operator<=(const Point& a, const Point& b) {
return !(b
} friend bool operator> (const Point& a, const Point& b) {