RFC-0051:适用于 C++ 的更安全的结构体

RFC-0051:更安全的 C++ 结构体
状态已拒绝
区域
  • FIDL
说明

允许 C++ 开发者编写 FIDL 代码,如果结构体未完全初始化,则会在编译时中断。

作者
提交日期(年-月-日)2018-07-19
审核日期(年-月-日)2019-03-14

拒绝理由

2019 年 3 月 14 日,此 RFC 最初被接受。不过,该实现从未合并,并且 C++ 绑定自那时起发生了重大变化。鉴于此,我们将追溯性地拒绝此 RFC。

摘要

允许 C++ 开发者编写 FIDL 代码,如果结构体未完全初始化,则会在编译时中断。

设计初衷

在 Peridot 中,我们有复杂的 FIDL 结构,随着我们对如何解决所面临问题的理解不断加深,我们也在不断更改这些结构。 这些结构体通常嵌套很深,并且在代码中发送的位置与构造位置相距甚远。 在迭代结构体时,我们经常会对语义进行重大更改,添加必需字段或将之前可选的字段设为必需字段。很难找到需要更新的所有代码。 这些错误不会显示为编译时错误,而是显示为运行时错误,很难与错误初始化结构的相应代码相关联。

在进行更改以要求将所有必需字段传递到结构构造函数之前,Dart 代码中普遍存在相同类别的问题。 此项变更使 Dart 代码的开发效率和稳健性大幅提升。

设计

这会修改 C++ 绑定库和代码生成器。它不会移除任何现有接口,只会添加一种构建 FIDL 结构体实例的新方式。

这为 FIDL 结构添加了构建器模式。 使用该功能后的效果如下所示:

FooPtr foo = Foo::Builder()->set_bar("hello")->set_baz("world");

结构类上的 Builder() 静态方法会返回一个模板化的 builder 对象。构建器模板参数用于捕获正在构建的结构体的类型以及结构体上每个未设置字段的类。 它包含结构体的实例。

字段类有两个方法:set_名称(value)方法,用于在实例上设置字段值并返回一个构建器,该构建器从构建器的模板实参中移除了相应字段;以及 Check() 方法,该方法对于可选字段是空操作,对于必需字段是 static_assert 失败。

构建器类会扩展其模板参数中的所有字段类型,以便开发者可以访问 setter 方法。随着开发者调用 setter 并接收新的构建器类型,构建器模板实参中的字段类列表会缩小。例如,省略部分模板恶作剧:

Foo::Builder() 是具有 set_bar()set_baz() 方法的 Builder<Foo, Foo::Field_bar, Foo::Field_baz>

Foo::Builder()->set_bar(...) 是具有 set_baz() 方法的 Builder<Foo, Foo::Field_baz>

Foo::Builder()->set_bar(...)->set_baz(...) 是没有任何 setter 方法的 Builder<Foo>

构建器具有针对结构类型和结构指针类型的隐式转换运算符。这些方法会对剩余的字段类型调用 Check() 方法,并返回构建器所持有的结构实例。 Check() 方法要么是空操作(对于可选字段),要么是 static_assert 失败,用于指定未设置哪个必填字段。

文档和示例

我们将更新 FIDL 教程和示例,以演示创建结构体实例的传统方式和新方式。

向后兼容性

此提案纯粹是附加内容。 该版本不会引入向后不兼容性。

性能

此更改不会产生运行时性能开销。 它在 Compiler Explorer 中进行了原型设计,专门用于确保不会生成或执行任何额外的代码。

它会向绑定库添加一个新的头文件,并在生成的 C++ 代码中为每个结构字段添加几行额外的代码。C++ 编译器需要做一些额外的工作来解析模板,但不会增加任何对编译有重大影响的额外步骤。

安全

此更改使我们能够将程序员错误从运行时错误转换为 build 时错误。这样可以减少程序的状态空间,并减少必须正确处理和测试的错误情况数量。 这种意外行为的减少有利于提高安全性。

测试

应扩展 C++ 绑定单元测试,以测试构建器是否正确设置了不同类型的字段。

很难测试编译器是否会捕获构建器的错误使用情况(即未能设置必需字段)。我们不清楚应该如何测试。

缺点、替代方案和未知因素

这会向 FIDL C++ 绑定库添加一些相当棘手的模板。 这会增加维护负担,并可能导致一些小的 build 时开销。

之前的模板方法使用位掩码,该方法具有更简单的模板,但存在一些限制,例如需要 64 个字段,并增加了 FIDL 编译器的复杂性。

我们还可以构建一个 lint 工具,尝试跟踪是否已设置所有必需字段。这似乎是一个相当复杂的数据流分析。

在先技术和参考资料

去年,我们更改了 Dart 绑定,使结构体构造函数可以为每个字段采用命名实参。必需字段标记为必需,以便 dartanalyzer 可以拒绝导致某些字段未初始化的更改。