String View API

涵盖了各种 string_view 迁移

使用 std::string 的 C++ 字符串字段 API 极大地限制了 protobuf 内部实现及其演进。例如,mutable_string_field() 返回 std::string*,这迫使我们使用 std::string 来存储字段。这使其在 arenas 上的交互变得复杂,我们必须维护 arena 捐赠状态来跟踪字符串负载分配是来自 arena 还是堆。

长期来看,我们希望将我们所有的运行时和生成的 API 迁移为接受 string_view 作为输入,并从访问器中返回它们。本文档描述了截至我们 30.x 版本时的迁移状态。

字符串字段访问器

作为 2023 edition 的一部分,string_type 特性发布了一个 VIEW 选项,以允许逐步迁移到生成的 string_view API。使用此特性将影响 stringbytes 字段的 C++ 生成代码

与 ctype 的交互

在 2023 edition 中,你仍然可以在字段级别指定 ctype,同时可以在文件或字段级别指定 string_type。不允许在同一字段上同时指定两者。如果 string_type 是在文件级别设置的,那么字段上指定的 ctype 将优先。

除了 VIEW 选项外,string_type 的所有可能值都有一个对应的 ctype 值,它们的拼写相同,行为也相同。例如,两个枚举都有一个 CORD 值。

在 2024 edition 及更高版本中,将不再可以指定 ctype

生成的单一字段

对于 2023 edition 中的这两种字段定义

bytes foo = 1 [features.(pb.cpp).string_type=VIEW];
string foo = 1 [features.(pb.cpp).string_type=VIEW];

编译器将生成以下访问器方法

  • ::absl::string_view foo() const: 返回字段的当前值。如果字段未设置,则返回默认值。
  • void clear_foo(): 清除字段的值。调用此函数后,foo() 将返回默认值。
  • bool has_foo(): 如果字段已设置,则返回 true
  • void set_foo(::absl::string_view value): 设置字段的值。调用此函数后,has_foo() 将返回 true,且 foo() 将返回 value 的一个副本。
  • void set_foo(const string& value): 设置字段的值。调用此函数后,has_foo() 将返回 true,且 foo() 将返回 value 的一个副本。
  • void set_foo(string&& value): 通过移动传入的字符串来设置字段的值。调用此函数后,has_foo() 将返回 true,且 foo() 将返回 value
  • void set_foo(const char* value): 使用 C 风格的以 null 结尾的字符串来设置字段的值。调用此函数后,has_foo() 将返回 true,且 foo() 将返回 value 的一个副本。

生成的重复字段

对于这两种字段定义

repeated string foo = 1 [features.(pb.cpp).string_type=VIEW];
repeated bytes foo = 1 [features.(pb.cpp).string_type=VIEW];

编译器将生成以下访问器方法

  • int foo_size() const: 返回字段中当前元素的数量。
  • ::absl::string_view foo(int index) const: 返回给定基于零的索引处的元素。使用 [0, foo_size()-1] 范围之外的索引调用此方法会产生未定义的行为。
  • void set_foo(int index, ::absl::string_view value): 设置给定基于零的索引处的元素的值。
  • void set_foo(int index, const string& value): 设置给定基于零的索引处的元素的值。
  • void set_foo(int index, string&& value): 通过移动传入的字符串来设置给定基于零的索引处的元素的值。
  • void set_foo(int index, const char* value): 使用 C 风格的以 null 结尾的字符串来设置给定基于零的索引处的元素的值。
  • void add_foo(::absl::string_view value): 将一个具有给定值的新元素追加到字段的末尾。
  • void add_foo(const string& value): 将一个具有给定值的新元素追加到字段的末尾。
  • void add_foo(string&& value): 通过移动传入的字符串,将一个新元素追加到字段的末尾。
  • void add_foo(const char* value): 使用 C 风格的以 null 结尾的字符串,将一个新元素追加到字段的末尾。
  • void clear_foo(): 从字段中移除所有元素。调用此函数后,foo_size() 将返回零。
  • const RepeatedPtrField<string>& foo() const: 返回存储字段元素的底层 RepeatedPtrField。此容器类提供了类似 STL 的迭代器和其他方法。
  • RepeatedPtrField<string>* mutable_foo(): 返回指向存储字段元素的底层可变 RepeatedPtrField 的指针。此容器类提供了类似 STL 的迭代器和其他方法。

生成的 Oneof 字段

对于任何这些 oneof 字段定义

oneof example_name {
    string foo = 1 [features.(pb.cpp).string_type=VIEW];
    ...
}
oneof example_name {
    bytes foo = 1 [features.(pb.cpp).string_type=VIEW];
    ...
}

编译器将生成以下访问器方法

  • bool has_foo() const: 如果 oneof 的情况是 kFoo,则返回 true
  • ::absl::string_view foo() const: 如果 oneof 的情况是 kFoo,则返回字段的当前值。否则,返回默认值。
  • void set_foo(::absl::string_view value):
    • 如果同一 oneof 中的任何其他字段已设置,则调用 clear_example_name()
    • 设置此字段的值并将 oneof 的情况设置为 kFoo
    • has_foo() 将返回 truefoo() 将返回 value 的一个副本,而 example_name_case() 将返回 kFoo
  • void set_foo(const string& value): 类似第一个 set_foo(),但从一个 const string 引用中复制。
  • void set_foo(string&& value): 类似第一个 set_foo(),但从传入的字符串中移动。
  • void set_foo(const char* value): 类似第一个 set_foo(),但从一个 C 风格的以 null 结尾的字符串中复制。
  • void clear_foo():
    • 如果 oneof 的情况不是 kFoo,则不作任何更改。
    • 如果 oneof 的情况是 kFoo,则释放该字段并清除 oneof 的情况。has_foo() 将返回 falsefoo() 将返回默认值,而 example_name_case() 将返回 EXAMPLE_NAME_NOT_SET

枚举名称辅助函数

从 2024 edition 开始,引入了一个新特性 enum_name_uses_string_view,并且默认值为 true。除非禁用,对于像下面这样的枚举:

enum Foo {
  VALUE_A = 0;
  VALUE_B = 5;
  VALUE_C = 1234;
}

除了 Foo 枚举外,protocol buffer 编译器还将在标准的生成代码之外生成以下新函数:

  • ::absl::string_view Foo_Name(int value): 返回给定数值的名称。如果不存在这样的值,则返回一个空字符串。如果多个值具有相同的数字,则返回第一个定义的值。在上面的例子中,Foo_Name(5) 将返回 VALUE_B

通过添加如下特性覆盖,可以恢复到旧的行为:

enum Foo {
  option features.(pb.cpp).enum_name_uses_string_view = false;

  VALUE_A = 0;
  VALUE_B = 5;
  VALUE_C = 1234;
}

在这种情况下,名称辅助函数将切换回 const string& Foo_Name(int value)