你好,我是卢誉声。

上一讲,咱们理解了后续C++规范演进中,极有可能到来的个性或库变更——动态反射、异步工作框架、网络库和Freestanding库。

从将来规范的演进路线中,咱们其实能够一窥到底,不难发现,C++有着极为清晰的演进路线。

与此同时,C++规范还在致力补救不足之处,比方不足标准化的高性能计算能力、异步编程库、针对大数据处理的Ranges扩大等等。接下来,就让咱们持续漫游之旅,畅想将来C++规范演进可能迎来的另外六个变动吧。

高性能计算反对

对于C++26来说,另一个重要指标是提供对高性能计算的反对。

不得不说,C++至今仍然能焕发活力,得益于人工智能等畛域对高性能计算的需要,C++简直是惟一一个同时正当兼顾性能和形象两个层面的编程语言。

然而,C++不足对高性能计算的规范反对,无论是根底的多维向量、对CPU SIMD的指令封装,还是更高层的线性代数算法都是一片空白。

因而,从C++20规范开始,一直有相干个性增加到规范库中,C++20中的一维数组视图span和C++23中的多维数组视图mdspan,都是线性代数的根底类型向量的前置个性。同时,C++23的多元索引操作符,也让多维向量拜访变得更加不便了。

在C++26中,预计增加许多要害的线性代数反对。

首先,可能会退出的就是多维数组mdarray。多维数组mdarray的设计和C++23中的多维数组视图mdspan是统一的,都能够用于示意多维数组。我在这里着重阐明它们之间的差别。

另一个C++26可能反对的个性是提供SIMD指令的封装。

在高性能计算中,SIMD(单指令多数据流)是常见的计算优化解决模式,各个CPU指令集都有相应指令提供反对,其指标是通过一条CPU指令计算多个多维向量。

目前C++编译器可能会在局部的计算优化中,主动应用这些指令。然而,在高性能计算中,咱们往往须要手动指定应用这些指令。因为规范库没有提供这些接口,导致咱们只能应用汇编指令或者相似Intel的Intrinsics接口来实现,常常须要做大量的平台适配工作。

为了解决这个问题,C++打算引入针对SIMD类型与指令的封装——这也是后续线性代数接口实现的计算根底。

最初,C++26可能会基于mdarray、mdspan提供线性代数函数库。

因为BLAS(Basic Linear Algebra Subprograms)库根本曾经成为C/C++中线性代数的事实标准,CBLAS、ATLAS和OpenBLAS等基于BLAS接口的库,也成了大部分科学计算库的基础设施(比方Python驰名的NumPy就是基于BLAS开发的)。

因而,C++的线性代数接口也就以BLAS接口为根底设计。C++的BLAS接口会应用mdspan形容向量数据类型,可能应用SIMD实现局部计算减速。至于与BLAS同时设计的LAPACK,尽管补充了更多线性代数性能,但因为各种起因可能要留待当前的C++规范决定何时实现。

Coroutines扩大

C++20中的Coroutines只提供了一套协定,具体实现须要开发者本人来实现。而C++23里的generator提供了在生成器这种场景下的coroutine规范实现。

而在C++26中,咱们终于能够看到面向一般协程工作的coroutine实现——std::lazy。

那么什么是std::lazy呢?咱们能够联合这段代码来了解。

#include <experimental/lazy>#include <string> struct Person {    int id;    std::string name;}; std::lazy<Person> loadPerson(int id);std::lazy<> savePerson(Person p); std::lazy<void> modifyPerson() {    Person person = co_await loadPerson(1);    person.name = "学生名称";    co_await savePerson(person);}

通过代码能够看到,自定义的coroutine换成lazy之后,咱们不再须要本人实现协程的调度过程,间接通过co_await就能调用loadPerson和savePerson,这可太棒了!

std::lazy甚至能够容许咱们指定具体的executor调度协程,比方前面的代码。

co_await f().via(e);

这里的e就是一个executor,这样咱们能够管制协程到底要通过哪个executor来唤醒执行,提供了更高的灵活性和更简略的接口。

说白了,std::lazy就是一个通用协程工作封装,起了这么一个名字,是因为规范委员会的SG1偏向于将task这个名字保留给当前的其余概念应用,最初LEGW(Library Evolution Work Group,即规范委员会负责库改良的工作组)就选用了lazy这个名字……

Ranges扩大

到C++23为止,Ranges曾经相当欠缺了,惟一问题就是咱们无奈像Python、JavaScript和Rust一样,不便地将序列(包含vector、map和set)打印到管制台上。

在C++26中,可能会提出容器和ranges的格式化接口计划,也就是咱们能够写出这种代码:

#include <ranges>#include <string>#include <vector>#include <cstdint>#include <fmt/ranges.h> int main() {    std::vector<int32_t> v{        1, 2, 3    };    fmt::print("{}\n", v);     std::string s = "xyx";    auto parts = s | std::views::split('x');     fmt::print("{}\n", parts);    fmt::print("<<{}>>\n", fmt::join(parts, "--"));     return 0;}

能够看出,相比原来的模式,这样的代码无论是调试代码,还是将容器ranges输入到文件中,编码都会更加不便。

Hive:Bucket Array容器框架

现在,C++仍然是高性能交易与游戏编程的外围撑持,在这些畛域中,常常须要实现大量数据块的申请、开释与疾速检索,咱们常常采纳一种名为 Bucket array的技术来解决此类问题。

这种数据结构的原理是,将一个元素数组划分成块的数组,每个块蕴含多个理论的元素,每个元素有一个布尔标记位,示意这个元素是否被删除了。当遍历元素时,会跳过这些被标记删除的元素,只有一个块中的所有元素被标记删除后,这个块才会被真正开释。这么做,能够防止遍历时遇到全空的块。插入元素时,所有块都满了才会申请新的块。

这种形式大幅度缩小了小对象的内存申请(都以块为单位申请内容),防止过多零散的删除操作引起的移位拷贝,所以性能损耗大大降低。而且,还能确保很多相干元素间断存储在一个块中,保障很多场景下随机拜访的性能。能够说,这是一种在特定场景下衡量了插入、删除和检索性能的数据结构。

因为在C++的次要反对场景中,会大量用到这种数据结构,因而C++规范提出了相干提案,可能会在C++26中正式纳入规范。

多线程无锁内存模型反对

基于多线程的高并发业务也是C++的重要利用场景,尤其在当初的高并发Web服务中有大量利用。

在这种场景中,性能的外围瓶颈可能并非长时间的计算,而是继续的高并发拜访。这种状况下,多个线程可能须要频繁地同时拜访雷同数据,天然就须要解决高并发场景下的数据竞争问题。

如果咱们采纳罕用的锁来解决问题,极其状况下会将本可并行的执行流变成串行执行流,重大拖慢并发性能。

为此,有针对不同场景的大量“无锁”内存模型,Hazard Pointer就是一种面向“单写者、多读者”场景的无锁内存模型与根底数据结构。

你无妨联想一下,在很多Web服务中咱们可能都会采纳相似“读写拆散”的机制来解决数据的读写,Hazard Pointer的原理也是一样的。这种数据结构的特点是,永远只有一个线程具备其所有权,并能够写入数据,其余线程只能拜访并读取其中的值,所以咱们用这种个性做无锁化的优化十分不便。

为了反对Hazard Pointer,C++采纳了RCU(read copy update)实现了一种面向多读少写个性的链式数据结构,也就是咱们熟知的“无锁队列”。

RCU联合Hazard Pointer为咱们提供了一种“单写者、多读者”的多读少写的高并发无锁内存模型,能够为咱们解决特定的高并发业务提供基础设施反对。

定制点对象调整

最初咱们聊聊定制点对象,这是为了解决ADL(Argument-Dependent Lookup,实参依赖查找)问题提出来的。

实质上,引入它是为了让用户能不便地应用命名空间中的符号。咱们还是联合代码示例了解。

#include <ranges>#include <string>#include <vector>#include <cstdint>#include <iostream> namespace cp {    struct Person {        friend void swap(Person& lhs, Person& rhs);        int32_t id;        std::string name;    };     void swap(Person& lhs, Person& rhs) {        std::cout << "Person swap" << std::endl;         Person temp = lhs;        lhs = rhs;        rhs = temp;    }     std::ostream& operator<<(std::ostream& os, const Person& person) {        os << person.id << " " << person.name << std::endl;         return os;    }}  int main() {    cp::Person p1{        .id = 1,        .name = "姓名1"    };     cp::Person p2{        .id = 2,        .name = "姓名2"    };     std::cout << p1 << std::endl;    // 通过p2找到cp::operator<<    operator<<(std::cout, p2) << std::endl;     // 通过p1,p2找到cp::swap    swap(p1, p2);     cp::operator<<(std::cout, p1) << std::endl;     return 0;}

能够看到,在代码第8行定义了Person类,第14行定义了swap函数,第22行定义了<<的操作符重载。这些符号都被定义在命名空间cp中。

当初来看一下代码41和43行,咱们都晓得其实C++的操作符重载只是一个语法糖,std::cout << p1的实质相当于operator<<(std::cout, p1),所以43行的用法没有什么问题。

然而,代码46行就很奇怪了,咱们明显没有using cp::swap,也没有using namespace cp,为什么这里C++能找到cp::swap这个函数呢?

命名空间是为了解决名称隔离的问题引入的,但也让咱们援用符号变得比拟麻烦。比如说,咱们为了防止命名空间净化,基本上不会应用using namespace,而是在每次援用符号时,都加上残缺的命名空间(比方应用cp::swap)。

不过有些问题,咱们必须要交给编译器来解决。比如说,代码中的operator<<重载。既然std::cout << p1只是operator<<(std::cout, p1)的语法糖,而operator<<(std::ostream&, const Person&)是在cp这个命名空间中定义的函数,那么编译器要怎么找到cp:: operator<<这个函数呢?

为此,C++提出了ADL。简略来说,就是调用函数时,只有有一个参数的类型属于函数所在的命名空间,那么调用的时候就不必加命名空间前缀(当然理论状况必定简单得多......)。

这样一来,编译器就能够通过operator<<(std::cout, p1)将std::operator<<或者cp::operator<<作为候选符号,最初匹配满足调用条件的版本。

这个能力,最终在C++规范中从操作符重载被扩大到了一般函数,就变成了咱们当初所知的ADL,这也解释了为什么代码46行可能编译胜利。

不过,这种个性在后续过程中被“非法”使用到了C++的其余局部,来实现用户自定义的扩大(想想模板元编程是怎么呈现的)。比方C++20 Ranges中的begin、end等函数,都须要相似的个性。但这种场景下应用ADL并不在语言设计的预期中,天然会存在各种各样的问题。

因而,C++在规范中一直对它修修补补。CPO(定制点对象)就是其中一种。你能够这样了解,C++给咱们提供了一个以特定对象作为扩大点,让开发者能够针对本人的类型定制对应的行为,这就是为什么叫做定制点。

不过CPO仍然存在很多问题,在筹备推出的execution中,就有很多符号须要此类反对以供第三方定制扩大。因而,C++原本筹备引入tag_invoke来缓解这个问题,但这只不过又是一个给ADL收烂摊子的计划。

所幸,因为execution个性提早,规范委员会才有机会在tag_invoke这种治标不治本的计划进入规范之前,在C++26之后(大概率在C++26),彻底消除在扩大点中应用ADL带来的问题,最终齐全对立扩大点的技术计划。

总结

人工智能技术倒退和高性能计算畛域,既须要靠近硬件层,又须要提供下层接口。惟一一个兼顾性能和形象两方面的编程语言,简直只有C++。

不过,尽管C++具备如此素质,但规范库统一不足对这些畛域所依赖的底层技术的反对。因而工程师们被迫引入大量第三方库,但集成过程里的问题(比方不同库之间的适配问题、体系结构、操作系统的兼容性问题)也是层出不穷,令人头大。

现在,从C++11再到C++20以及后续演进规范,咱们能分明看到C++的扭转以及演进路线,C++规范将提供更多“标准化的解决方案”,这些倒退无望大幅升高开发者编写代码的复杂度。

由此,咱们有理由揣测,之后C++在机器学习和高性能计算等重要应用领域中,会大放异彩,越战越勇。

咱们在课程中曾本人实现了简直所有的C++ Coroutines接口约定。然而能够看到,在后续的规范演进中,coroutine库行将到来,届时,咱们就能够通过std::lazy来彻底简化异步编程的工作。这可太让人期待了。

最初,Ranges针对format的扩大也让代码调试与容器输入变得更加不便。

这些新个性的提出和对现有规范中的个性的补足,将会把C++推向一个全新的高度。C++26或其后续规范,将是一个极为令人激动的重大变更。让咱们一起期待它们的到来,以及编译器对新规范的反对吧。

课后思考

最初给你留两道思考题。

1.在人工智能畛域,C++无处不在。那么,你能说出哪些技术或工具在应用C++呢?

2.我在这一讲中提到了名为Bucket array的技术,用于解决内存碎片的问题。那么,你在日常工作中,是如何防止内存碎片导致的性能降落问题呢?

欢送说出你的实际,与大家一起分享。咱们一起探讨,下一讲见。

文章起源:极客工夫《古代 C++20 实战高手课》