注意:此网站已被弃用。该网站将在 2023 年 1 月 31 日后关闭,而流量将重定向到位于 https://protobuf.dev 的新网站。在此期间,我们仅会针对 protobuf.dev 进行更新。

C++ 生成的代码

使用集合让一切井井有条 根据您的偏好保存内容并对其进行分类。

本页面准确说明了协议缓冲区编译器针对任何给定的协议定义生成的 C++ 代码。proto2 和 proto3 生成的代码之间的任何差异都会突出显示 - 请注意,这些差异位于生成的代码中,如本文档中所述,而不是在两个版本中相同的基本消息类/接口。在阅读本文档之前,您应阅读 proto2 语言指南和/或 proto3 语言指南

编译器调用

协议缓冲区编译器在使用 --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它们必须已存在。

软件包

如果 .proto 文件包含 package 声明,则文件的全部内容都将放入相应的 C++ 命名空间中。例如,假设存在 package 声明:

package foo.bar;

文件中的所有声明都将位于 foo::bar 命名空间中。

信息

假设有一个简单的消息声明:

message Foo {}

协议缓冲区编译器会生成一个名为 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.so (libprotobuf.lib) 相关联。“精简版”库比完整库小得多,而且更适合手机资源有限的系统。

不应创建自己的 Foo 子类。如果您创建此类的子类并替换某个虚拟方法,系统会忽略替换,因为许多生成的方法调用会进行去虚拟化以提高性能。

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

  • bool ParseFromString(const string& 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:返回解析此消息时遇到的一组未知字段。
  • UnknownFieldSet* mutable_unknown_fields():返回一个指针,指向解析此消息时遇到的可变未知字段集。

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

  • static const Descriptor* descriptor():返回类型的描述符。其中包含有关类型的信息,包括其字段及其类型。此 API 可以与反射一起使用,以编程方式检查字段。
  • static const Foo& default_instance():返回 Foo 的单例常量实例,它与新构建的 Foo 实例完全相同(因此,所有单数字段均未设置,所有重复字段均为空)。请注意,您可以通过调用其 New() 方法将消息的默认实例用作工厂。

嵌套类型

一条消息可以在另一个消息内声明。例如: message Foo { message Bar { } }

在本例中,编译器会生成两个类:FooFoo_Bar。此外,编译器会在 Foo 中生成一个类型定义符,如下所示:

typedef Foo_Bar Bar;

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

字段

除了上一部分中介绍的方法外,协议缓冲区编译器还会为 .proto 文件中的消息中定义的每个字段生成一组访问器方法。这些方法采用小写/蛇形格式,例如 has_foo()clear_foo()

与访问器方法一样,编译器会为包含其字段编号的每个字段生成一个整数常量。常量名称为字母 k,后跟转换为驼峰式大小写的字段名称,后跟 FieldNumber。例如,在 optional int32 foo_bar = 5; 字段中,编译器会生成常量 static const int kFooBarFieldNumber = 5;

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

对于返回指针的字段访问器,在对消息进行下一次修改或不修改访问时,该指针可能会失效。这包括不考虑任何常量、调用任何字段的任何访问器、调用从 Message 继承的任何方法或通过其他方式访问消息(例如,使用复制构造函数复制消息)。因此,在两次不同的访问器调用中,返回的指针值无法保证始终相同。

单数字段 (proto2)

对于以下任一字段定义:

optional int32 foo = 1;
required int32 foo = 1;

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

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

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

单数字段 (proto3)

对于此字段定义:

int32 foo = 1;

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

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

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

单字符串字段 (proto2)

对于以下任一字段定义:

optional string foo = 1;
required string foo = 1;
optional bytes foo = 1;
required bytes foo = 1;

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

  • bool has_foo() const:如果设置了该字段,则返回 true
  • const string& foo() const:返回字段的当前值。如果该字段未设置,则返回默认值。
  • void set_foo(const string& value):设置该字段的值。调用此方法后,has_foo() 将返回 true,而 foo() 将返回 value 的副本。
  • void set_foo(string&& value)(C++11 及更高版本):设置字段的值,从传递的字符串移走。调用此方法后,has_foo() 将返回 true,而 foo() 将返回 value 的副本。
  • void set_foo(const char* value):使用 C 样式的 null 终止字符串设置字段的值。调用此方法后,has_foo() 将返回 true,而 foo() 将返回 value 的副本。
  • void set_foo(const char* value, int size):与上面类似,但字符串大小是显式提供的,而不是通过查找 null 终止符字节来确定。
  • string* mutable_foo():返回一个指针,指向存储相应字段值的可变 string 对象。如果在调用之前未设置该字段,则返回的字符串将为空(不是默认值)。调用此方法后,has_foo() 将返回 true,而 foo() 将返回写入指定字符串中的任何值。
  • void clear_foo():清除该字段的值。调用此方法后,has_foo() 将返回 false,而 foo() 将返回默认值。
  • 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() 将返回默认值。

单字符串字段 (proto3)

对于以下任一字段定义:

string foo = 1;
bytes foo = 1;

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

  • const string& foo() const:返回字段的当前值。如果未设置此字段,则返回空字符串/空字节。
  • void set_foo(const string& value):设置该字段的值。调用此方法后,foo() 将返回 value 的副本。
  • void set_foo(string&& value)(C++11 及更高版本):设置字段的值,从传递的字符串移走。调用此方法后,foo() 将返回 value 的副本。
  • void set_foo(const char* value):使用 C 样式的 null 终止字符串设置字段的值。调用此方法后,foo() 将返回 value 的副本。
  • void set_foo(const char* value, int size):与上面类似,但字符串大小是显式提供的,而不是通过查找 null 终止符字节来确定。
  • 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() 将返回空字符串/空字节。

单数枚举字段 (proto2)

根据枚举类型:

enum Bar {
  BAR_VALUE = 0;
  OTHER_VALUE = 1;
}

对于以下任一字段定义:

optional Bar foo = 1;
required Bar foo = 1;

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

  • bool has_foo() const:如果设置了该字段,则返回 true
  • Bar foo() const:返回字段的当前值。如果该字段未设置,则返回默认值。
  • void set_foo(Bar value):设置该字段的值。调用此函数后,has_foo() 将返回 true,而 foo() 将返回 value。在调试模式下(即未定义 NDEBUG),如果 value 与为 Bar 定义的任何值均不匹配,此方法将取消该进程。
  • void clear_foo():清除该字段的值。调用此方法后,has_foo() 将返回 false,而 foo() 将返回默认值。

单数枚举字段 (proto3)

根据枚举类型:

enum Bar {
  BAR_VALUE = 0;
  OTHER_VALUE = 1;
}

对于此字段定义:

Bar foo = 1;

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

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

单个嵌入式消息字段

根据消息类型:

message Bar {}

对于以下任一字段定义:

//proto2
optional Bar foo = 1;
required Bar foo = 1;
//proto3
Bar foo = 1;

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

  • bool has_foo() const:如果设置了该字段,则返回 true
  • const Bar& foo() const:返回字段的当前值。如果未设置此字段,则返回 Bar,但未设置其任何字段(可能是 Bar::default_instance())。
  • Bar* mutable_foo():返回一个指针,指向存储相应字段值的可变 Bar 对象。如果在调用之前未设置此字段,则返回的 Bar 将不会设置任何字段(也就是说,与新分配的 Bar 完全相同)。调用此函数后,has_foo() 将返回 truefoo() 将返回对同一 Bar 实例的引用。
  • void clear_foo():清除该字段的值。调用此方法后,has_foo() 将返回 false,而 foo() 将返回默认值。
  • void set_allocated_foo(Bar* bar):将 Bar 对象设置为该字段,并释放先前的字段值(如果存在)。如果 Bar 指针不是 NULL,则消息将获得已分配的 Bar 对象的所有权,并且 has_foo() 将返回 true。否则,如果 BarNULL,则行为与调用 clear_foo() 相同。
  • Bar* release_foo():释放字段的所有权,并返回 Bar 对象的指针。调用此方法后,调用方将获得所分配 Bar 对象的所有权,has_foo() 将返回 falsefoo() 将返回默认值。

重复的数字字段

对于此字段定义:

repeated int32 foo = 1;

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

  • int foo_size() const:返回当前字段中的元素数量。
  • int32 foo(int index) const:返回位于指定索引(从零开始)处的元素。如果使用 [0, foo_size()) 之外的索引调用此方法,则会产生未定义的行为。
  • void set_foo(int index, int32 value):设置元素在给定索引(从零开始)处的值。
  • void add_foo(int32 value):将新元素附加到具有给定值的字段末尾。
  • void clear_foo():从该字段中移除所有元素。调用此方法后,foo_size() 将返回零。
  • const RepeatedField<int32>& foo() const:返回存储字段元素的底层 RepeatedField。此容器类提供类似于 STL 的迭代器和其他方法。
  • RepeatedField<int32>* mutable_foo():返回一个指针,指向存储该字段元素的底层可变 RepeatedField。此容器类提供类似于 STL 的迭代器和其他方法。

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

重复的字符串字段

对于以下任一字段定义:

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

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

  • int foo_size() const:返回当前字段中的元素数量。
  • const string& foo(int index) const:返回位于指定索引(从零开始)处的元素。如果使用 [0, foo_size()) 之外的索引调用此方法,则会产生未定义的行为。
  • void set_foo(int index, const string& value):设置元素在给定索引(从零开始)处的值。
  • void set_foo(int index, const char* value):使用 C 样式的 null 终止字符串,设置元素在给定索引(从零开始)处的值。
  • void set_foo(int index, const char* value, int size):与上面类似,但字符串大小是显式提供的,而不是通过查找 null 终止符字节来确定。
  • string* mutable_foo(int index):返回一个指向可变 string 对象的指针,该对象在指定的索引值(从零开始)内存储元素的值。如果使用 [0, foo_size()) 之外的索引调用此方法,则会产生未定义的行为。
  • void add_foo(const 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_VALUE = 0;
  OTHER_VALUE = 1;
}

对于此字段定义:

repeated Bar foo = 1;

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

  • int foo_size() const:返回当前字段中的元素数量。
  • Bar foo(int index) const:返回位于指定索引(从零开始)处的元素。如果使用 [0, foo_size()) 之外的索引调用此方法,则会产生未定义的行为。
  • void set_foo(int index, Bar value):设置元素在给定索引(从零开始)处的值。在调试模式下(即未定义 NDEBUG),如果 value 与为 Bar 定义的任何值均不匹配,此方法将取消该进程。
  • void add_foo(Bar value):将新元素附加到具有给定值的字段末尾。在调试模式下(即未定义 NDEBUG),如果 value 与为 Bar 定义的任何值均不匹配,此方法将取消该进程。
  • void clear_foo():从该字段中移除所有元素。调用此方法后,foo_size() 将返回零。
  • const RepeatedField<int>& foo() const:返回存储字段元素的底层 RepeatedField。此容器类提供类似于 STL 的迭代器和其他方法。
  • RepeatedField<int>* mutable_foo():返回一个指针,指向存储该字段元素的底层可变 RepeatedField。此容器类提供类似于 STL 的迭代器和其他方法。

重复的嵌入式消息字段

根据消息类型:

message Bar {}

对于此字段定义:

repeated Bar foo = 1;

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

  • int foo_size() const:返回当前字段中的元素数量。
  • const Bar& foo(int index) const:返回位于指定索引(从零开始)处的元素。如果使用 [0, foo_size()) 之外的索引调用此方法,则会产生未定义的行为。
  • Bar* mutable_foo(int index):返回一个指向可变 Bar 对象的指针,该对象在指定的索引值(从零开始)内存储元素的值。如果使用 [0, foo_size()) 之外的索引调用此方法,则会产生未定义的行为。
  • Bar* add_foo():在字段末尾添加新元素并返回一个指向它的指针。返回的 Bar 是可变的,并且不会设置其任何字段(也就是说,它与新分配的 Bar 完全相同)。
  • void clear_foo():从该字段中移除所有元素。调用此方法后,foo_size() 将返回零。
  • const RepeatedPtrField<Bar>& foo() const:返回存储字段元素的底层 RepeatedPtrField。此容器类提供类似于 STL 的迭代器和其他方法。
  • RepeatedPtrField<Bar>* mutable_foo():返回一个指针,指向存储该字段元素的底层可变 RepeatedPtrField。此容器类提供类似于 STL 的迭代器和其他方法。

其中一个数字字段

对于此 oneof 字段定义:

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

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

  • bool has_foo() const(仅限 proto2):如果其中之一是 kFoo,则返回 true
  • int32 foo() const:如果任一情况为 kFoo,则返回字段的当前值。否则,返回默认值。
  • void set_foo(int32 value)
    • 如果设置了同一 one 中任何其他一个字段,则调用 clear_example_name()
    • 设置此字段的值,并将单例设为 kFoo
    • has_foo()(仅适用于 proto2)将返回 true、foo() 将返回 value,而 example_name_case() 将返回 kFoo
  • void clear_foo()
    • 如果其中一个不是 kFoo,则系统不会进行任何更改。
    • 如果 oneof 为 kFoo,则清除字段和 oneof case。has_foo()(仅适用于 proto2)将返回 falsefoo() 将返回默认值,example_name_case() 将返回 EXAMPLE_NAME_NOT_SET

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

Oneof 字符串字段

对于以下任一字段定义:

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

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

  • bool has_foo() const:如果其中一个为 kFoo,则返回 true
  • const string& foo() const:如果其中一个类型为 kFoo,则返回字段的当前值。否则,返回默认值。
  • void set_foo(const string& value)
    • 如果设置了同一 one 中任何其他一个字段,则调用 clear_example_name()
    • 设置此字段的值,并将单例设为 kFoo
    • has_foo() 将返回 truefoo() 将返回 value 的副本,example_name_case() 将返回 kFoo
  • void set_foo(const char* value)
    • 如果设置了同一 one 中任何其他一个字段,则调用 clear_example_name()
    • 使用以 C 样式的 null 终止字符串的形式设置字段的值,并将单引号设为 kFoo
    • has_foo() 将返回 truefoo() 将返回 value 的副本,example_name_case() 将返回 kFoo
  • void set_foo(const char* value, int size):与上面类似,但字符串大小是显式提供的,而不是通过查找 null 终止符字节来确定。
  • string* mutable_foo()
    • 如果设置了同一 one 中任何其他一个字段,则调用 clear_example_name()
    • 将 case 设置为 kFoo,并返回指向用于存储该字段值的可变字符串对象的指针。如果在调用前,该用例不是 kFoo,则返回的字符串将为空(而不是默认值)。
    • has_foo() 将返回 truefoo() 将返回写入指定字符串的任何值,example_name_case() 将返回 kFoo
  • void clear_foo()
    • 如果此单一请求的情况不是 kFoo,则不会发生任何变化。
    • 如果 oneof case 为 kFoo,则释放该字段并清除 oneof case。has_foo() 将返回 falsefoo() 将返回默认值,example_name_case() 将返回 EXAMPLE_NAME_NOT_SET
  • void set_allocated_foo(string* value)
    • 调用 clear_example_name()
    • 如果字符串指针不是 NULL:将字符串对象设置为相应字段,并将任一情况设为 kFoo。消息拥有所分配字符串对象的所有权,has_foo() 将返回 trueexample_name_case() 将返回 kFoo
    • 如果字符串指针为 NULLhas_foo() 将返回 false,而 example_name_case() 将返回 EXAMPLE_NAME_NOT_SET
  • string* release_foo()
    • 如果其中一个不是 kFoo,则返回 NULL
    • 清除此一种情况,释放字段的所有权,并返回字符串对象的指针。调用此方法后,调用方将获得所分配字符串对象的所有权,has_foo() 将返回 false,foo() 将返回默认值,example_name_case() 将返回 EXAMPLE_NAME_NOT_SET

Oneof Enum 字段

根据枚举类型:

enum Bar {
  BAR_VALUE = 0;
  OTHER_VALUE = 1;
}

对于 oneof 字段定义:

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

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

  • bool has_foo() const(仅限 proto2):如果其中之一是 kFoo,则返回 true
  • Bar foo() const:如果任一情况为 kFoo,则返回字段的当前值。否则,返回默认值。
  • void set_foo(Bar value)
    • 如果设置了同一 one 中任何其他一个字段,则调用 clear_example_name()
    • 设置此字段的值,并将单例设为 kFoo
    • has_foo()(仅适用于 proto2)将返回 truefoo() 将返回 valueexample_name_case() 将返回 kFoo
    • 在调试模式下(即未定义 NDEBUG),如果 value 与为 Bar 定义的任何值均不匹配,此方法将取消该进程。
  • void clear_foo()
    • 如果此单一请求的情况不是 kFoo,则不会发生任何变化。
    • 如果 oneof 大小写为 kFoo,系统会清除字段的值和 oneof 大小写。has_foo()(仅适用于 proto2)将返回 falsefoo() 将返回默认值,example_name_case() 将返回 EXAMPLE_NAME_NOT_SET

Oneof 嵌入式消息字段

根据消息类型:

message Bar {}

对于 oneof 字段定义:

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

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

  • bool has_foo() const:如果其中之一是 kFoo,则返回 true。
  • const Bar& foo() const:如果任一情况为 kFoo,则返回字段的当前值。否则,返回 Bar::default_instance()
  • Bar* mutable_foo()
    • 如果设置了同一 one 中任何其他一个字段,则调用 clear_example_name()
    • 将 case 设为 kFoo,并返回指向用于存储该字段值的可变 Bar 对象的指针。如果上述任何一种情况在调用之前不是 kFoo,则返回的 Bar 将不会设置任何字段(也就是说,它与新分配的 Bar 完全相同)。
    • 调用此方法后,has_foo() 将返回 truefoo() 将返回对同一 Bar 实例的引用,example_name_case() 将返回 kFoo
  • void clear_foo()
    • 如果此单一请求的情况不是 kFoo,则不会发生任何变化。
    • 如果 oneof case 等于 kFoo,则释放该字段并清除此 case。has_foo() 将返回 falsefoo() 将返回默认值,example_name_case() 将返回 EXAMPLE_NAME_NOT_SET
  • void set_allocated_foo(Bar* bar)
    • 调用 clear_example_name()
    • 如果 Bar 指针不是 NULL:将 Bar 对象设置为相应字段,并将上述任一情况设为 kFoo。消息拥有已分配的 Bar 对象的所有权,has_foo() 将返回 true,example_name_case() 将返回 kFoo
    • 如果指针为 NULLhas_foo() 将返回 false,而 example_name_case() 将返回 EXAMPLE_NAME_NOT_SET。(此行为类似于调用 clear_example_name()
  • Bar* release_foo()
    • 如果其中一个不是 kFoo,则返回 NULL
    • 如果 oneof case 为 kFoo,则清除该 case,释放字段的所有权并返回 Bar 对象的指针。调用此方法后,调用方会获得所分配 Bar 对象的所有权,has_foo() 会返回 falsefoo() 会返回默认值,example_name_case() 会返回 EXAMPLE_NAME_NOT_SET

映射字段

对于此映射字段定义:

map<int32, int32> weight = 1;

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

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

google::protobuf::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
  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);
}

添加数据的最简单方法是使用法线贴图语法,例如:

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 与标准地图搭配使用

google::protobuf::Map 支持与 std::mapstd::unordered_map 相同的迭代器 API。如果您不想直接使用 google::protobuf::Map,则可以通过执行以下操作将 google::protobuf::Map 转换为标准地图:

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

请注意,这会创建整个地图的深层副本。

您还可以按标准地图构造 google::protobuf::Map,如下所示:

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

解析未知值

在传输线上,.proto 地图相当于每个键值对的映射条目消息,而映射本身就是映射条目的重复字段。与普通消息类型一样,已解析的地图条目消息可能包含未知字段:例如,定义为 map<int32, string> 的映射中类型为 int64 的字段。

如果地图条目消息的传输格式中有未知字段,将被舍弃。

如果映射条目消息的传输格式存在未知枚举值,则其在 proto2 和 proto3 中的处理方式不同。在 proto2 中,整个地图条目消息被放入包含消息的未知字段集中。在 proto3 中,它会被当作一个已知的枚举值来放在映射字段中。

不限

假设有一个 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,
                const string& 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 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),并将对象设为 EXAMPLE_NAME_NOT_SET,则释放对象。

枚举

假设有如下枚举定义:

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

协议缓冲区编译器将生成具有相同一组值的名为 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(const string& 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 消息时,系统也会保留无效枚举值,且枚举字段访问器会返回这些无效值。

在 Switch 语句中使用 proto3 枚举时请务必小心。Proto3 枚举是开放枚举类型,其可能的值不在指定符号范围内。解析 proto3 消息时会保留无法识别的枚举值,并由枚举字段访问器返回。没有默认用例的 proto3 枚举上的 Switch 语句将无法捕获所有用例,即使列出了所有已知字段也是如此。这可能会导致数据损坏和运行时崩溃等意外行为。始终添加默认情况或在开关之外明确调用 Foo_IsValid(int) 以处理未知枚举值。

您可以在消息类型中定义枚举。在本例中,协议缓冲区编译器会生成代码,使其显示枚举类型本身已声明嵌套在消息的类中。Foo_descriptor()Foo_IsValid() 函数被声明为静态方法。实际上,枚举类型本身及其值在全局范围内使用含重名的名称进行声明,并使用类型定义符和一系列常量定义导入到类作用域中。这样做只是为了解决声明排序的问题。不要依赖于损坏的顶级名称;假设枚举真正嵌套在消息类中。

扩展(仅限 proto2)

假设一条具有扩展范围的消息:

message Foo {
  extensions 100 to 199;
}

协议缓冲区编译器将为 Foo 生成一些额外的方法:HasExtension()ExtensionSize()ClearExtension()GetExtension()SetExtension()MutableExtension()AddExtension()SetAllocatedExtension()ReleaseExtension()。上述每种方法都接受一个扩展标识符(如下所述)作为其第一个参数,用于标识扩展字段。其余参数和返回值与将针对与扩展标识符相同类型的常规(非扩展)字段生成的相应访问器方法完全相同。(GetExtension() 对应于没有特殊前缀的访问器)。

根据扩展定义:

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

对于单数扩展字段 bar,协议缓冲区编译器会生成一个名为 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));

同样,对于重复的扩展字段 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_ext 已声明为嵌套在 Baz 内。它可以如下使用:

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

场地分配

Arena 分配是一项仅限 C++ 的功能,可帮助您优化内存用量,并提高在使用协议缓冲区时的性能。在 .proto 中启用表演区分配,会为用于 C++ 生成的代码添加与使用表演平台相关的其他代码。请参阅表演场馆分配指南,详细了解表演场馆分配 API。

服务

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

option cc_generic_services = true;

然后,协议缓冲区编译器将根据本部分所述的文件中的服务定义来生成代码。但是,生成的代码可能并不可取,因为它不依赖于任何特定的 RPC 系统,因此需要的间接层比为一个系统定制的代码多。如果您不希望生成此代码,请将以下行添加到文件中:

option cc_generic_services = false;

如果上述两行都没有给出,该选项会默认为 false,因为通用服务已弃用。(请注意,对于 2.4.0 之前的版本,该选项默认为 true)。

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

本部分的其余部分将介绍启用抽象服务后生成的协议缓冲区编译器生成的内容。

接口

根据服务定义:

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

协议缓冲区编译器将生成 Foo 类来表示此服务。对于服务定义中定义的每个方法,Foo 都有一个虚拟方法。在本例中,Bar 方法的定义如下:

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

这类参数等同于 Service::CallMethod() 的参数,不过 method 参数是隐含参数,并且 requestresponse 指定了其确切类型。

这些生成的方法是虚拟的,但不是纯虚拟的。默认实现只需调用 controller->SetFailed() 并提供一条错误消息,指明该方法尚未实现,然后调用 done 回调。在实现您自己的服务时,您必须将此生成的服务子类化,并视情况实现其方法。

FooService 接口的子类。协议缓冲区编译器会自动生成 Service 方法的实现,如下所示:

还会生成以下静态方法:

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

Stub

协议缓冲区编译器还会为每个服务接口生成一个“存根”实现,供希望向已实现服务的服务器发送请求的客户端使用。对于 Foo 服务(如上所示),将定义桩实现 Foo_Stub。与嵌套消息类型一样,系统使用类型定义符,因此 Foo_Stub 也可称为 Foo::Stub

Foo_StubFoo 的子类,该子类也实现了以下方法:

  • Foo_Stub(RpcChannel* channel):构建用于在指定通道上发送请求的新桩。
  • Foo_Stub(RpcChannel* channel, ChannelOwnership ownership):构建一个向指定频道发送请求并可能拥有该频道的新桩。如果 ownershipService::STUB_OWNS_CHANNEL,则在删除桩对象的同时,也会删除频道。
  • RpcChannel* channel():返回此桩的渠道,即传递给构造函数。

此外,桩还会将服务的每个方法实现为渠道的封装容器。调用其中一个方法只会调用 channel->CallMethod()

协议缓冲区库不包含 RPC 实现。不过,它包含将生成的服务类挂接到您选择的任意 RPC 实现所需的所有工具。您只需提供 RpcChannelRpcController 的实现即可。如需了解详情,请参阅 service.h 的文档。

插件插入点

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

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

请勿生成依赖于标准代码生成器声明的私有类成员的代码,因为这些实现细节可能会在将来的协议缓冲区版本中发生变化。