RFC-0051:C++ 的更安全的结构体 | |
---|---|
状态 | 已拒绝 |
区域 |
|
说明 | 允许 C++ 开发者编写 FIDL 代码,如果结构体未完全初始化,则该代码会在编译时中断。 |
作者 | |
提交日期(年-月-日) | 2018-07-19 |
审核日期(年-月-日) | 2019-03-14 |
拒绝理由
此 RFC 于 2019 年 3 月 14 日首次获得接受。不过,实现从未合并,并且 C++ 绑定自那时起已发生重大变化。鉴于此,我们将会追溯拒绝此 RFC。
摘要
允许 C++ 开发者编写 FIDL 代码,如果结构体未完全初始化,则该代码会在编译时中断。
设计初衷
在 Peridot 中,我们有复杂的 FIDL 结构体,随着我们更好地了解如何解决我们要解决的问题,我们会对其进行更改。结构体通常会深层嵌套,并在远离其构建位置的代码中发送。迭代结构体时,我们通常会对语义进行破坏性更改,添加必填字段或将之前的可选字段设为必填。很难跟踪需要更新的所有代码。这些错误不会显示为编译时错误,而是显示为运行时错误,这类错误很难与错误初始化结构体代码相关联。
在做出更改(要求将所有必填字段传入结构体构造函数)之前,Dart 代码中也存在类似的问题。这项变更让 Dart 代码的开发变得更加高效和稳健。
设计
这会修改 C++ 绑定库和代码生成器。它不会移除任何现有接口,而只是添加了一种构建 FIDL 结构体实例的新方法。
这为 FIDL 结构体添加了构建器模式。 使用方式如下:
FooPtr foo = Foo::Builder()->set_bar("hello")->set_baz("world");
结构体类上的 Builder() 静态方法会返回模板化构建器对象。构建器模板参数会捕获要构建的结构体的类型以及结构体上每个未设置字段的类。它包含结构体的实例。
字段类有两种方法:set_
name(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++ 编译器必须执行一些额外的工作来解析模板,但不会在编译过程中添加任何会产生重大影响的额外步骤。
安全
通过这项更改,我们可以将程序员的错误从运行时错误转换为构建时错误。这会缩减程序的状态空间,并减少必须正确处理和测试的错误情形的数量。减少意外行为有助于提高安全性。
测试
应扩展 C++ 绑定单元测试,以测试构建器是否正确设置了不同类型的字段。
很难测试编译器是否会捕获构建器的错误用法(例如:未设置必需字段)。我们不清楚该如何进行测试。
缺点、替代方案和未知情况
这会向 FIDL C++ 绑定库添加一些相当棘手的模板。这会带来维护负担,并且可能会产生一些小的构建时间开销。
之前的模板方法使用了位掩码,这种方法的模板更简单,但会强加 64 个必需字段等限制,并增加了 FIDL 编译器的复杂性。
我们还可以构建一个 lint 工具,用于跟踪是否已设置所有必填字段。这似乎是一项非常复杂的数据流分析。
在先技术和参考文档
去年,Dart 绑定发生了变化,以便结构体构造函数接受每个字段的命名实参。必填字段会被标记为必填,以便 dartanalyzer 可以拒绝导致某些字段未初始化的更改。