旅行报告:波兰弗罗茨瓦夫秋季 ISO C++ 会议
上周,我参加了在波兰弗罗茨瓦夫举行的 ISO C++ 标准化委员会 2024 年秋季会议。这是针对即将推出的 C++26 标准和主要 C++26 功能冻结的第五次会议。有关所有取得进展的论文的概述,请阅读 Herb Sutter 的旅行报告。 合同和档案是本次会议取得最大进展的大项目。合同被转发到措辞审查,但仍然受到一些人的强烈反对。Profiles 本质上是标准化的静态分析,以提高内存安全性,尽管有些人认为它无效。由于各种日程安排冲突,我没有参加这些空间的任何相关讨论。相反,我将分享我对范围、重新定位和反射的想法。
范围
我花了第一天半的时间共同主持了第 9 研究组,这是 的研究小组。在讨论过程中,我们意识到 range 库中的两个大漏洞需要尽快解决。它们都涉及大小范围的概念。std::ranges
第一个漏洞与提案 P3179—C++ parallel range algorithms有关,该提案为 中的算法添加了执行策略。这使得运行多线程算法变得轻而易举:您不是 write,而是 write,现在您的输入被拆分为多个块并并行处理。std::ranges
std::ranges::transform(vec, output, fn)
std::ranges::transform(std::execution::par, vec, output, fn)
这种将 input 拆分为多个块需要具有已知大小的随机访问范围,因此与单线程算法相比,并行算法具有更严格的要求。对于采用两个 input range 的算法,都需要调整 both for the size 。否则,计算所有 的 ,首先需要一个串行循环来找到正确的大小,然后才能拆分输入。std::ranges::transform(std::execution::par, a, b, output, fn)
fn(a[i], b[i])
0 ≤ i < min(std::ranges::size(a), std::ranges::size(b))
一家供应商不喜欢两个范围都需要调整大小的要求。他们有一些客户想要编写类似 或 的代码。这两个函数只是永远重复,并且生成一个无限的数字序列,显然都比 长。因此,明显的语义是 call 或 for all 。但是,视图不提供 ,因为它们是无限的。因此,供应商的实现要求只调整一个范围的大小,并假设另一个范围更长,但无法静态检查。更好的解决方案需要 新概念 .然后我们可以要求 它采用两个大小范围或一个大小范围和一个无限范围。std::ranges::transform(std::execution::par, vec, std::views::repeat(n), output, fn)
std::ranges::transform(std::execution::par, vec, std::views::iota(0), output, fn)
std::views::repeat(n)
n
std::views::iota(0)
std::ranges::size(vec)
fn(vec[i], n)
fn(vec[i], i)
0 ≤ i < std::ranges::size(vec)
std::ranges::size
std::ranges::infinite_range
std::ranges::transform
这样的概念也有助于解决一些库问题,例如 LWG4019 指出这只是一个无限循环,因为它试图找到无限范围的终点。编译器错误会带来更好的用户体验,并且可以通过要求将非无限范围传递给 来实现。std::views::iota(0) | std::views::reverse
std::views::reverse
std::views::reverse
std::simd
提案(在 C++26 中被接受为工作草案)的作者 Matthias Kretz 发现了第二个漏洞。在 P3299 中,他想用更安全的范围构造函数替换 的不安全迭代器构造函数。理想的语义是允许从大小与 SIMD 大小匹配的范围进行隐式转换,否则允许使用自定义越界语义进行显式转换。但是,要有条件地允许隐式转换,我们需要的范围不仅在没有循环 () 的情况下动态已知,而且在编译时静态知道其大小。现在,所能做的就是对类型进行硬编码,比如 and 一个静态大小的实例化。std::simd
std::ranges::sized_range
std::array
std::span
这里需要一个适当的概念。在 think-cell,我们在 的基础上构建了这样一个概念,它允许我们查询给定范围类型的大小。我之前曾在博客中介绍过该领域的一个有用 ifor,以及如何将其无缝扩展到所有范围适配器中。tc::constexpr_size
我将与相关各方合作,将 和 (人名待定) 带到 2 月举行的下一次会议的第 9 研究组。如果运气好,并且国家机构对最终委员会草案进行投票施加了足够的压力,他们可以及时赶上 C++26,这样现有提案就可以从中受益。std::ranges::infinite_range
std::ranges::constexpr_sized_range
搬迁
自从添加了 move 语义以来,人们一直在要求一种形式的破坏性 move。现在,需要部分形成对象的 moved-from 状态,因为析构函数仍将运行。这意味着不可能实现像 mobileable non-null 这样的东西:添加 move 构造函数需要一个 moved-from 状态,而析构函数是无作的。这需要将其设置为 .std::unique_ptr
nullptr
此外,像 这样的作首先分配新内存,然后移动现有元素,最后销毁旧内存中的元素,效率不高。原则上,move + destroy 步骤可以替换为 的 高级版本。move 构造函数只需创建一个 moved-from 状态,以确保析构是无作;如果从未执行过销毁,则通常可以通过复制字节来完成。std::vector::reserve
std::memcpy
此功能的当前迭代 P2786 通过引入 “琐碎可重定位” 类型的思想来关注优化方面。在这些类型中,对 move 构造函数的调用后跟对析构函数的调用可以替换为新的 primitive operation ,该作本质上是执行一些额外的魔法来结束和启动生命周期。简单的 relocatable 属性是自动计算的:如果所有成员都是 trivily relocatable 并且没有自定义移动作,则该类是 trivially relocatable。对于具有自定义移动作的类型,您必须通过声明 move + destroy 可以替换为 来选择加入,编译器无法为您确定这一点。std::trivially_relocate
std::memcpy
std::trivially_relocate
同样,本文还介绍了 “replaceable” 类型。如果 move 赋值运算符等效于对析构函数的调用,然后对 move 构造函数的调用,则类型是可替换的。对于大多数类型来说,情况都是如此,除非它们包含引用或具有 propagate_on_container_move_assignment
为 false 的分配器。与 trivially relocatable 一样,如果所有成员都是可替换的,并且该类型没有自定义移动运算符,则类型是可替换的,否则该类型必须选择加入。如果类型是可替换的并且很容易重定位,则对 move 赋值运算符后跟析构函数的调用也可以替换为 。否则,只能替换后跟析构函数的 move 构造函数。std::trivially_relocate
显然,这个特定的提案背后有很多戏剧性和历史。它让负责 C++ 语言功能的小组 EWG 达成了非常紧密的共识,并被交给了 LEWG,这是我第一次看到这个功能。除了查看该功能的界面外,EWG 还要求我们查看选择加入所需的关键字。由于没有人想出更好的选择,我们最终得到了一个 and 上下文关键字,放在类的名称之后:std::trivially_relocate
trivially_relocatable
replaceable
class unique_ptr trivially_relocatable replaceable { … };
想做这件事吗?没问题,也添加这些:final
[[nodiscard]]
class [[nodiscard]] unique_ptr final trivially_relocatable replaceable { … };
P2822 提出了用于控制对 ADL 可见的命名空间的语法,这是我非常喜欢的一个功能。指定 our 不参与 ADL 意味着我们的类声明如下所示:unique_ptr
class [[nodiscard]] unique_ptr final trivially_relocatable replaceable namespace() { … };
不用说,这太糟糕了,我讨厌它的一切。
在 LEWG 中转发 P2786 的 trivially relocatable 的投票失败,因此现在该论文的状态尚不清楚。
反射
好消息是,P2996—Reflection for C++26 正在按计划用于 C++26。最后一个需要解决的大问题是目标 C 块的语法歧义(是的,真的)。通过将 reflection 运算符从 更改为 ,可以解决此问题。^foo
^^foo
坏消息是,我的论文 P3429—Reflection header should minimize standard library dependencies(反射头应最小化标准库依赖关系)被彻底拒绝,除了本质上是对措辞的驱动修复。我预料到会被拒绝:LEWG,负责设计标准库的小组,历来并不真正同情那些不想使用标准库的人,但我仍然感到失望。我真的希望底层编译器内置函数保持解耦状态,以便您可以为它们提供自己的轻量级抽象,而不依赖于在编译时进行构造。这样,如果你对 using for detect 编译器感到满意,你就不会被迫包含 to use reflection。std::vector
<meta>
#ifdef
一线希望是,由于反射 API 非常昂贵,编译器实现者几乎肯定会投入一些工程工作来加快评估速度。例如,他们可以将类型视为在本机代码中实现的内置类型,或者使用字节码指令来评估 .constexpr
std::vector<std::meta::info>
constexpr
也许这样我们最终会得到 C++ 作为在 VM 中运行的解释型语言。