C++ 生成代码指南

精确描述了协议缓冲区编译器为任何给定的协议定义生成的 C++ 代码。

proto2、proto3 和 editions 生成代码之间的任何差异都会在此处突出显示。请注意,这些差异是本文档所述的生成代码中的差异,而不是基础消息类/接口中的差异,后者在所有版本中都是相同的。在阅读本文档之前,您应该阅读 proto2 语言指南proto3 语言指南2023 年版语言指南

编译器调用

Protocol buffer 编译器在调用 --cpp_out= 命令行标志时会生成 C++ 输出。--cpp_out= 选项的参数是您希望编译器写入 C++ 输出的目录。编译器为每个 .proto 文件输入创建一个头文件和一个实现文件。输出文件的名称是通过获取 .proto 文件名并进行两个更改来计算的

  • 扩展名 (.proto) 分别被替换为头文件或实现文件的 .pb.h.pb.cc
  • proto 路径(通过 --proto_path=-I 命令行标志指定)被替换为输出路径(通过 --cpp_out= 标志指定)。

因此,举例来说,假设您像下面这样调用编译器:

protoc --proto_path=src --cpp_out=build/gen src/foo.proto src/bar/baz.proto

编译器将读取文件 src/foo.protosrc/bar/baz.proto 并生成四个输出文件:build/gen/foo.pb.hbuild/gen/foo.pb.ccbuild/gen/bar/baz.pb.hbuild/gen/bar/baz.pb.cc。如果需要,编译器将自动创建目录 build/gen/bar,但它不会创建 buildbuild/gen;它们必须已经存在。

包(Packages)

如果 .proto 文件包含 package 声明,则该文件的所有内容都将放置在相应的 C++ 命名空间中。例如,给定 package 声明

package foo.bar;

文件中的所有声明都将驻留在 foo::bar 命名空间中。

消息

给定一个简单的消息声明:

message Foo {}

Protocol buffer 编译器将生成一个名为 Foo 的类,该类公开派生自 google::protobuf::Message。该类是具体类;没有纯虚方法未实现。Message 中虚拟但非纯虚的方法可能会被 Foo 重写,具体取决于优化模式。默认情况下,Foo 实现所有方法的专用版本以获得最大速度。但是,如果 .proto 文件包含行

option optimize_for = CODE_SIZE;

那么 Foo 将仅重写功能所需的最小方法集,并依赖于其余方法的基于反射的实现。这会显着减小生成代码的大小,但也会降低性能。或者,如果 .proto 文件包含

option optimize_for = LITE_RUNTIME;

那么 Foo 将包含所有方法的快速实现,但将实现 google::protobuf::MessageLite 接口,该接口仅包含 Message 方法的子集。特别是,它不支持描述符或反射。但是,在这种模式下,生成代码只需要链接 libprotobuf-lite.so(Windows 上为 libprotobuf-lite.lib),而不是 libprotobuf.solibprotobuf.lib)。“lite”库比完整库小得多,更适合资源受限的系统,例如手机。

应该创建自己的 Foo 子类。如果您对此类进行子类化并重写虚拟方法,则重写可能会被忽略,因为许多生成的调用都进行了去虚拟化以提高性能。

Message 接口定义了允许您检查、操作、读取或写入整个消息的方法,包括从二进制字符串解析和序列化到二进制字符串。

  • bool ParseFromString(::absl::string_view data):从给定的序列化二进制字符串(也称为线格式)解析消息。
  • bool SerializeToString(string* output) const:将给定的消息序列化为二进制字符串。
  • string DebugString():返回一个字符串,显示 proto 的 text_format 表示形式(仅应用于调试)。

除了这些方法之外,Foo 类还定义了以下方法:

  • Foo():默认构造函数。
  • ~Foo():默认析构函数。
  • Foo(const Foo& other):复制构造函数。
  • Foo(Foo&& other):移动构造函数。
  • Foo& operator=(const Foo& other):赋值运算符。
  • Foo& operator=(Foo&& other):移动赋值运算符。
  • void Swap(Foo* other):与另一个消息交换内容。
  • const UnknownFieldSet& unknown_fields() const:返回解析此消息时遇到的未知字段集。如果 .proto 文件中指定了 option optimize_for = LITE_RUNTIME,则返回类型更改为 std::string&
  • UnknownFieldSet* mutable_unknown_fields():返回指向解析此消息时遇到的未知字段的可修改集指针。如果 .proto 文件中指定了 option optimize_for = LITE_RUNTIME,则返回类型更改为 std::string*

注意: 复制构造函数和赋值运算符会深度复制消息数据。这确保了每个消息对象都拥有并管理自己的数据副本,从而防止了诸如双重释放或释放后使用之类的错误。此行为与标准 C++ 中拥有其数据的对象(如 std::vector)的实践一致。对于来自具有不同复制语义的语言(例如 JavaScript 或 TypeScript,其中浅复制可能更常见)的开发者来说,重要的是要注意对复制的消息的修改不会影响原始消息,反之亦然。

该类还定义了以下静态方法:

  • static const Descriptor* descriptor():返回类型的描述符。它包含有关类型的信息,包括它有哪些字段以及它们的类型是什么。这可以与 反射 一起用于以编程方式检查字段。
  • static const Foo& default_instance():返回 Foo 的常量单例实例,该实例与新构造的 Foo 实例相同(因此所有单一字段都未设置,所有重复字段都为空)。请注意,消息的默认实例可以通过调用其 New() 方法作为工厂来使用。

Abseil flag 支持

Message 对象对 Abseil 的标志解析/取消解析逻辑具有原生支持。它们可用作 ABSL_FLAG 声明的类型。标志语法为 :format,options...:value,其中

  • formattextserialized 之一。
  • options 是一个可能为空的选项列表。每种格式都有其支持的选项。
  • value 是指定格式的有效负载。

有效的选项是:

  • 对于 text
    • base64:表示 value 已编码为 base64。
    • ignore_unknown:指定时,未知字段/扩展名将被丢弃。否则,它们会导致解析失败。
  • 对于 serialized
    • base64:表示 value 已编码为 base64。建议将 serializedbase64 一起使用,因为在 shell 中传递二进制数据既困难又容易出错。

请注意,消息类型的标志支持不适用于 LITE_RUNTIME 配置。

示例

ABSL_FLAG(MyProtoType, my_proto_config, {},
          "This is a proto config description.");

嵌套类型

消息可以声明在另一个消息内部。例如:

message Foo {
  message Bar {}
}

在这种情况下,编译器将生成两个类:FooFoo_Bar。此外,编译器将在 Foo 中生成一个 typedef,如下所示:

typedef Foo_Bar Bar;

这意味着您可以使用嵌套类型的类,就好像它是嵌套类 Foo::Bar 一样。但是,请注意 C++ 不允许前向声明嵌套类型。如果您想在另一个文件中前向声明 Bar 并使用该声明,则必须将其标识为 Foo_Bar

字段

除了上一节中描述的方法外,Protocol buffer 编译器还会为 .proto 文件中定义的消息的每个字段生成一组访问器方法。这些方法采用小写/蛇形命名法,例如 has_foo()clear_foo()

除了访问器方法之外,编译器还会为每个字段生成一个整数常量,其中包含其字段编号。常量名称是字母 k,后跟转换为驼峰式命名的字段名,然后是 FieldNumber。例如,给定字段 optional int32 foo_bar = 5;,编译器将生成常量 static const int kFooBarFieldNumber = 5;

对于返回 const 引用的字段访问器,在对消息进行下一次修改访问之前,该引用可能会失效。这包括调用任何字段的任何非 const 访问器、调用从 Message 继承的任何非 const 方法,或通过其他方式修改消息(例如,通过使用该消息作为 Swap() 的参数)。相应地,如果在此期间没有对消息进行任何修改访问,则返回引用的地址仅保证在访问器的不同调用之间保持不变。

对于返回指针的字段访问器,在对消息进行下一次修改或非修改访问时,该指针可能会失效。这包括(无论 const 性如何)调用任何字段的任何访问器、调用从 Message 继承的任何方法或通过其他方式访问消息(例如,通过复制消息使用复制构造函数)。相应地,返回指针的值从不保证在访问器的两次不同调用之间保持相同。

生成的字段名

保留的关键字会在生成的输出中附加下划线。

例如,以下 proto3 定义语法

message MyMessage {
  string false = 1;
  string myFalse = 2;
}

生成以下部分输出

  void clear_false_() ;
  const std::string& false_() const;
  void set_false_(Arg_&& arg, Args_... args);
  std::string* mutable_false_();
  PROTOBUF_NODISCARD std::string* release_false_();
  void set_allocated_false_(std::string* ptr);

  void clear_myfalse() ;
  const std::string& myfalse() const;
  void set_myfalse(Arg_&& arg, Args_... args);
  std::string* mutable_myfalse();
  PROTOBUF_NODISCARD std::string* release_myfalse();
  void set_allocated_myfalse(std::string* ptr);

显式存在数值字段

对于具有 显式存在性 的数值字段的字段定义

int32 foo = 1;

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

  • bool has_foo() const:如果字段已设置,则返回 true
  • int32_t foo() const:返回字段的当前值。如果字段未设置,则返回默认值。
  • void set_foo(::int32_t value):设置字段的值。调用此方法后,has_foo() 将返回 truefoo() 将返回 value
  • void clear_foo():清除字段的值。调用此方法后,has_foo() 将返回 falsefoo() 将返回默认值。

对于其他数值字段类型(包括 bool),int32_t 会根据 标量值类型表 替换为相应的 C++ 类型。

隐式存在数值字段

对于具有 隐式存在性 的数值字段的字段定义

int32 foo = 1;

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

  • ::int32_t foo() const:返回字段的当前值。如果字段未设置,则返回 0。
  • void set_foo(::int32_t value):设置字段的值。调用此方法后,foo() 将返回 value
  • void clear_foo():清除字段的值。调用此方法后,foo() 将返回 0。

对于其他数值字段类型(包括 bool),int32_t 会根据 标量值类型表 替换为相应的 C++ 类型。

显式存在字符串/字节字段

注意: 自 2023 年版起,如果 features.(pb.cpp).string_type 设置为 VIEW,则会生成 string_view API。

对于具有 显式存在性 的这些字段定义

string foo = 1;
bytes foo = 2;

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

  • bool has_foo() const:如果字段已设置,则返回 true

  • const string& foo() const:返回字段的当前值。如果字段未设置,则返回默认值。

  • void set_foo(...):设置字段的值。调用此方法后,has_foo() 将返回 truefoo() 将返回 value 的副本。

  • string* mutable_foo():返回一个指向存储字段值的可修改 string 对象的指针。如果调用前字段未设置,则返回的字符串将为空(不是默认值)。调用此方法后,has_foo() 将返回 truefoo() 将返回写入给定字符串的任何值。

    注意: 此方法将在新的 string_view API 中删除。

  • void clear_foo():清除字段的值。调用此方法后,has_foo() 将返回 falsefoo() 将返回默认值。

  • void set_allocated_foo(string* value):将 string 对象设置到字段并释放先前存在的字段值(如果存在)。如果 string 指针不是 NULL,则消息将拥有分配的 string 对象,并且 has_foo() 将返回 true。消息可以随时删除分配的 string 对象,因此指向该对象的引用可能会失效。否则,如果 valueNULL,则行为与调用 clear_foo() 相同。

  • string* release_foo():释放字段的所有权并返回 string 对象的指针。调用此方法后,调用者将拥有分配的 string 对象,has_foo() 将返回 falsefoo() 将返回默认值。

隐式存在字符串/字节字段

注意: 自 2023 年版起,如果 features.(pb.cpp).string_type 设置为 VIEW,则会生成 string_view API。

对于具有 隐式存在性 的这些字段定义

string foo = 1 [features.field_presence = IMPLICIT];
bytes foo = 1 [features.field_presence = IMPLICIT];

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

  • const string& foo() const:返回字段的当前值。如果字段未设置,则返回空字符串/空字节。
  • void set_foo(Arg_&& arg, Args_... args):设置字段的值。调用此方法后,foo() 将返回 value 的副本。
  • string* mutable_foo():返回一个指向存储字段值的可修改 string 对象的指针。如果调用前字段未设置,则返回的字符串将为空。调用此方法后,foo() 将返回写入给定字符串的任何值。
  • void clear_foo():清除字段的值。调用此方法后,foo() 将返回空字符串/空字节。
  • void set_allocated_foo(string* value):将 string 对象设置到字段并释放先前存在的字段值(如果存在)。如果 string 指针不是 NULL,则消息将拥有分配的 string 对象。消息可以随时删除分配的 string 对象,因此指向该对象的引用可能会失效。否则,如果 valueNULL,则行为与调用 clear_foo() 相同。
  • string* release_foo():释放字段的所有权并返回 string 对象的指针。调用此方法后,调用者将拥有分配的 string 对象,foo() 将返回空字符串/空字节。

支持 Cord 的单一字节字段

v23.0 添加了对单一 bytes 字段(包括 oneof 字段)的 absl::Cord 支持。单一 stringrepeated stringrepeated bytes 字段不支持使用 Cord

要将单一 bytes 字段设置为使用 absl::Cord 存储数据,请使用以下语法:

// edition (default settings)
bytes foo = 25 [ctype=CORD];
bytes foo = 26 [ctype=CORD, features.field_presence = IMPLICIT];

Cord 的使用不适用于 repeated bytes 字段。Protoc 会忽略这些字段上的 [ctype=CORD] 设置。

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

  • const ::absl::Cord& foo() const:返回字段的当前值。如果字段未设置,则返回空 Cord(proto3)或默认值(proto2 和 editions)。
  • void set_foo(const ::absl::Cord& value):设置字段的值。调用此方法后,foo() 将返回 value
  • void set_foo(::absl::string_view value):设置字段的值。调用此方法后,foo() 将返回 value 作为 absl::Cord
  • void clear_foo():清除字段的值。调用此方法后,foo() 将返回空 Cord(proto3)或默认值(proto2 和 editions)。
  • bool has_foo():如果字段已设置,则返回 true。仅适用于 proto3 中的 optional 字段和 editions 中的显式存在性字段。

显式存在枚举字段

给定枚举类型

enum Bar {
  BAR_UNSPECIFIED = 0;
  BAR_VALUE = 1;
  BAR_OTHER_VALUE = 2;
}

对于具有 显式存在性 的此字段定义

Bar bar = 1;

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

  • bool has_bar() const:如果字段已设置,则返回 true
  • Bar bar() const:返回字段的当前值。如果字段未设置,则返回默认值。
  • void set_bar(Bar value):设置字段的值。调用此方法后,has_bar() 将返回 truebar() 将返回 value。在调试模式下(即,未定义 NDEBUG),如果 valueBar 的任何已定义值都不匹配,此方法将中止进程。
  • void clear_bar():清除字段的值。调用此方法后,has_bar() 将返回 falsebar() 将返回默认值。

隐式存在枚举字段

给定枚举类型

enum Bar {
  BAR_UNSPECIFIED = 0;
  BAR_VALUE = 1;
  BAR_OTHER_VALUE = 2;
}

对于具有 隐式存在性 的此字段定义

Bar bar = 1;

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

  • Bar bar() const:返回字段的当前值。如果字段未设置,则返回默认值(0)。
  • void set_bar(Bar value):设置字段的值。调用此方法后,bar() 将返回 value
  • void clear_bar():清除字段的值。调用此方法后,bar() 将返回默认值。

显式存在嵌入消息字段

给定消息类型:

message Bar {}

对于具有 显式存在性 的此字段定义

Bar bar = 1;

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

  • bool has_bar() const:如果字段已设置,则返回 true
  • const Bar& bar() const:返回字段的当前值。如果字段未设置,则返回一个字段未设置的 Bar(可能是 Bar::default_instance())。
  • Bar* mutable_bar():返回一个指向存储字段值的可修改 Bar 对象的指针。如果调用前字段未设置,则返回的 Bar 将没有字段被设置(即,它将与新分配的 Bar 相同)。调用此方法后,has_bar() 将返回 truebar() 将返回对同一个 Bar 实例的引用。
  • void clear_bar():清除字段的值。调用此方法后,has_bar() 将返回 falsebar() 将返回默认值。
  • void set_allocated_bar(Bar* value):将 Bar 对象设置到字段并释放先前存在的字段值(如果存在)。如果 Bar 指针不是 NULL,则消息将拥有分配的 Bar 对象,并且 has_bar() 将返回 true。否则,如果 BarNULL,则行为与调用 clear_bar() 相同。
  • Bar* release_bar():释放字段的所有权并返回 Bar 对象的指针。调用此方法后,调用者将拥有分配的 Bar 对象,has_bar() 将返回 falsebar() 将返回默认值。

重复数值字段

对于此字段定义:

repeated int32 foo = 1;

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

  • int foo_size() const:返回字段中当前元素的数量。要检查空集合,请考虑使用底层 RepeatedField 中的 empty() 方法而不是此方法。
  • int32_t foo(int index) const:返回给定零基索引处的元素。使用超出 [0, foo_size()) 范围的索引调用此方法会导致未定义行为。
  • void set_foo(int index, int32_t value):设置给定零基索引处元素的 قيمة。
  • void add_foo(int32_t value):将具有给定值的元素追加到字段末尾。
  • void clear_foo():从字段中删除所有元素。调用此方法后,foo_size() 将返回零。
  • const RepeatedField<int32_t>& foo() const:返回存储字段元素的底层 RepeatedField。此容器类提供类似 STL 的迭代器和其他方法。
  • RepeatedField<int32_t>* mutable_foo():返回存储字段元素的底层可修改 RepeatedField 的指针。此容器类提供类似 STL 的迭代器和其他方法。

对于其他数值字段类型(包括 bool),int32_t 会根据 标量值类型表 替换为相应的 C++ 类型。

重复字符串字段

注意: 自 2023 年版起,如果 features.(pb.cpp).string_type 设置为 VIEW,则会生成 string_view API。

对于这两个字段定义中的任何一个

repeated string foo = 1;
repeated bytes foo = 1;

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

  • int foo_size() const:返回字段中当前元素的数量。要检查空集合,请考虑使用底层 RepeatedField 中的 empty() 方法而不是此方法。
  • const string& 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 set_foo(int index, const char* value, int size):使用显式指定大小的 C 风格字符串设置给定零基索引处元素的 قيمة,而不是通过查找 null 终止符字节来确定。
  • string* mutable_foo(int index):返回一个指向存储给定零基索引处元素值的可修改 string 对象的指针。使用超出 [0, foo_size()) 范围的索引调用此方法会导致未定义行为。
  • 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 add_foo(const char* value, int size):使用显式指定大小的字符串将元素追加到字段末尾,而不是通过查找 null 终止符字节来确定。
  • string* add_foo():将一个空的字符串元素追加到字段末尾,并返回指向它的指针。
  • void clear_foo():从字段中删除所有元素。调用此方法后,foo_size() 将返回零。
  • const RepeatedPtrField<string>& foo() const:返回存储字段元素的底层 RepeatedPtrField。此容器类提供类似 STL 的迭代器和其他方法。
  • RepeatedPtrField<string>* mutable_foo():返回存储字段元素的底层可修改 RepeatedPtrField 的指针。此容器类提供类似 STL 的迭代器和其他方法。

重复枚举字段

给定枚举类型

enum Bar {
  BAR_UNSPECIFIED = 0;
  BAR_VALUE = 1;
  BAR_OTHER_VALUE = 2;
}

对于此字段定义:

repeated Bar bar = 1;

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

  • int bar_size() const:返回字段中当前元素的数量。要检查空集合,请考虑使用底层 RepeatedField 中的 empty() 方法而不是此方法。
  • Bar bar(int index) const:返回给定零基索引处的元素。使用超出 [0, bar_size()) 范围的索引调用此方法会导致未定义行为。
  • void set_bar(int index, Bar value):设置给定零基索引处元素的 قيمة。在调试模式下(即,未定义 NDEBUG),如果 valueBar 的任何已定义值都不匹配且是封闭枚举,此方法将中止进程。
  • void add_bar(Bar value):将具有给定值的元素追加到字段末尾。在调试模式下(即,未定义 NDEBUG),如果 valueBar 的任何已定义值都不匹配,此方法将中止进程。
  • void clear_bar():从字段中删除所有元素。调用此方法后,bar_size() 将返回零。
  • const RepeatedField<int>& bar() const:返回存储字段元素的底层 RepeatedField。此容器类提供类似 STL 的迭代器和其他方法。
  • RepeatedField<int>* mutable_bar():返回存储字段元素的底层可修改 RepeatedField 的指针。此容器类提供类似 STL 的迭代器和其他方法。

重复内嵌消息字段

给定消息类型:

message Bar {}

对于这些字段定义

repeated Bar bar = 1;

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

  • int bar_size() const:返回字段中当前元素的数量。要检查空集合,请考虑使用底层 RepeatedField 中的 empty() 方法而不是此方法。
  • const Bar& bar(int index) const:返回给定零基索引处的元素。使用超出 [0, bar_size()) 范围的索引调用此方法会导致未定义行为。
  • Bar* mutable_bar(int index):返回一个指向存储给定零基索引处元素值的可修改 Bar 对象的指针。使用超出 [0, bar_size()) 范围的索引调用此方法会导致未定义行为。
  • Bar* add_bar():将一个新元素追加到字段末尾并返回指向它的指针。返回的 Bar 是可修改的,并且没有任何字段被设置(即,它将与新分配的 Bar 相同)。
  • void clear_bar():从字段中删除所有元素。调用此方法后,bar_size() 将返回零。
  • const RepeatedPtrField<Bar>& bar() const:返回存储字段元素的底层 RepeatedPtrField。此容器类提供类似 STL 的迭代器和其他方法。
  • RepeatedPtrField<Bar>* mutable_bar():返回存储字段元素的底层可修改 RepeatedPtrField 的指针。此容器类提供类似 STL 的迭代器和其他方法。

Oneof 数值字段

对于这个 oneof 字段定义

oneof example_name {
    int32 foo = 1;
    ...
}

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

  • bool has_foo() const:如果 oneof 情况是 kFoo,则返回 true
  • int32 foo() const:如果 oneof 情况是 kFoo,则返回字段的当前值。否则,返回默认值。
  • void set_foo(int32 value):
    • 如果同一 oneof 中的任何其他 oneof 字段已设置,则调用 clear_example_name()
    • 设置此字段的值并将 oneof 情况设置为 kFoo
    • has_foo() 将返回 true,foo() 将返回 valueexample_name_case() 将返回 kFoo
  • void clear_foo():
    • 如果 oneof 情况不是 kFoo,则不会进行任何更改。
    • 如果 oneof 情况是 kFoo,则清除字段值和 oneof 情况。has_foo() 将返回 falsefoo() 将返回默认值,example_name_case() 将返回 EXAMPLE_NAME_NOT_SET

对于其他数值字段类型(包括 bool),int32_t 会根据 标量值类型表 替换为相应的 C++ 类型。

Oneof 字符串字段

注意: 自 2023 年版起,可能会生成 string_view API。

对于这些 oneof 字段定义中的任何一个

oneof example_name {
    string foo = 1;
    ...
}
oneof example_name {
    bytes foo = 1;
    ...
}

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

  • bool has_foo() const:如果 oneof 情况是 kFoo,则返回 true
  • const string& foo() const:如果 oneof 情况是 kFoo,则返回字段的当前值。否则,返回默认值。
  • void set_foo(::absl::string_view value):
    • 如果同一 oneof 中的任何其他 oneof 字段已设置,则调用 clear_example_name()
    • 设置此字段的值并将 oneof 情况设置为 kFoo
    • has_foo() 将返回 truefoo() 将返回 value 的副本,example_name_case() 将返回 kFoo
  • void set_foo(const string& value):与第一个 set_foo() 类似,但从 const 字符串引用复制。
  • void set_foo(string&& value):与第一个 set_foo() 类似,但移动传递的字符串。
  • void set_foo(const char* value):与第一个 set_foo() 类似,但从 C 风格的以 null 终止的字符串复制。
  • void set_foo(const char* value, int size):与第一个 set_foo() 类似,但从具有显式指定大小的字符串复制,而不是通过查找 null 终止符字节来确定。
  • string* mutable_foo():
    • 如果同一 oneof 中的任何其他 oneof 字段已设置,则调用 clear_example_name()
    • 将 oneof 情况设置为 kFoo 并返回指向存储字段值的可修改字符串对象的指针。如果调用前 oneof 情况不是 kFoo,则返回的字符串将为空(不是默认值)。
    • has_foo() 将返回 truefoo() 将返回写入给定字符串的任何值,example_name_case() 将返回 kFoo
  • void clear_foo():
    • 如果 oneof 情况不是 kFoo,则不会进行任何更改。
    • 如果 oneof 情况是 kFoo,则释放字段并清除 oneof 情况。has_foo() 将返回 falsefoo() 将返回默认值,example_name_case() 将返回 EXAMPLE_NAME_NOT_SET
  • void set_allocated_foo(string* value):
    • 调用 clear_example_name()
    • 如果字符串指针不是 NULL:将字符串对象设置到字段并将 oneof 情况设置为 kFoo。消息将拥有分配的字符串对象,has_foo() 将返回 trueexample_name_case() 将返回 kFoo
    • 如果字符串指针为 NULL,则 has_foo() 将返回 falseexample_name_case() 将返回 EXAMPLE_NAME_NOT_SET
  • string* release_foo():
    • 如果 oneof 情况不是 kFoo,则返回 NULL
    • 清除 oneof 情况,释放字段的所有权,并返回字符串对象的指针。调用此方法后,调用者将拥有分配的字符串对象,has_foo() 将返回 false,foo() 将返回默认值,example_name_case() 将返回 EXAMPLE_NAME_NOT_SET

Oneof 枚举字段

给定枚举类型

enum Bar {
  BAR_UNSPECIFIED = 0;
  BAR_VALUE = 1;
  BAR_OTHER_VALUE = 2;
}

对于 oneof 字段定义

oneof example_name {
    Bar bar = 1;
    ...
}

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

  • bool has_bar() const:如果 oneof 情况是 kBar,则返回 true
  • Bar bar() const:如果 oneof 情况是 kBar,则返回字段的当前值。否则,返回一个字段未设置的 Bar(可能是 Bar::default_instance())。
  • void set_bar(Bar value):
    • 如果同一 oneof 中的任何其他 oneof 字段已设置,则调用 clear_example_name()
    • 设置此字段的值并将 oneof 情况设置为 kBar
    • has_bar() 将返回 truebar() 将返回 valueexample_name_case() 将返回 kBar
    • 在调试模式下(即,未定义 NDEBUG),如果 valueBar 的任何已定义值都不匹配且是封闭枚举,此方法将中止进程。
  • void clear_bar():
    • 如果 oneof 情况不是 kBar,则不会进行任何更改。
    • 如果 oneof 情况是 kBar,则清除字段值和 oneof 情况。has_bar() 将返回 falsebar() 将返回默认值,example_name_case() 将返回 EXAMPLE_NAME_NOT_SET

Oneof 嵌入消息字段

给定消息类型:

message Bar {}

对于 oneof 字段定义

oneof example_name {
    Bar bar = 1;
    ...
}

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

  • bool has_bar() const:如果 oneof 情况是 kBar,则返回 true。
  • const Bar& bar() const:如果 oneof 情况是 kBar,则返回字段的当前值。否则,返回一个字段未设置的 Bar(可能是 Bar::default_instance())。
  • Bar* mutable_bar():
    • 如果同一 oneof 中的任何其他 oneof 字段已设置,则调用 clear_example_name()
    • 将 oneof 情况设置为 kBar 并返回指向存储字段值的可修改 Bar 对象的指针。如果调用前 oneof 情况不是 kBar,则返回的 Bar 将没有任何字段被设置(即,它将与新分配的 Bar 相同)。
    • 调用此方法后,has_bar() 将返回 truebar() 将返回对同一个 Bar 实例的引用,example_name_case() 将返回 kBar
  • void clear_bar():
    • 如果 oneof 情况不是 kBar,则不会进行任何更改。
    • 如果 oneof 情况等于 kBar,则释放字段并清除 oneof 情况。has_bar() 将返回 falsebar() 将返回默认值,example_name_case() 将返回 EXAMPLE_NAME_NOT_SET
  • void set_allocated_bar(Bar* bar):
    • 调用 clear_example_name()
    • 如果 Bar 指针不是 NULL:将 Bar 对象设置到字段并将 oneof 情况设置为 kBar。消息将拥有分配的 Bar 对象,has_bar() 将返回 true,example_name_case() 将返回 kBar
    • 如果指针为 NULL,则 has_bar() 将返回 falseexample_name_case() 将返回 EXAMPLE_NAME_NOT_SET。(行为类似于调用 clear_example_name()
  • Bar* release_bar():
    • 如果 oneof 情况不是 kBar,则返回 NULL
    • 如果 oneof 情况是 kBar,则清除 oneof 情况,释放字段的所有权,并返回 Bar 对象的指针。调用此方法后,调用者将拥有分配的 Bar 对象,has_bar() 将返回 falsebar() 将返回默认值,example_name_case() 将返回 EXAMPLE_NAME_NOT_SET

映射字段

对于此 map 字段定义:

map<int32, int32> weight = 1;

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

  • const google::protobuf::Map<int32, int32>& weight();:返回一个不可修改的 Map
  • google::protobuf::Map<int32, int32>* mutable_weight();:返回一个可修改的 Map

google::protobuf::Map 是一种特殊的容器类型,在协议缓冲区中用于存储 map 字段。从其下面的接口可以看出,它使用了 std::mapstd::unordered_map 方法中常用的子集。

template<typename Key, typename T> {
class Map {
  // Member types
  typedef Key key_type;
  typedef T mapped_type;
  typedef MapPair< Key, T > value_type;

  // Iterators
  iterator begin();
  const_iterator begin() const;
  const_iterator cbegin() const;
  iterator end();
  const_iterator end() const;
  const_iterator cend() const;
  // Capacity
  int size() const;
  bool empty() const;

  // Element access
  T& operator[](const Key& key);
  const T& at(const Key& key) const;
  T& at(const Key& key);

  // Lookup
  bool contains(const Key& key) const;
  int count(const Key& key) const;
  const_iterator find(const Key& key) const;
  iterator find(const Key& key);

  // Modifiers
  pair<iterator, bool> insert(const value_type& value);
  template<class InputIt>
  void insert(InputIt first, InputIt last);
  size_type erase(const Key& Key);
  iterator erase(const_iterator pos);
  iterator erase(const_iterator first, const_iterator last);
  void clear();

  // Copy
  Map(const Map& other);
  Map& operator=(const Map& other);
}

添加数据的最简单方法是使用常规 map 语法,例如:

std::unique_ptr<ProtoName> my_enclosing_proto(new ProtoName);
(*my_enclosing_proto->mutable_weight())[my_key] = my_value;

pair<iterator, bool> insert(const value_type& value) 会隐式深度复制 value_type 实例。将新值插入 google::protobuf::Map 的最有效方法如下:

T& operator[](const Key& key): map[new_key] = new_mapped;

google::protobuf::Map 与标准 map 一起使用

google::protobuf::Map 支持与 std::mapstd::unordered_map 相同的迭代器 API。如果您不想直接使用 google::protobuf::Map,可以通过以下方法将 google::protobuf::Map 转换为标准 map:

std::map<int32, int32> standard_map(message.weight().begin(),
                                    message.weight().end());

请注意,这将对整个 map 进行深度复制。

您也可以通过以下方式从标准 map 构建 google::protobuf::Map

google::protobuf::Map<int32, int32> weight(standard_map.begin(), standard_map.end());

解析未知值

在传输时,一个 .proto map 等同于每个键/值对的 map 条目消息,而 map 本身是 map 条目的重复字段。与普通消息类型一样,解析的 map 条目消息可能包含未知字段:例如,在定义为 map<int32, string> 的 map 中,类型为 int64 的字段。

如果在 map 条目消息的传输格式中有未知字段,它们将被丢弃。

如果在 map 条目消息的传输格式中有未知的枚举值,它在 proto2、proto3 和 editions 中的处理方式不同。在 proto2 中,整个 map 条目消息将被放入包含消息的未知字段集中。在 proto3 中,它被放入 map 字段,就好像它是已知的枚举值一样。使用 editions 时,默认情况下它会镜像 proto3 的行为。如果 features.enum_type 设置为 CLOSED,则它会镜像 proto2 的行为。

Any

给定一个像这样的 Any 字段

import "google/protobuf/any.proto";

message ErrorStatus {
  string message = 1;
  google.protobuf.Any details = 2;
}

在我们生成的代码中,details 字段的 getter 返回 google::protobuf::Any 的实例。这提供了以下特殊方法来打包和解包 Any 的值:

class Any {
 public:
  // Packs the given message into this Any using the default type URL
  // prefix “type.googleapis.com”. Returns false if serializing the message failed.
  bool PackFrom(const google::protobuf::Message& message);

  // Packs the given message into this Any using the given type URL
  // prefix. Returns false if serializing the message failed.
  bool PackFrom(const google::protobuf::Message& message,
                ::absl::string_view type_url_prefix);

  // Unpacks this Any to a Message. Returns false if this Any
  // represents a different protobuf type or parsing fails.
  bool UnpackTo(google::protobuf::Message* message) const;

  // Returns true if this Any represents the given protobuf type.
  template<typename T> bool Is() const;
}

Oneof

给定一个像这样的 oneof 定义

oneof example_name {
    int32 foo_int = 4;
    string foo_string = 9;
    ...
}

编译器将生成以下 C++ 枚举类型:

enum ExampleNameCase {
  kFooInt = 4,
  kFooString = 9,
  EXAMPLE_NAME_NOT_SET = 0
}

此外,它将生成以下方法:

  • ExampleNameCase example_name_case() const:返回指示哪个字段已设置的枚举。如果没有任何字段被设置,则返回 EXAMPLE_NAME_NOT_SET
  • void clear_example_name():如果 oneof 字段集使用指针(Message 或 String),则释放对象,并将 oneof 情况设置为 EXAMPLE_NAME_NOT_SET

枚举

注意: 自 2024 年版起,在某些功能设置下可能会生成 string_view API。有关更多信息,请参阅 枚举名称助手

给定一个枚举定义,例如:

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

Protocol buffer 编译器将生成一个名为 Foo 的 C++ 枚举类型,其中包含相同的取值集合。此外,编译器将生成以下函数:

  • const EnumDescriptor* Foo_descriptor():返回类型的描述符,其中包含有关此枚举类型定义了哪些取值的相关信息。
  • bool Foo_IsValid(int value):如果给定的数值与 Foo 的定义值之一匹配,则返回 true。在上面的示例中,如果输入是 0、5 或 1234,它将返回 true
  • const string& Foo_Name(int value):返回给定数值的名称。如果不存在这样的值,则返回空字符串。如果多个值具有此数字,则返回第一个定义的。在上面的示例中,Foo_Name(5) 将返回 "VALUE_B"
  • bool Foo_Parse(::absl::string_view name, Foo* value):如果 name 是此枚举的有效值名称,则将其值分配给 value 并返回 true。否则返回 false。在上面的示例中,Foo_Parse("VALUE_C", &some_foo) 将返回 true 并将 some_foo 设置为 1234。
  • const Foo Foo_MIN:枚举的最小有效值(示例中的 VALUE_A)。
  • const Foo Foo_MAX:枚举的最大有效值(示例中的 VALUE_C)。
  • const int Foo_ARRAYSIZE:始终定义为 Foo_MAX + 1

在将整数转换为 proto2 枚举时要小心。 如果将整数转换为 proto2 枚举值,则整数必须是该枚举的有效值之一,否则结果可能未定义。如有疑问,请使用生成的 Foo_IsValid() 函数来测试转换是否有效。将 proto2 消息的枚举类型字段设置为无效值可能会导致断言失败。在解析 proto2 消息时读取无效枚举值将被视为未知字段。这些语义在 proto3 中已更改。将任何整数安全地转换为 proto3 枚举值,只要它适合 int32。在解析 proto3 消息时,无效的枚举值也会被保留,并通过枚举字段访问器返回。

在使用 proto3 和 editions 枚举进行 switch 语句时要小心。 Proto3 和 editions 枚举是开放枚举类型,可能存在超出指定符号范围的值。(Editions 枚举可以使用 enum_type 功能设置为封闭枚举。)对于开放枚举类型,未识别的枚举值在解析消息时将被保留,并通过枚举字段访问器返回。即使列出了所有已知字段,在没有默认 case 的开放枚举上的 switch 语句也无法捕获所有情况。这可能导致意外行为,包括数据损坏和运行时崩溃。始终添加 default case 或在 switch 外部显式调用 Foo_IsValid(int) 来处理未知枚举值。

您可以在消息类型内部定义一个枚举。在这种情况下,protocol buffer 编译器生成的代码会使其看起来好像枚举类型本身被声明为嵌套在消息的类中。Foo_descriptor()Foo_IsValid() 函数被声明为静态方法。实际上,枚举类型本身及其值在全局范围内以 mangled 名称声明,并通过 typedef 和一系列常量定义导入到类的作用域中。这是为了解决声明顺序问题。不要依赖 mangled 的顶层名称;假装枚举确实嵌套在消息类中。

Abseil flag 支持

生成的 enum 值对 Abseil 的标志解析/取消解析逻辑具有原生支持。它们可用作 ABSL_FLAG 声明的类型。

标志解析器同时支持标签和数字。无效的标签/数字会导致解析失败。

扩展 (仅限 proto2 和 editions)

给定一个带有扩展范围的消息

message Foo {
  extensions 100 to 199;
}

Protocol buffer 编译器将为 Foo 生成一些额外的方​​法:HasExtension()ExtensionSize()ClearExtension()GetExtension()SetExtension()MutableExtension()AddExtension()SetAllocatedExtension()ReleaseExtension()。这些方法中的每一个都以扩展标识符(稍后在本节中描述)作为其第一个参数,该标识符标识一个扩展字段。其余参数和返回值与为与扩展标识符相同类型的普通(非扩展)字段生成的相应访问器方法完全相同。(GetExtension() 对应于没有特殊前缀的访问器。)

给定一个扩展定义:

extend Foo {
  optional int32 bar = 123;
  repeated int32 repeated_bar = 124;
  optional Bar message_bar = 125;
}

对于单一扩展字段 bar,protocol buffer 编译器生成一个名为 bar 的“扩展标识符”,您可以使用 Foo 的扩展访问器来访问此扩展,如下所示:

Foo foo;
assert(!foo.HasExtension(bar));
foo.SetExtension(bar, 1);
assert(foo.HasExtension(bar));
assert(foo.GetExtension(bar) == 1);
foo.ClearExtension(bar);
assert(!foo.HasExtension(bar));

对于消息扩展字段 message_bar,如果该字段未设置,foo.GetExtension(message_bar) 将返回一个字段未设置的 Bar(可能是 Bar::default_instance())。

同样,对于重复扩展字段 repeated_bar,编译器生成一个名为 repeated_bar 的扩展标识符,您也可以将其与 Foo 的扩展访问器一起使用:

Foo foo;
for (int i = 0; i < kSize; ++i) {
  foo.AddExtension(repeated_bar, i)
}
assert(foo.ExtensionSize(repeated_bar) == kSize)
for (int i = 0; i < kSize; ++i) {
  assert(foo.GetExtension(repeated_bar, i) == i)
}

(扩展标识符的确切实现很复杂,并且涉及到模板的魔法使用——但是,您不需要担心扩展标识符的工作原理即可使用它们。)

扩展可以声明在另一种类型内部嵌套。例如,一种常见的模式是这样做:

message Baz {
  extend Foo {
    optional Baz foo_ext = 124;
  }
}

在这种情况下,扩展标识符 foo_extBaz 内部声明。它可以如下使用:

Foo foo;
Baz* baz = foo.MutableExtension(Baz::foo_ext);
FillInMyBaz(baz);

Arena 分配

Arena 分配是一项仅 C++ 的功能,可帮助您优化内存使用并提高处理协议缓冲区时的性能。在 .proto 文件中启用 Arena 分配会为使用 Arena 添加额外的代码到您的 C++ 生成代码中。您可以在 Arena 分配指南 中找到有关 Arena 分配 API 的更多信息。

服务(Services)

如果 .proto 文件包含以下行:

option cc_generic_services = true;

那么 Protocol buffer 编译器将根据文件中找到的服务定义生成代码,如本节所述。但是,生成的代码可能不理想,因为它不与任何特定的 RPC 系统挂钩,因此需要比为某个系统定制的代码更多的间接级别。如果您不希望生成此代码,请在文件中添加此行:

option cc_generic_services = false;

如果上述两行都未给出,该选项默认为 false,因为通用服务已被弃用。(请注意,在 2.4.0 之前,该选项默认为 true

基于 .proto 语言服务定义的 RPC 系统应提供插件来生成适合该系统的代码。这些插件很可能要求禁用抽象服务,以便它们可以生成自己同名的类。

本节的其余部分描述了当启用抽象服务时,Protocol Buffer 编译器会生成什么。

接口

给定一个服务定义:

service Foo {
  rpc Bar(FooRequest) returns(FooResponse);
}

Protocol buffer 编译器将生成一个名为 Foo 的类来表示此服务。Foo 将为服务定义中定义的每个方法提供一个虚拟方法。在这种情况下,Bar 方法定义如下:

virtual void Bar(RpcController* controller, const FooRequest* request,
                 FooResponse* response, Closure* done);

参数等同于 Service::CallMethod() 的参数,只是 method 参数是隐含的,而 requestresponse 指定了它们的精确类型。

这些生成的方​​法是虚拟的,但不是纯虚的。默认实现只是调用 controller->SetFailed() 并显示一个指示方法未实现的错误消息,然后调用 done 回调。实现自己的服务时,必须继承此生成的服务并根据需要实现其方法。

FooService 接口的子类。Protocol Buffer 编译器会自动生成 Service 方法的实现,如下所示:

  • GetDescriptor:返回服务的 ServiceDescriptor
  • CallMethod:根据提供的方法描述符确定正在调用哪个方法,并直接调用它,将请求和响应消息对象向下转换为正确的类型。
  • GetRequestPrototypeGetResponsePrototype:为给定方法返回正确类型的请求或响应的默认实例。

还生成了以下静态方法:

  • static ServiceDescriptor descriptor():返回类型的描述符,其中包含有关此服务有哪些方法以及它们的输入和输出类型的信息。

存根

Protocol buffer 编译器还为每个服务接口生成一个“stub”实现,供希望向实现该服务的服务器发送请求的客户端使用。对于(前面描述的)Foo 服务,将定义 stub 实现 Foo_Stub。与嵌套消息类型一样,使用 typedef,因此 Foo_Stub 也可以称为 Foo::Stub

Foo_StubFoo 的子类,它还实现了以下方法:

  • Foo_Stub(RpcChannel* channel):构造一个在给定通道上发送请求的新 stub。
  • Foo_Stub(RpcChannel* channel, ChannelOwnership ownership):构造一个在给定通道上发送请求的新 stub,并可能拥有该通道。如果 ownershipService::STUB_OWNS_CHANNEL,则当 stub 对象被删除时,它也将删除该通道。
  • RpcChannel* channel():返回此 stub 的通道,如构造函数中传递的。

stub 另外实现了每个服务的方法,作为通道的包装器。调用其中一个方法只是调用 channel->CallMethod()

Protocol Buffer 库不包含 RPC 实现。但是,它包含了将生成的服务类连接到您选择的任何任意 RPC 实现所需的所有工具。您只需要提供 RpcChannelRpcController 的实现。有关更多信息,请参阅 service.h 的文档。

插件插入点

希望扩展 C++ 代码生成器输出的 代码生成器插件 可以使用给定的插入点名称插入以下类型的代码。除非另有说明,每个插入点都会出现在 .pb.cc 文件和 .pb.h 文件中。

  • includes:Include 指令。
  • namespace_scope:属于文件包/命名空间但不在任何特定类内的声明。出现在所有其他命名空间范围代码之后。
  • global_scope:属于顶层的声明,在文件命名空间之外。出现在文件的末尾。
  • class_scope:TYPENAME:属于消息类的成员声明。TYPENAME 是完整的 proto 名称,例如 package.MessageType。出现在类中所有其他公共声明之后。此插入点仅出现在 .pb.h 文件中。

不要生成依赖于标准代码生成器声明的私有类成员的代码,因为这些实现细节在未来版本的 Protocol Buffers 中可能会改变。