think-cell

旅行报告:美国圣路易斯 ISO C++ 夏季会议

 

两周前,我参加了在美国圣路易斯举行的 ISO C++ 标准化委员会 2024 年夏季会议。这是即将推出的 C++26 标准的第四次会议。有关所有取得进展的论文的概述,请查看 reddit 上的合作之旅报告。像往常一样,我把大部分时间都花在了库演进上,在那里我们讨论 C++ 标准库的提案,我还主持了 SG 9,这是一个为提案提供早期反馈的研究组。这一次,我还在 SG 23 度过了一整天,这是一个专注于安全和安保的研究小组。我参与的大多数讨论都是没有争议的(或者非常有争议的,但最终不像命名那样重要),但我对 C++ 中的发送者/接收者、反射和借用检查有一些想法。std::ranges

发送方/接收方

P2300 是一个大提案,它增加了一种编写并发程序的新方法。就像改变了我们编写范围算法的方式一样,也将改变我们编写并发代码的方式。这个想法是将 senders(本质上是最终会发送一些值的 lazy future)和 receivers(只是花哨的回调)组合在一起来描述数据流。然后,可以使用特殊发送方将执行转移到不同的线程或将它们调度到线程池中。就像组合视图一样,组合 sender 是纯粹的声明性的,并且仅在需要值时才开始执行。这样,您就可以构建执行图,而无需担心同步,并在以后执行它。这是一个绝妙的设计。std::rangesstd::execution

只是细节涉及很多复杂性。

作为一个典型的 C++ 提案,它的目标是成为每个用例的 100% 解决方案。这需要一大堆自定义机制来采用特定执行上下文的算法,大量的元编程来计算完成签名,以及许多新概念和实用函数。如果最终用户想要编写现有发送者,则使用运算符或编写协程并创建发送者非常简单,但下面的实现很复杂,编写自己的发送者并非易事。与 类似,这可能会导致更长的编译时间,优化器需要做更多的工作来消除所有这些抽象,并且如果您做错了什么,则会出现不可读的错误消息。|co_awaitstd::ranges

我不喜欢的一个特别复杂是环境的概念。发送方可以有一个关联的环境,该环境实质上是以命名属性元组的形式进行的编译时键值存储。发件人可以向环境添加新属性或查询现有属性,而组合会保留这些更改。这样,您就可以实现某种形式的依赖关系注入。假设您有一个第三方库,该库实现了基于发送者的算法。你可以通过与你自己的发送者一起编写它来使用它,发送输入值,例如从文件中读取它们:

 
std::execution::sender auto some_algorithm(std::execution::sender auto input); // third-party
std::execution::sender auto read_file(std::string_view path); // your own code

int main() {
	auto input = read_file(path);
	auto value = some_algorithm(input);
	...
}
组合两个发送者以构建算法

假设两者都需要分配内存。既然你自己写了,你就用你花哨的分配器来做这件事,并且也想做这件事。这可以使用环境来完成:如果将分配器放入环境中,则 的实现可以在环境中查询分配器并使用分配器(如果提供)。这样,您可以自定义行为,而无需额外的参数来 ;它们由较早的发送者注入。如果参数很多,那就很方便了。read_filesome_algorithmread_filesome_algorithmread_filesome_algorithmsome_algorithm

此外,该环境还可用于查询有关当前发送者链本身的信息。例如,在线程池上安排工作的发送方可以将线程池放在环境中。这些计划的发件人可以访问它,以便在他们正在执行的同一池中安排更多工作。在某种程度上,环境等同于发送方/接收方。thread_local

但是,环境也引入了依赖于环境的发件人的概念。主要示例是 。这是从环境中读取指定属性并发送其值的发送程序。它发送的值的类型是什么?嗯,这取决于所讨论的环境。因此,一般来说,在编写完所有发件人并准备好执行工作之前,不知道完整的完成签名。这可能发生在离合成的实际位置很远的地方。假设我们不小心将 a 与实现中的 allocator 属性相关联,而不是 .但是,的实现需要一个分配器,因此在读取属性时,它会获取错误的类型,这将导致编译器错误。仅当我们在 中组合它们时,才会检测到此错误。如果我们使用第四个位置的环境将来自不同库的两个发送者组合到第三个库中,则可能需要进行大量挖掘才能找出到底出了什么问题。std::execution::read_env(property)std::pmr::memory_resource*read_fileAllocatorsome_algorithmmain

在圣路易斯,Eric Niebler 发表了 P3164,这是一篇很棒的论文,旨在改进误用发送方/接收方时的编译器错误消息。处理依赖于环境的发件人必须经历很多麻烦,这让我怀疑环境是否真的值得。

除了发送方/接收方固有的复杂性外,还存在程序问题。P2300 是一篇庞大而复杂的论文,有很多值得信赖的作者。有人担心它可能没有得到适当的审查,因为委员会成员无法完全理解错综复杂的设计细节,而只是相信作者他们做得足够好。我个人也对大量在机上的论文以某种方式修改了主要提案感到担忧。我不想最终出现一些发送方/接收方的情况,但关键修改(例如诊断改进)错过了 C++26 的最后期限,并且当不再可能进行重大更改时,以后无法应用。

因此,虽然 P2300 在全体投票中被通过,现在是将成为 C++26 标准的工作草案的一部分,但这是一个非常微弱的投票,只有 1/3 的投票反对采用。我预计一些争议会持续到下一次会议。

反射

P2996 向 C++ 添加了反射。它具有对实体进行反射的能力,从而生成对象。然后,您可以使用标准库 API 查询有关它的信息,创建新对象并将其转换回代码,或生成全新的代码。然后,这可以用于最终实现枚举到字符串的转换(除其他外)。std::meta::infostd::meta::info

在语言方面,它已经通过了进化小组委员会,目前正在接受规范的审查。但是,选定的反射语法(使用 prefix 运算符,如 in )可能会导致使用 Objective C 块解析歧义,这在 C++ 中由 clang 编译器扩展支持。作者的第二个选择,如 中的 prefix 运算符也不理想,因为您可能希望将结果对象作为模板参数传递,编写类似 .不幸的是,是一个二合字母……^^foo%%foostd::meta::infosome_template<%foo><%

同时,library evolution 审查了提议的 library API 以查询有关对象的信息。从本质上讲,对象是内部编译器 AST 的不透明句柄,并且有一堆函数来获取信息。请注意,你反映什么并不重要,一个函数、一个类型、一个表达式、一个命名空间,它们都会导致相同的类型——。这是为了确保 C++ 中的未来更改不会导致反射 API 中的 API 或 ABI 中断。因此,编译器无法使用类型系统检测无意义的函数调用。例如,调用 integer 的 reflection 没有意义,但调用会编译。我们花了很长时间讨论这应该是前提条件冲突还是返回空字符串(这显然是前提条件冲突)。我们花了更长的时间讨论 的行为,但最终(正确地)决定它应该是一个空字符串,因为匿名命名空间是一个可以有名称的实体,但它就是没有——不像整数那样,要求名称是没有意义的。API 审查远未完成,但它将继续在 Telecons 上进行,以确保反射成为 C++26 的一部分。std::meta::infostd::meta::infostd::meta::infostd::meta::name_of42std::meta::name_of(reflection_of_anonymous_namespace)42

我唯一剩下关心的是与标准库其余部分的耦合。例如,返回类成员的编译时。我不喜欢这样。从哲学上讲,用于从编译器查询信息的低级 API 不应依赖于标准库。许多项目不使用 ,因为它们可以更好地实现它。但是,他们无法实现反射 API,只有编译器可以。因此,使用现有的 API 会迫使每个人都依赖 .此外,除非你使用模块,否则如果你小心避免使用标准库头文件,包括 和 (for ) 可能会大大增加编译时间。由于反射代码必须位于头文件中,并且不能隐藏在 中,因此这是一个问题。实现者还告诉我,与编译器知道的一些自定义构建类型相比,在编译时构造 a 的成本真的很高。因此,我将写一篇论文,通过切换到例如 and 而不是 和 。std::meta::members_ofstd::vector<std::meta::info>std::vectorstd::vector<vector><string_view>std::meta::name_of.cppstd::vectorstd::meta::info_arrayconst char*std:: vectorstd::string_view

尽管如此,我还是很高兴在 C++26 中使用反射。

C++ 借用检查

由于对安全编程语言的日益推动,C++ 委员会成立了第 23 研究组 (SG 23) 来讨论安全和安保提案。我参加了他们星期二的会议。

Bjarne Stroustrup 提出了 P3274,这是他关于添加安全配置文件的提案。这个想法是使用 attributes 选择性地选择加入部分代码中的编译时和运行时检查。例如,您可以选择加入执行范围检查以防止越界范围访问的配置文件。这当然是我们可以在 C++ 中采用更多检查而不破坏现有代码的唯一方法,但论文本身还没有提出更详细的具体配置文件。

与此同时,Sean Baxter 在他的 C++ 编译器中实现了 Rust 的整个借用检查器。它引入了带有生命周期注解的 borrow checked 引用,然后像在 Rust 中一样进行检查。这确实是一项令人印象深刻的工作,并且表明添加 C++ 的安全子集不存在技术问题。但是,存在采用问题。类型系统中的这种侵入性更改意味着在调用仍然使用常规引用(即所有引用)的库函数时,您将不会进行借用检查。对于他的 demo,他必须编写自己的 demo 并获得好处。这基本上将语言分叉为安全和不安全的库,并且所有内容都必须重写为安全版本。除非所有调用的函数都是安全的(或者你使用不安全的块),否则你也不能将函数标记为安全。这需要自下而上(部分)重写。但是,如果您无论如何都要重写代码,为什么不在 Rust 中重写它并免费获得采用 Rust 生态系统的所有其他好处呢?std2::string_viewstd2::vector

最终,我认为 C++ 不会或不需要成为一种安全的语言。我们已经有了一个安全的系统编程语言 Rust。如果你在编写安全很重要的代码,你应该使用 Rust。相反,我们应该把精力集中在改进与 Rust 的互作上。这样,C++ 可以轻松地将安全模块用于网络或解析等关键代码,并且 Rust 可以访问大量的遗留 C++ 代码。