当前位置:   article > 正文

C++20新特性概览

c++20

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++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 <iostream>
export module MyModule; 
export void MyFunc() 
{
    std::cout << "This is my function" << std::endl;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
引用模块
// main.cpp
import MyModule;
int main(int argc, char** argv) 
{ 
    MyFunc();
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

范围库(Ranges)

Ranges 是什么 ?

  • Range 代表一组元素, 或者一组元素中的一段
  • 类似 begin/end 这样的迭代器对

我们知道,迭代器是一种抽象出来的访问器,它隔离了容器类型和元素类型,并提供遍历能力。因此,我们就可以把数据和算法分离开来,数据的存储使用容器,数据的处理使用算法,两者用迭代器进行实现配合,实现了很高的复用性。在实际的应用中,我们一般需要传入一对迭代器,来表示算法所作用的范围。而这个范围在删除或增加元素的时候,迭代器会失效,或者算法的范围会变小。但是呢,可能接下来会将变化后的范围传给下一个算法,这个时候就不是很方便了。当然,利用C++的灵活性,我们也可以在函数外或使用引用在参数中重新获取最新的迭代器范围,或者建立一个tuple来返回最新的迭代器范围。但是这些做法不具备类型安全,抽象性不高,泛化能力弱。所以将一对迭代器在进行一层抽象——Ranges 也就随之而生,让我们更加便利,高效,强有力并只注重业务逻辑地利用算法。

好处:

  • 消除迭代器对的误搭配

  • 使变换/过滤等串联操作成为可能

  • 惰性求值

  • 向组装函数一样组装算法=>创造了非凡可能

    注释:C++11后,我们可以自由组装函数,主要依赖于仿函数和函数适配器

    • 仿函数=>重载操作符()的结构体或类
    • 函数适配器=>组装函数,主要有以下几个:
      • bind1st bind2nd not1 not2
      • unary_function binary_function
      • mem_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" */
};

  • 1
  • 2
  • 3
  • 4
  • 5
  • 使用管道操作符
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" */
};
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 序列工厂
auto result { view::iota(10)                                         // 产生一个无限的序列
   | views::filter([](const auto& value) { return value % 2 == 0; }) // 过滤奇数
   | views::take(10)                                                 // 只取前10个
};
  • 1
  • 2
  • 3
  • 4

使用了管道操作符,代码的可读性和灵活性都得到了极大的提升:不需要在外部定义转化或过滤数据的函数,直接使用Lambda表达式。管道操作符也很好的为我们指明了数据的流向,让数据的处理流程更加清楚。最重要的是管道含义,不必产生数据处理过程中产生的中间数组,直接一个数据一个数据的处理,并且惰性计算,比如例子中的result在定义时并没有进行计算,而是等到使用时才进行计算。

概念库(Concepts)

作用

  • 对模板类、模板函数的模板参数进行约束
  • 编译期进行约束
  • 帮助编译错误信息

C++20以前

我们可以利用一下技巧进行模板参数的限制:

  • type supprot
  • trait
  • std::enable_if std::enable_if_t
  • std::is_type std::is_same std::is_interage
  • function overload

我们在使用这些技巧进行模板参数的限制,代码看来有点疑惑,不够优雅。

C++20之后

concept是一个十分强大的工具:

放的位置比较随意,易于理解,还可以进行复合和组合,以表示复杂的限制。

我们再也不去考虑用语言特性去实施限制啦,它让我们的限制易于理解,并可轻易做到。

例子

  • 定义
template<typename T> // 定义一个concept
concept Incrementable = requires(T x) {x++; ++x;};
  • 1
  • 2
  • 使用
template<Incrementable T>                          // 直接限制
void Foo(T t); 

template<typename T> requires Incrementable<T>     // 函数前 requires限制
void Foo(T t); 

template<typename T>                               // 函数后 reqires限制
void Foo(T t) requires Incrementable<T>; 

void Foo(Incrementable auto t);                    // 函数参数限制
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 复合
template <typename T>
concept C = requires (T& x, T& y) {
  { x.swap(y) } noexcept;
  { x.size() } -> std::convertible_to<std::size_t>;
  ...
};
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 组合
template<typename T> 
requires Incrementable<T> && Decrementable<T> 
void Foo(T t); 

template<typename T>
concept C = Incrementable<T> && Decrementable<T>;
void Foo(C auto t);
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 标准库将提供一些有用的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()
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • C++协程
generator<int> GetSequenceGenerator(int startValue, size_t numberOfValues)
{
    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;
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 其他人实现的
generator<int> sep(int start)       // 无限生成器
{
    while(true) co_yield start++;
}

generator<int> take(generator<int>& src, int nums) // 只拿前nums个数
{
    if(nums <= 0) co_return;
    for(auto e : src) {
        co_yield e;
        if(num-- == 0) break;
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

和很显然第二个写法抽象性更高,表达能力也强一些,自然复用能力更好。

并发库(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<shared_ptr<T>>, atomic<weak_ptr<T>>
    • 内部原理可能使用了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<Job> jobs;
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()
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60

同步库(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 之前 [=] 隐式捕获this
  • C++20 开始 需要显式捕获this: [=, this]

这个我个人觉得C++组委会觉得对于this的捕获其实更像是引用捕获,而不是值捕获。

2. 模板形式的 Lambda 表达式

C++20支持在Lambda表达式中使用模板语法,其使用形式如下:

[]template<T>(T x) {/* ... */}; 
[]template<T>(T* p) {/* ... */}; 
[]template<T, int N>(T (&a)[N]) {/* ... */};
  • 1
  • 2
  • 3

模板可以让程序泛化,而不必在乎类型,这是思考的基本出发点。但是我们却忘了模板还有一个最最基础的作用,那就是类型推断。比如函数模板,不用指定类型,使用最佳匹配。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<decltype(x)>; 
    T copy{x}; 
    T::static_function(); 
    using Iterator = typename T::iterator; 
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • C++20之后:
auto func = []<typename T>(vector<T> vec){ 
    T copy{x}; 
    T::static_function(); 
    using Iterator = typename T::iterator; 
}
  • 1
  • 2
  • 3
  • 4
  • 5
简化完美转发类型推断
  • C++20以前:
auto func = [](auto&& ...args) { 
    return foo(std::forward<decltype(args)>(args)...); 
}
  • 1
  • 2
  • 3
  • C++20之后:
auto func = []<typename …T>(T&& …args){ 
    return foo(std::forward(args)...); 
}
  • 1
  • 2
  • 3

3.支持初始化捕捉

  • C++20以前:
template<class F, class... Args> 
auto delay_invoke(F f, Args... args){ 
    return [f, args...]{ 
        return std::invoke(f, args...); 
    } 
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • C++20之后:
template<class F, class... Args> 
auto delay_invoke(F f, Args... args){ 
    return [f = std::move(f), args = std::move(args)...](){ 
        return std::invoke(f, args...); 
    } 
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

指定初始化(Designated Initializers)

struct Data { 
    int anInt = 0; 
    std::string aString; 
}; 
Data d{ .aString = "Hello" };
  • 1
  • 2
  • 3
  • 4
  • 5

在很多时候,我们可能由于成员过多,不记得构造函数的元素循序,进行构造是必须再次查看对应关系才能进行初始化。现在你只要知道你想初始化的条目即可完成正确的构造。帅不帅。

船型操作符 <=>

  • 正规名称: 三路比较运算符
  • 三路比较结果如下:
- (a <=> b) < 0 // 如果 a < b 则为 true
- (a <=> b) > 0 // 如果 a > b 则为 true
- (a <=> b) == 0 // 如果 a 与 b 相等或者等价 则为 true
  • 1
  • 2
  • 3

一般情况: 自动生成所有的比较操作符(6个)

  • 用法:auto X::operator<=>(const Y&) = default;
  • 例子:
#include <compare> 
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<a); 
    } 
    friend bool operator> (const Point& a, const Point& b) { 
        return b<a; 
    } 
    friend bool operator>=(const Point& a, const Point& b) { 
        return !(a<b); 
    } 
}; 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30

高级情况:

  • 支持非默认比较:只需自定义操作符==

  • 支持部分自定义:自定义自己的某些比较操作符

  • 支持选择比较格的类型(见离散数学的定义):严序,弱序,偏序

  • 标准库类型支持 <=> 如:vector, string, map, set, sub_match, …

范围 for 循环语句支持初始化语句

C++17开始支持switch 语句初始化和if 语句初始化,现在C++20发扬风范,开始支持范围 for 循环初始化 ,其样例代码如下:

struct Foo { 
    std::vector<int> values; 
}; 

Foo GetData() { 
    return Foo(); 
} 

int main() { 
    for (auto data = GetData(); auto& value : data.values) { 
        // Use 'data’ 
    } 
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

有人可能说这就是一个语法糖,也就是能简化代码而已,没什么大差别。其实不然,这些初始化都是有作用域的,在相应的语句结束之后会自动释放内存。大多数情况下,判断变量和循环变量在操作完之后也就没有什么作用了,及时清理内存和作用域限制给我们帮了极大的忙,这也就是C++标准一直推荐加入语句内初始化的原因。

非类型模板形参支持字符串

C++支持对非类型模板参数。但是C++对其进行了限制,仅以下类型可以:

  • 整型常量/枚举类型
  • 指向对象/函数/成员变量的指针(不允许浮点数指针)
  • 对象/函数的左值引用
  • std::nullptr_t

现在C++20放松了这个限制,增加了两条:

  • 浮点数指针)
  • 某些类类型(有一些限制)

这次专门支持了字符串类型,这对于正则表达式有很大的意义,不同的pattern是不同的类型,我们就可以利用型别信息做很多事情。比如相互的替换,编译期判断正则表达式的正确性等等等。

示例

template<auto& s> void DoSomething() { 
    std::cout << s << std::endl; 
} 

auto m { ctre::match<"[a-z]+([0-9]+)">(str) }
  • 1
  • 2
  • 3
  • 4
  • 5

C++属性符

[[likely]], [[unlikely]]

  • 先验概率指导编译器优化
switch (value) { 
    case 1: break; 
    [[likely]] case 2: break; 
    [[unlikely]] case 3: break; 
}
  • 1
  • 2
  • 3
  • 4
  • 5

[[nodiscard(reason)]]

  • 表明返回值不可抛弃, 加入理由的支持
[[nodiscard("Ignoring the return value will result in memory leaks.")]] 
void* GetData() { /* ... */ }
  • 1
  • 2

日历(Calendar)功能

  • 增加日历和时区的支持
  • 只支持公历(Gregorian calendar)
  • 其他日历也可通过扩展加入, 并能和 进行交互

示例:

  • 初始化 年、月、日的方法
// creating a year 
auto y1 = year{ 2019 }; 
auto y2 = 2019y; 
// creating a mouth
auto m1 = month{ 9 }; 
auto m2 = September; 
// creating a day 
auto d1 = day{ 18 }; 
auto d2 = 18d;
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 创建完整的日期
year_mouth_day fulldate1{2019y, September, 18d}; 
auto fulldate2 = 2019y / September / 18d; 
year_mouth_day fulldate3{Monday[3]/September/2019}; // Monday[3] 表示第三个星期一
  • 1
  • 2
  • 3
  • 产生了新的时长单位, 类似于秒, 分钟, …
using days = duration<signed interger type of at least 25bits, 
                      ratio_multiply<ratio<24>, hours::period>>; 
using weeks = ...; 
using mouths = ...; 
using years = ...;
  • 1
  • 2
  • 3
  • 4
  • 5
  • 时长互相转化
weeks w{1}; // 1 周 
days d{w}; // 将 1 周 转换成天数
  • 1
  • 2

时区(Timezone)功能

标准库对为测量时间,提供了计数系统时钟。C++为了增加对时区的支持,标准库拓展提供了地理时区时钟,就和酒店里面各个地方的时间钟一样。

C++20前的系统时钟:

  • system_clock:日历时钟,可被手工调整
  • steady_clock:稳定增长的时钟,由机器tick计算而来
  • high_resolution_clock:拥有最小计次周期的时钟
  • std::filesystem::file_time_type::clock:文件系统时钟

C++20增加的时钟:

  • utc_clock:表示协调世界时,人为“协调”(闰秒),从00:00:00, 1 January 1970开始
  • tai_clock:表示国际原子时,恒定时间,从00:00:00, 1 January 1958开始。
  • gps_clock:表示GPS时间,没有闰秒,从00:00:00, 6 January 1980开始
  • file_clock:用于文件系统,它的epoch未指定。
  • local_t:表示本地(虚拟)时间,未指定时区。

关于UTC,TAI,GPS时间的背景知识,可查阅相关文档。

示例:

  • 新增system_clock相关的别名
template<class Duration> 
using sys_time = std::chrono::time_point<std::chrono::system_clock, Duration>; 
using sys_seconds = sys_time<std::chrono::seconds>; 
using sys_days = sys_time<std::chrono::days>; 
  • 1
  • 2
  • 3
  • 4
  • 日期和时间点的相互转化
system_clock::time_point t =  sys_days{ 2019y / September / 18d }; // date -> time_point 
auto yearmonthday =  year_month_day{ floor<days>(t) }; // time_point -> date
  • 1
  • 2
  • 日期 + 时间
auto t = sys_days{2019y/September/18d} + 9h + 35min + 10s; // 2019-09-18 09:35:10 UTC
  • 1
  • 时区用例
zoned_time ac_zt("Antarctica/Casey", sys_days{2021y/September/15d}+16h+45min); 
auto zt = zoned_time{ current_zone(), system_clock::now() };
const auto& tzdb = get_tzdb();
const time_zone* local_tz = tzdb.locate_zoned("America/Los_angeles");
auto ad_zt = zoned_time{ "America/Denver",  local_days{Wednesday[3] / September / 2019} + 9h }; 
auto utc_zt = zoned_time{ "Etc/UTC", ad_zt};
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

std::span

  • 头文件

  • 某段连续数据的”视图”

  • 能够读和写数据

  • 不持有数据, 不管理数据即不分配和销毁数据

  • 拷贝非常快(类似 string_view)

  • 推荐按值方式传递给函数(像内置类型,平凡类型的数据,类指针类型,一般都推荐值传递)

  • 不支持数据跨步(stride)

  • 可通过运行期确定长度也可编译器确定长度

    • 示例:
    int data[42]; 
    std::span<int, 42> a {data};   // fixed-size: 42 ints 
    std::span<int> b {data};       // dynamic-size: 42 ints 
    std::span<int, 50> c {data};   // compilation error 
    std::span<int> d{ ptr, len };  // dynamic-size: len ints
    
    • 1
    • 2
    • 3
    • 4
    • 5
  • 支持以下操作:
    • Iterators (begin, cbegin, rbegin, …)

    • front(), back(), operator[], data()

    • size(), size_bytes(), empty()

    • first(count): 返回由前n个元素组成的子段

    • last(count): 返回由后n个元素组成的子段

    • subspan(offset, count): 返回由offset到offset+count-1元素组成的子段

特性测试宏

通过它可以判断编译器是否支持某个功能, 例如

  • 语言特性

    • __has_cpp_attribute(fallthrough)
    • __cpp_binary_literals
    • __cpp_char8_t
    • __cpp_coroutines
  • 标准库特性

    • __cpp_lib_concepts
    • __cpp_lib_ranges
    • __cpp_lib_scoped_lock
    • __cpp_lib_any
    • __cpp_lib_bool_constant
    • __cpp_lib_filesystem
  • version
    • 包含 C++ 标准库版本, 发布日期, 版权证书, 特性宏等

这对于我们检查编译器特性和标准库信息十分有用,对跨平台十分重要,可以提前检验编译器和标准库是否符合当前编码要求。

常量表达式(constexpr) 的更新

constexpr的意义

constexpr(常量表达式)是为了解决C++历史遗留问题,它一种比const 更严格的束缚, 它规定了表达式本身在编译期间可知。具体来说有以下特性:

  • const是运行期常量 constexpr是编译期常量

  • const其实是readonly(只读),而constexpr才是const(常量)

  • constexpr 所修饰的函数,返回值则不一定要求是编译期常量 ==>函数返回值一定是编译时常量,且在编译期进行计算(C++20 consteval)

为了突破类类型的只读含义,C++发明了一个关键词mutable。含义是:即使处于const对象里,对象里被mutable修饰的内容仍然可变(运行期),这对const成员函数有极大的意义,在此就不展开了。此时,就更能理解只读的含义了:constexpr是一定全部不可以变的,这才是真正的常量。编译期常量有什么好处呢?函数可以在编译期就进行计算,减少运行期的开销(还记得开头C++之父的目标么),第二个就是可以在元编程世界里大显身手。

constexpr函数你可以理解成一个只于输入有关的函数,也就是说输入相同的参数,结果必定相同。相当于Python的一等函数,或者说闭包函数。(可能描述不准确)。

在C++11标准中,不允许常量表达式作用于virtual的成员函数,因为virtual表示的是运行时的行为,这与constexpr“可以在编译时期进行计算”的意义是冲突的。“在编译时期进行计算”是对编译器的一个建议,C++11标准并没有强制要求编译器一定要在编译时期对常量表达式函数进行计算。要求函数返回值一定是编译时常量,且在编译期进行计算,C++20增加了consteval。也正是有了consteval,C++20放松了constexpr修饰的要求。

constexpr 修饰函数

C++11:

  • constexpr函数的返回类型以及所有形参必须是字面值类型
  • constexpr函数必须有且只有一条return语句
  • 但是constexpr函数可以返回一个非常量
  • constexpr函数定义建议放在头文件中,但是不强制要求

C++14:

  • constexpr函数可以使用分支控制语句,拥有多个返回语句
  • constexpr函数可以使用循环控制语句
  • 可以修改生命周期和常量表达式相同的变量了

C++17:

  • constexpr可以修饰lambda表达式
  • constexpr可以修饰if语句 => if constexpr

C++20:

  • constexpr 虚函数
    • constexpr 的虚函数可以重写非 constexpr 的虚函数
    • constexpr 虚函数可以重写 constexpr 的虚函数
  • constexpr 函数可以:
    • 使用 dynamic_cast()typeid
    • 动态内存分配
    • 更改union成员的值
    • 包含 try/catch
      • 但是不允许throw语句
      • 在触发常量求值的时候 try/catch 不发生作用

constexpr string & vector

  • std::stringstd::vector 类型现在可以作为 constexpr
  • constexpr 支持反射做准备

注释:

。。。

consteval 函数

constexpr 函数可能编译期执行, 也可以在运行期执行。 consteval 只能在编译器执行, 如果不满足要求编译不通过。

constinit

强制指定以常量方式初始化,这就可以放在编译期初始化了。注意,这只要求了初始化,但没有要求只读。如果要求只读就和constexpr一样了,如果要求可变,好像和普通的变量没什么区别。哦,那编译期初始化了他会放在哪呢?它的生命周期又是多少呢?这样一想,是不是有点像static了!!!

哈哈哈,现在以const开头有关的关键字的特性都掌握了把,是不是有些混乱?这个时候我们就要从两个维度来看问题了,一个是初始化实在什么时候,另一个是可不可以改变,具体结论在下图,慢慢悟吧:

在这里插入图片描述

用 using 引用 enum类型

为了提高枚举类型的安全性和数据类型指定性(继承内置整数类型),防止命名冲突,进而引入了emun class来对枚举进行作用域限制。但是代码中必须标明类型名,这样很不方便。因此,C++20使用了与命名空间,类内类型一致解决方法:using。但是为了减少暴露、防止入侵式代码,建议在尽可能小的作用域打开枚举。

示例

enum class CardTypeSuit { 
    Clubs, 
    Diamonds, 
    Hearts, 
    Spades 
}; 

std::string_view GetString(const CardTypeSuit cardTypeSuit) { 
    switch (cardTypeSuit) { mingmin222
    case CardTypeSuit::Clubs: 
        return "Clubs"; 
    case CardTypeSuit::Diamonds: 
        return "Diamonds"; 
    case CardTypeSuit::Hearts: 
         return "Hearts"; 
    case CardTypeSuit::Spades: 
         return "Spades";henbufangbian
    } 
} 
std::string_view GetString(const CardTypeSuit cardTypeSuit) { 
    switch (cardTypeSuit) { 
        using enum CardTypeSuit; // 这里是关键
        case Clubs: return "Clubs"; 
        case Diamonds: return "Diamonds"; 
        case Hearts: return "Hearts"; 
        case Spades: return "Spades"; 
    } 
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28

格式化库(std::format)

这是一个十分类似于Python 的字符串格式化。github上有一个非常经典优秀的实现库:https://github.com/fmtlib/fmt

示例

std::string s = std::format("Hello, world!\n");       // s == "Hello, world!\n"
std::string s = std::format("The answer is {}.", 42); // s == "The answer is 42."
std::string s = std::format("I'd rather be {1} than {0}.", "right", "happy");    // s == "I'd rather be happy than right."
std::vector<int> v = {1, 2, 3}; std::string s = fmt::print("{}\n", v);           // s == "[1, 2, 3]"
// 当然还支持各种格式化输出如:时间,日期, 年月
// 还支持各种输出流指定比如文件,控制台,数据库
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

增加数学常量

  • 头文件
  • 包含 e, log2e, log10e pi, inv_pi, inv_sqrt pi ln2, ln10, sqrt2, sqrt3, inv_sqrt3, egamma
  • 他们的精度很高,我们再也不需要自己去查找定义这些数值了

std::source_location

  • 用来代替__FILE__、__LINE__、__FUNCTION__、__DATA__、__TIME__、
  • C++类进行封装,这下好处可大了,真的是太好了,越来越C++了!
  • 构造用source_location::current()
  • 获取的是调用方所在位置

他们主要是用于来获取代码位置, 对于日志和错误信息尤其有用!

示例

void LogInfo(string_view info, const source_location& location = source_location::current()) 
{
  cout << location.file_name() << ":" << location.line() << ": " << info << endl;
}

int main() 
{
  LogInfo("Welcome to Cpp");
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

位运算

加入循环移位, 计数0和1位等功能

  • endian 指示标量类型的端序

  • bit_cast: 类型转化

  • has_single_bit(): 检查一个数是否为二的整数次幂

  • bit_ceil(): 寻找不小于给定值的最小的二的整数次幂

  • bit_floor(): 寻找不大于给定值的最大的二的整数次幂

  • bit_width(): 寻找表示给定值所需的最小位数

  • rotl(): 计算逐位左旋转的结果

  • rotr(): 计算逐位右旋转的结果

  • countl_zero(): 从最高位起计量连续的 0 位的数量

  • countl_one(): 从最高位起计量连续的 1 位的数量

  • countr_zero(): 从最低位起计量连续的 0 位的数量

  • countr_one(): 从最低位起计量连续的 1 位的数量

  • popcount(): 计量无符号整数中为 1 的位的数量

    注释:

    • 左右旋转:又称循环移位。

    • 左旋转:二进制左移的时候,低位不用0填充而用高位溢出的部分填充。

    • 右旋转:二进制右移的时候,高位不用0填充而用低位溢出的部分填充。

一些小更新

  • 字符串支持 starts_with, ends_with
  • map 支持 contains 查询是否存在某个键
  • list 和 forward list 的 remove, remove_if 和 unique 操作返回 size_type 表明删除个数
  • 增加 shift_left, shift_right
  • midpoint 计算中位数, 可避免溢出
  • lerp 线性插值 lerp( float a, float b, float t ) 返回 a+t(b-a)
  • 新的向量化策略 unsequenced_policy(execution::unseq)

这些小的更新很有用也很容易用。

如果文章有用,欢迎点赞、打赏、转发。最重要的还是要谢谢大家的支持,我会一如既往地推送深度好文…

替精神支柱大猫仔化缘了,喵~
声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/AllinToyou/article/detail/208377
推荐阅读
相关标签
  

闽ICP备14008679号