共计 5383 个字符,预计需要花费 14 分钟才能阅读完成。
你好,我是卢誉声。
在上一讲中,咱们探讨了 C ++23 带来的变动。因为 C ++23 曾经是解冻个性,所以咱们探讨得十分具体。C++23 作为“更好的 C ++20”,其本质是针对 C ++20 进行改良和修补,所以涵盖的内容比拟无限。
然而,作为继 C ++20 之后的又一重大规范变更,C++26 及其后续演进将会给咱们带来诸多重量级个性。为了更好地了解 C ++ 规范的演进思路、把握 C ++ 规范演进的底层逻辑,并一窥将来的变动,这一讲中,咱们把视角转向 C ++26 及其后续演进。
不过,因为 C ++26 还处在提案阶段。所以,咱们只能预测一下那些“大概率会进入 C ++26”的个性,兴许其中一些个性会被推延或产生扭转,但并不影响咱们剖析 C ++ 规范演进的底层逻辑和将来。
好,就让咱们从动态反射开始明天的内容。
动态反射
动态反射(static reflection),很有可能是 C ++26 中行将引入的最为重量级的个性。但但凡理解何为反射机制,同时相熟 C ++ 的人,看到这个个性时,预计会虎躯一震——什么?C++ 要反对反射?
然而咱们须要沉着一下,这个个性的定语很重要,这个反射是“动态的”(static)。
对于反射的概念,还是很容易了解的,就是编程语言提供一套机制,帮忙开发者在代码中获取类型的相干信息,比方类型名称、大小、类的成员、函数参数信息等等。它容许开发者在代码中,依据反射信息执行相应的操作,这让语言变得更加“动静”——也就是依据反射信息来确定代码如何执行。
其实早在 C ++98 规范,在引入 dynamic_cast 和 RTTI 时,C++ 就容许开发者获取无限的运行时类型信息。然而,这些运行时类型信息十分贫乏,除了反对类型转换以外,并没有什么其余用途。而且,即使是这些无限的性能,也特地消耗 C ++ 的运行时资源,与 C ++ 本身的设计理念有些偏差。
不过,C++ 新提出的动态反射则大不相同。这个个性秉持了古代 C ++ 一以贯之的理念,在编译时获取并确定所有信息。所以说,这个动态反射也就和 C ++11 之后引入的 type_traits 一样,能够在编译时获取所有的信息。并在编译时,通过模板和 constexpr 等形式实现相干计算。
动态反射规范的个性尚处于探讨阶段,不过曾经有相干的 TS(Technical specifications,即技术规范)。因而,咱们来理解一下,在 TS 中是如何应用动态反射的?
首先,C++ 会提供一个新的关键字——reflexpr,用于获取一个符号的“元数据”。咱们能够联合前面这个例子来了解。
#include <cstdint>
#include <experimental/reflect>
int main() {
int32_t num = 0;
using NumMeta = reflexpr(num);
return 0;
}
你不必尝试运行这个例子——目前临时还没有编译器反对它。咱们关注点就在代码第 7 行,通过 reflexpr 获取了 num 的元数据。
咱们须要晓得,reflexpr 返回的元数据并不是一个变量,而是一个类型,所以要用 using 或者 typedef 来为其定义一个别名,这样能力在前面应用。
那么获取到的类型别名要如何应用呢?咱们持续看代码。
#include <string>
#include <iostream>
#include <experimental/reflect>
int main() {using Type1 = reflexpr(std::string);
using Type2 = reflexpr(std::u8string);
std::cout << std::experimental::reflect::get_name_v<Type2> << std::endl;
static_assert(Type1 == "basic_string");
std::cout << std::experimental::reflect::reflects_same_v<Type1, Type2> << std::endl;
return 0;
}
获取到类型别名后,咱们就能够应用 C ++ 提供的 reflect 工具函数,动态地获取对于类型别名的所有信息,比方代码第 9 行通过 get_name_v 获取了 Type2 的类型名称,第 12 行通过 reflects_same_v 判断两个类型是否雷同。
因为动态反射的类型信息都存储在类型中,因而,reflect 的工具都能够实现成工具类型或 constexpr 函数。所以,咱们也能够在 static_assert 和模板参数中,配合 C ++ Concepts 个性,通过模板实现针对不同类型细节执行不同的操作逻辑。
事实上,正是因为动态反射须要基于 constexpr、static_assert 和 concept 实现。因而,直到 C ++26 之后,动态反射才可能成为备选的提案。
基于这些基础设施,C++ 的编译时计算会变得前所未有的弱小。
此外,动态反射的 TS 还提供了面向反射元数据类型的一系列 concept,这样咱们就能够通过定义模板函数,实现更多的反射元数据的计算与判断。
能够说,如果 C ++26 及其后续演进真的实现了动态反射 TS,C++ 的编译器“动静”个性就根本完满了。
异步工作框架
从 C ++11 开始,古代 C ++ 始终在试图扩大、欠缺并发工作治理,从根底的 thread 反对,到 future、promise、async、并行算法,到 C ++20 的协程,都在逐步完善 C ++ 规范库的并发工作反对。
C++11 提供的 thread 解决了基于线程的并发工作的根底设置,通过 atomic 解决了细粒度的原子操作问题,通过 mutex 和信号量解决了线程同步问题,通过 promise、future 和 async 解决了并发工作的创立与根本调度问题。
但直到 C ++20 为止,咱们仍然须要关注很多并发工作执行的细节问题,无奈通过规范库解决并发工作的高层调度问题。比方前面这些问题。
- 如何管制同时执行的并发工作数量。
- 如何解决工作的谬误重试机制。
- 如何解决多个异步工作之间的串行、并行甚至条件调度。
- 如何更不便地在两个并发工作中发送接管音讯等等。
C++ Executors 的指标就是解决这些问题,咱们这就来说说 executors 中的概念与提供的能力。
第一个概念就是 executor。
executor 实质是一个 concept,示意能够被 execute 和 schedule 等调度函数调用的类型,它能够是一个函数、仿函数,也能够是一个 Lambda 函数。
对于其余调度函数,它们通过调用 executor 来提交并发工作。如果说,咱们须要通过线程池来执行工作,那么能够创立一个线程池对象,并从线程池对象中获取一个 executor。
#include <string>
#include <iostream>
#include <execution>
int main() {
using namespace std::execution;
std::static_thread_pool pool(4);
executor auto poolExecutor = pool.executor();
execute(poolExecutor, [] {std::cout << "这是一个在线程池中执行的工作";});
return 0;
}
在代码第 8 行,定义了一个大小为 4 的线程池。接着,通过 executor 成员函数获取了一个能够在线程池中执行并发工作的 executor。而后,调用 execute 函数,execute 会调用 poolExecutor 将这个 Lamba 函数提交到线程池中执行。
这种状况下,executor 帮咱们屏蔽了提交并发工作的所有细节,为其余的任务调度函数提供通用的调度接口。当然 executor 只是一个形象,所以底层实现并不一定是线程——咱们同样能够将 coroutine 包装成 executor,因而 executor 是一个通用的并发工作接口。
第二个重要的概念是 sender/receiver。
尽管规范定义了通用的 executor。然而,用于调用 executor 执行工作的 execute 函数,它的返回类型为 void。因而,咱们无奈通过链式调用的模式将多个并发工作串联在一起执行,就更不用说实现一些更高级的并发工作连贯了。
为此,规范提出了 sender 和 receiver。sender 是一个创立之后不会主动执行的调度工作,须要等到在它前面连贯一个 receiver 之后,才会开始执行。当 sender 执行实现后,就会调用 receiver 约定的接口将数据传递给 receiver,并开始执行 receiver,规范中将 receiver 连贯到 sender 后的函数就是 connect,伪代码如下所示。
#include <string>
#include <iostream>
#include <execution>
int main() {sender auto snd = create_sender();
receiver auto rec = create_receiver();
std::execution::connect(snd, src);
return 0;
}
那咱们要如何实现链式调用呢?这时就须要引入 通用异步算法 这个概念了。
所谓通用异步算法,就是一个接管用户自定义工作作为 sender 的函数,该函数会调用 connect 将该 sender 与算法外部的一个 receiver 连贯,而后包装成一个新的 sender 返回给调用方,这样调用方就能够将这个 sender 传给下一个通用算法或者 connect 其余的计算工作,最终造成链式调用。规范库中 future/promise 的 then 就是通过这种形式实现的。
最初一个重要的概念是 scheduler。
咱们可能常常会碰到一种状况,就是有多个并发工作,应用雷同的 sender。如果间接链接 sender 对象和多个工作。很有可能会产生竞争问题。
为此,C++ 规范提出了 scheduler。它次要通过 schedule 函数返回,该函数会返回一个新的 sender 作为外部 sender 的工厂,这样即便将同一个 scheduler 连贯到多个 receiver,也不会引发数据竞争问题。
网络库
接下来,我为你介绍一个“有些可能”推出的规范网络库——networking。我为什么说“有些可能”呢?这是因为规范网络库曾经在规范中,被推延了无数次😂。不过,我还是心愿它能呈现在 C ++26。
在理论工程项目中,网络编程曾经是不可或缺的一部分。但十分惋惜,C++ 始终没有将网络反对标准化。
事实上,C++ 始终将 networking 安顿在标准化的过程中,原定应该在 C ++17 和 C ++20 之间增加到规范库中,不过因为各种起因当初也就提早到至多 C ++26 了。
目前网络库的整体设计基于 Boost 实现的 ASIO 库,我为你梳理了一张表格,不便你理解 C ++ 网络库的整体设计。
从表格中能够看出,每个概念都定义在对应的头文件中,最终被蕴含在 <experimental/net> 中。等到网络库齐全标准化后,就会被移出 experiment 成为正式的头文件。Boost ASIO 曾经十分成熟,网络库的更多细节曾经具备大量材料,如果你想理解更多,课后能够自行搜寻。
尽管网络库的设计基于 Boost 的 ASIO,但因为古代 C ++ 曾经提供了大量的基础设施,包含算法、并发模型、I/ O 流、协程等,这也是为什么网络库的标准化工夫在一直提早,毕竟它的基础设施的标准化优先级必定是更高的。
最初,咱们只能冀望网络库不要再延期了,这样对于 C ++ 的新开发者来说,在解决网络编程时能够大幅度降低门槛和编程复杂度。
Freestanding 反对
最初,咱们再聊聊 Freestanding 库。家喻户晓,C++ 可能使用于嵌入式等环境开发,在这些环境中能够使用的资源可能非常少,而如果残缺实现 C ++ 的规范库可能须要许多零碎调用或者资源撑持,这在很多嵌入式环境中是不可能满足要求的。
因而,C++ 提出了 Freestanding 库,也就是在无需操作系统调用和存储空间耗费的前提下,须要实现的规范库最小子集。
从 C ++11 到 C ++26 中规范库有了大幅更新,那么天然 C ++26 中 Freestanding 库的要求也就会有极大补充,预计这不是大多数人须要关怀的问题,这里就不开展了。如果你感兴趣,能够课后自行搜寻理解更多细节。
总结
这一讲中,咱们一起探讨了 C ++26 及其后续演进的前四个猜测,最重要的是上面列出的这三个。
- 动态反射:配合 constexpr、static_assert 和 concept 一起,成为动态反射的外围基础设施,让编译时计算成为支流技术的要害补充。
- 异步工作框架:通过 executor、sender/receiver 和 scheduler,管制同时执行的并发工作数量、解决工作的谬误重试机制、解决多个异步工作之间的串行、并行甚至条件调度,更不便地在两个并发工作中发送接管音讯。
- 网络库:在规范中推延数次的库变更。在解决网络编程时,能够大幅度降低门槛和编程复杂度。
下一讲,咱们持续畅想 C ++ 的将来变动,敬请期待。
课后思考
咱们在这一讲中重复提到一个词,即 TS 技术规范。请你浏览无关技术规范方面的定义,尝试了解 C ++ 规范委员会是如何应用技术规范将次要个性变更和规范制订过程“解耦合”的。
欢送聊聊你的想法,并与大家一起分享。咱们一起交换,下一讲见!
文章起源:极客工夫《古代 C++20 实战高手课》