返回介绍

9.6 进行中的工作

发布于 2024-08-19 12:44:36 字数 12092 浏览 0 评论 0 收藏 0

当然,很多把目标放在 C++20 之后版本的工作还在进行中,而另一些原本目标是在 C++20 发布的工作则没能及时完成,尤其是:

  • §8.8.1:网络和执行器——再度延迟。
  • §9.6.1:契约——断言、前置条件和后置条件;原本目标是 C++20,但延迟了。
  • §9.6.2:反射——基于当前编译的代码将代码注入程序;目标是 C++23。

另外,工作组和研究组也仍有工作正在进行中(§3.2)[Stroustrup 2018d]。

9.6.1 契约

契约的特殊之处在于,不但很多人希望它可以进入 C++20,而且契约是被投票写入 C++20 的工作文件中的,只是在最后一刻被从中移除。一个由 John Spicer 主持的新的研究组 SG21 已经成立,试图为 C++23 或者 C++26 提供某种形式的契约。契约于 C++20 的遭遇是令人惋惜的,但可能也能给人以启发。

各种形式的契约在 C++ 和其他语言中都有着悠久的历史。我记得在 1970 年代初,当我第一次遇到 Peter Naur 的不变量 [Naur 1966] 的时候,我一度被它深深吸引。在 1990 年代早期,一个被称为 A++ 的断言系统被考虑用于 C++,但却被认为涉及面太广而不现实。在 1980 年代晚期,Bertrand Meyer 曾推广过 Eiffel 里契约的概念 [Meyer 1994]。作为 C++0x 努力的一部分,一些提案 [Crowl and Ottosen 2006] 在 C++ 委员会受到了高度重视,但最终却失败了,主要原因在于被认为过于复杂,写法也不优雅。

多年来,Bloomberg(那家纽约市的金融信息公司)一直使用一个名为契约的实时断言系统去捕获代码中的问题。2013 年,来自 Bloomberg 的 John Lakos 提议标准化该系统 [Lakos and Zakharov 2013]。这个提案受到了好评,但它遇到两个问题:

  • 它基于宏
  • 它严格来说是代码实现中的断言,而不是可以增强接口的东西

修订接踵而至,但是共识却并没有出现。为了打破僵局,一群来自微软、Facebook、谷歌和马德里的卡洛斯三世大学的人提出一个简单契约的 系统,该系统不使用宏,并且对前置条件和后置条件提供支持(正如 C++0x 所尝试的)[Garcia et al. 2015]。和 Bloomberg 的提案一样,这一提案得到了多年大规模工业应用的背书,但它的重点是在静态分析中使用契约。J. Daniel Garcia(卡洛斯三世大学)努力工作以求做出满足各方面需求的设计,但该提案也遭到了反对。

经过了无数次的会议、多篇论文和(偶尔激烈的)讨论之后,妥协显然是难以达成了。两个小组请求我来进行协调。我之前宣称,讨论太专注在细枝末节上 了,而我们需要一个最小提案,包含两个小组的核心诉求,而不是有争议的细节。他们要我来证明我的推断,拿一个这样的最小提案出来。在我和两个小组的代表轮 番讨论、工作了相当一段时间之后,我们最终联合各方共同起草了联合提案 [Dos Reis et al. 2016a]。我认为这个设计技术上是相当充分的,并非一个政治上的妥协。它旨在满足三方面的需求(按重要性排序):

  • 系统和可控的运行期测试
  • 为静态分析器提供信息
  • 为优化器提供信息

在 J. Daniel Garcia 领导的进一步工作之后,该提案最终在 2018 年 6 月正式被 C++20 采纳 [Dos Reis et al. 2018]。

为避免引入新的关键字,我们使用属性语法。例如,[[assert: x+y>0]]。一个契约对一个有效的程序不起任何作用,因此这种方式满足属性的原来概念(§4.2.10)。

有三种契约:

  • assert——可执行代码中的断言
  • expects——函数声明中的前置条件
  • ensure——函数声明中的后置条件

有三种不同等级的契约检查:

  • audit——代价高昂的谓词,仅在某些调试模式检查
  • default——代价低廉的谓词,即使在生产代码中检查也是可行的
  • axiom——主要给静态分析器看的谓词,在运行期从不检查

在违反契约时,将执行(可能是用户安装的)契约违反处理程序。默认行为是程序立即终止。

我发现一个有意思的事:有一种构建模式允许程序在契约失败后继续执行。我的第一反应是疯了吧!契约旨在防止违反契约的程序运行。那算是最最常见的反应了。不论如何,John Lakos 坚信,基于 Bloomberg 代码的相关经验,当你把契约加入一个大型的古老代码仓库,契约总是会被违反:

  • 某些代码会违反契约,而实际上并没有做任何该契约所要防止的事情。
  • 某些新契约本身就包含错误。
  • 某些新契约具有意料之外的效果。

有了继续的选项,你可以使用契约违反处理程序去记录日志并继续运行。这样的话,你既可以在单次运行中检测到多次违规,也可以让契约在假定正确的老代码中启用。人们相信这是逐步采用契约的关键。

我们并没有找到充足的理由去添加类不变量,或允许在覆盖函数中削弱前置条件,或允许在覆盖函数中增强后置条件。要点是简单。理想情况是先为 C++20 提供一个最小的初始设计,然后如有需要再于其上添砖加瓦。

这个设计由 J. Daniel Garcia 实现,并于 2018 年 6 月投票通过进入 C++ 委员会的 C++20 的工作文件中。像往常一样,虽然规范还有一些问题,但我们相信能够赶在最终标准发布前的两年内修复所有的问题。例如,人们发现工作文件文本中允许编译器基 于所有契约(无论检查与否)进行优化。那并非有意而为之。从所有的契约在正确的程序中都有效的角度看,这是合理的,但是这么做,对于那些带有特别为捕获不可能的错误而写的契约的程序来说却是灾难性的。考虑下面的例子:

[[assert: p!=nullptr]]
p->m = 7;

假如 p==nullptr,那么 p->m 将是未定义行为。编译器被允许假设未定义行为不会发生;由此编译器优化掉那些导致未定义行为的代码。这样做的结果可能让人大吃一惊。在这样的情况下,如果违反契约之后程序能够继续执行,编译器将被允许假定 p->m 是有效的,因此 p!=nullptr;然后编译器会消除契约关于 p==nullptr 的检查。这种被称为时间旅行优化的做法当然是与契约的初衷大相径庭,还好若干补救方案被及时提出 [Garcia 2018; Stroustrup 2019c; Voutilainen 2019a]。

2018 年 8 月,在 C++20 新提案的最后期限过后,由 John Lakos 领导的 Bloomberg 的一个小组,包括 Hyman Rosen 和 Joshua Berne 在内,提出了一系列重新设计的提案 [Berne et al. 2018; Berne and Lakos 2018a,b; Lakos 2018]。特性冻结的日期(审议新提案的最后一天)是由委员会全体投票表决确定的。这些提案则是基于在契约自身中规定契约行为的方案。例如,[[assert check_maybe_continue: x>0]][[assert assume: p!=nullptr]]

与其使用构建模式去控制所有契约(比如,激活所有默认契约或关闭所有基于契约的运行期检查)的行为,你不如直接修改单个契约的代码。在这方面,这些新方案与工作文件中决议通过的设计大相径庭。考虑下面的例子:

[[assert assume: p!=nullptr]]

这将使得 2014 年被否决的基于宏的方案卷土重来,因为管理代码变化的显然方式是用宏,例如:

[[assert MODE1: p!=nullptr]]

这里的 MODE1 可以被 #define 成所支持的若干选项之一,如 assumedefault。或者,大致等效地,通过命令行上的参数(类似于命令行宏)来定义诸如 assume 之类的限定符的含义。

本质上,契约违约后继续执行的可能性与程序员对契约含义的控制的两者的结合,将把契约机制从断言系统转变为一种新的控制流机制。

一些提案甚至建议放弃对静态分析的支持。类似这样的提案有几十个变种,全都来得太晚,没一个能增进共识。

大量涌入的新奇提案(来自 Bloomberg 团队和其他团队,比如,[Berne 2019; Berne and Lakos 2019; Khlebnikov and Lakos 2019; Lakos 2019; Rosen et al. 2019])和成百上千讨论这些提案的电子邮件阻碍了真正必需的讨论,即对工作文件中的设计现状进行问题修复。正如我曾不断警告的那样(比如 [Stroustrup 2019c]),这些企图重新设计契约的提案的结果是,在 Nico Josuttis 的提议下,契约被从 C++20 中移除 [Josuttis et al. 2019b]。我认为去年关于契约的讨论是一个典型的例子,谁都得不到任何东西,因为有人只想要按他们的方式来。新的研究组 SG21 能否为 C++23 或 C++26 交付某种能够被更广泛接受的东西,时间将会给出答案。

9.6.2 静态反射

2013 年,一个研究反射的研究组(SG7)成立了,并发出了征集意见的呼吁 [Snyder and Carruth 2013]。有一个广泛的共识,那就是 C++ 需要静态反射机制。更确切地说,我们需要一种方法来写出能检查它自己是属于哪个程序的一部分的代码,并基于此往该程序中注入代码。这样我们就可以用简洁的 代码替换冗长而棘手的样板代码、宏和语言外的生成器。比如,我们可以为下面的场景自动生成函数,如:I/O 流、日志记录、比较、用于存储和网络的封送处理(marshalling)、构建和使用对象映射、枚举的字符串化、测试支持,及其他的更多可能 [Chochlík et al. 2017; Stroustrup 2018g]。反射研究组的目标是为 C++20 或 C++23 做好准备;我们认为 C++17 并不是一个现实的目标。

大家普遍认同,依赖在运行期遍历一个始终存在的数据结构的反射/内省方式不适合 C++,因为这种数据的大小、语言构件的完整表示的复杂性和运行期遍历的成本都会是问题。

很快出现了一些提案 [Chochlík 2014; Silva and Auresco 2014; Tomazos and Spertus 2014],并且,在接下来的数年里,由 Chandler Carruth 主持的研究组召开了多次会议试图决定其范围和方向。选定的方式基于类型,这些类型以经典的面向对象的类层次结构来组织,需要泛型的地方由概念(§6) 支持 [Chochlík 2015; Chochlík and Naumann 2016; Chochlík et al. 2017]。该方式主要由 Matóš Chochlík、Axel Naumann 和 David Sankel 发展和实现。结果作为一项技术规范在 2019 得以批准 [Sankel 2018]。

在静态反射(预期的)长时间的酝酿期内,基于 constexpr 函数(§9.3.3) 的编译期计算稳步发展,最终出现了基于函数而不是类层次结构的静态反射的提案。主要的拥护者是 Andrew Sutton、Daveed Vandevoorde、Herb Sutter 和 Faisal Vali [Sutton and Sutter 2018; Sutton et al. 2018]。设计焦点转移的主要论据,一部分是由于分析和生成代码这些事天生就是函数式的,而且基于 constexpr 函数的编译期计算已经发展到元编程和反射相结合的地步。这种方法的另一个优点(最先由 Daveed Vandevoorde 提出)是,用于函数的编译器内部数据结构,跟用于类型层次结构的比起来,会更小巧,生命周期也更短,因此它们使用内存会少得多,编译起来也快得多。

2019 年 2 月在科隆召开的标准会议上,David Sankel 和 Michael Park 展示了一个结合了这两个方法优点的设计 [Sankel and Vandevoorde 2019]。在最根本的层面上仅有一个单一的类型存在。这达到了最大的灵活性,并且编译器开销也最小。

最重要的是,静态类型的接口可以通过一种类型安全的转换来实现(从底层的单一类型 meta::info 到更具体的类型,如 meta::type_meta::class_)。这里有一个基于 [Sankel and Vandevoorde 2019] 的例子。通过概念重载(§6.3.2),它实现了从 meta::info 到更具体类型的转换。考虑下面的例子:

namespace meta {
    consteval std::span<type_> get_member_types(class_ c) const;
}

struct baz {
    enum E { /*...*/ };
    class Buz{ /*...*/ };
    using Biz = int;
};

void print(meta::enum_);    // 打印一个枚举类型
void print(meta::class_);   // 打印一个类类型
void print(meta::type_);    // 打印任何类型

void f()
{
    constexpr meta::class_ metaBaz = reflexpr(baz);
    template for (constexpr meta::type_ member : get_member_types(metaBaz))
        print(meta::most_derived(member));
}

这里关键的新语言特性是 reflexpr 运算符,它返回一个(元)对象,该对象描述了它的参数,还有 template for [Sutton et al. 2019],根据一个异质结构中的元素的类型扩展每个元素,从而遍历该结构的各元素。

此外,我们也有机制可以将代码注入正在编译的程序中。

类似这样的东西很可能会在 C++23 或 C++26 中成为标准。

作为一个副作用,在反射方案上的雄心勃勃的工作也刺激了编译期求值功能的改进:

  • 标准中的类型特征集(§4.5.1)
  • 源代码位置的宏(如 __FILE____LINE__)被内在机制所替代 [Douglas and Jabot 2019]
  • 编译期计算的功能(例如,用于确保编译期求值的 consteval
  • 展开语句(template for——到 C++23 就可以用来遍历元组中的元素 [Sutton et al. 2019])。

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据
    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
    原文