C++ 生成代码指南

精确描述 protocol buffer 编译器为任何给定的协议定义生成的 C++ 代码。

突出显示 proto2 和 proto3 生成的代码之间的任何差异 - 请注意,这些差异存在于本文档中所述的生成的代码中,而不是基本消息类/接口中,这两个版本中的基本消息类/接口是相同的。在阅读本文档之前,您应该阅读proto2 语言指南和/或proto3 语言指南

编译器调用

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;它们必须已经存在。

如果.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(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:返回在解析此消息时遇到的未知字段集。如果在.proto文件中指定了option optimize_for = LITE_RUNTIME,则返回类型将更改为std::string&
  • UnknownFieldSet* mutable_unknown_fields():返回指向在解析此消息时遇到的可变未知字段集的指针。如果在.proto文件中指定了option optimize_for = LITE_RUNTIME,则返回类型将更改为std::string*

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

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

生成的文件名

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

例如,以下 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);

嵌套类型

可以在另一条消息内声明一条消息。例如

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() 的参数)。相应地,仅当在此期间未对消息进行修改访问时,才能保证返回的引用的地址在访问器的不同调用之间保持相同。

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

可选数字字段 (proto2 和 proto3)

对于以下任一字段定义

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

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

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

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

隐式存在数字字段 (proto3)

对于以下字段定义

int32 foo = 1;  // no field label specified, defaults to implicit presence.

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

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

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

可选字符串/字节字段 (proto2 和 proto3)

对于以下任何字段定义

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

隐式存在字符串/字节字段 (proto3)

对于以下任一字段定义

string foo = 1;  // no field label specified, defaults to implicit presence.
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 样式的空终止字符串设置字段的值。调用此方法后,foo() 将返回 value 的副本。
  • void set_foo(const char* value, int size):与上面类似,但字符串大小是显式给出的,而不是通过查找空终止符字节来确定的。
  • 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 添加了对 absl::Cord 的支持,用于单数 bytes 字段(包括 oneof 字段)。单数 stringrepeated stringrepeated bytes 字段不支持使用 Cord

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

optional bytes foo = 25 [ctype=CORD];
bytes bar = 26 [ctype=CORD];

对于 repeated bytes 字段,无法使用 cord。Protoc 会忽略这些字段上的 [ctype=CORD] 设置。

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

  • const ::absl::Cord& foo() const:返回字段的当前值。如果字段未设置,则返回空 Cord(proto3)或默认值(proto2)。
  • void set_foo(const ::absl::Cord& value):设置字段的值。调用此方法后,foo() 将返回 value
  • void set_foo(::absl::string_view value):设置字段的值。调用此方法后,foo() 将以 absl::Cord 的形式返回 value
  • void clear_foo():清除字段的值。调用此方法后,foo() 将返回空 Cord(proto3)或默认值(proto2)。
  • bool has_foo():如果字段已设置,则返回 true

可选枚举字段 (proto2 和 proto3)

给定枚举类型

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

对于以下任一字段定义

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

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

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

隐式存在枚举字段 (proto3)

给定枚举类型

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

对于此字段定义

Bar foo = 1;  // no field label specified, defaults to implicit presence.

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

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

可选嵌入消息字段 (proto2 和 proto3)

给定消息类型

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() 将返回 falsefoo() 将返回默认值。
  • 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:返回字段中当前元素的数量。要检查空集,请考虑使用底层 RepeatedField 中的 empty() 方法,而不是此方法。
  • 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:返回字段中当前元素的数量。要检查空集,请考虑使用底层 RepeatedField 中的 empty() 方法,而不是此方法。
  • const string& foo(int index) const:返回给定基于零的索引处的元素。使用超出 [0, foo_size()-1] 范围的索引调用此方法会导致未定义的行为。
  • 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_UNSPECIFIED = 0;
  BAR_VALUE = 1;
  BAR_OTHER_VALUE = 2;
}

对于此字段定义

repeated Bar foo = 1;

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

  • int foo_size() const:返回字段中当前元素的数量。要检查空集,请考虑使用底层 RepeatedField 中的 empty() 方法,而不是此方法。
  • 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:返回字段中当前元素的数量。要检查空集,请考虑使用底层 RepeatedField 中的 empty() 方法,而不是此方法。
  • 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 字段定义

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

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

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

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

Oneof 字符串字段

对于任何这些 oneof 字段定义

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

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

  • bool has_foo() const:如果 oneof case 为 kFoo,则返回 true
  • const string& foo() const:如果 oneof case 为 kFoo,则返回字段的当前值。否则,返回默认值。
  • void set_foo(const string& value):
    • 如果同一个 oneof 中的任何其他 oneof 字段已设置,则调用 clear_example_name()
    • 设置此字段的值并将 oneof case 设置为 kFoo
    • has_foo() 将返回 truefoo() 将返回 value 的副本,并且 example_name_case() 将返回 kFoo
  • void set_foo(const char* value):
    • 如果同一个 oneof 中的任何其他 oneof 字段已设置,则调用 clear_example_name()
    • 使用 C 样式的以 null 结尾的字符串设置字段的值并将 oneof case 设置为 kFoo
    • has_foo() 将返回 truefoo() 将返回 value 的副本,并且 example_name_case() 将返回 kFoo
  • void set_foo(const char* value, int size):与上面类似,但字符串大小是显式给出的,而不是通过查找空终止符字节来确定的。
  • string* mutable_foo():
    • 如果同一个 oneof 中的任何其他 oneof 字段已设置,则调用 clear_example_name()
    • 将 oneof case 设置为 kFoo 并返回指向存储字段值的可变字符串对象的指针。如果在调用之前 oneof case 不是 kFoo,则返回的字符串将为空(不是默认值)。
    • has_foo() 将返回 truefoo() 将返回写入给定字符串中的任何值,并且 example_name_case() 将返回 kFoo
  • void clear_foo():
    • 如果 oneof case 不是 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:将字符串对象设置为字段并将 oneof case 设置为 kFoo。消息将获取已分配字符串对象的拥有权,has_foo() 将返回 true,并且 example_name_case() 将返回 kFoo
    • 如果字符串指针为 NULL,则 has_foo() 将返回 false,并且 example_name_case() 将返回 EXAMPLE_NAME_NOT_SET
  • string* release_foo():
    • 如果 oneof case 不是 kFoo,则返回 NULL
    • 清除 oneof case,释放字段的拥有权并返回字符串对象的指针。调用此方法后,调用方将获取已分配字符串对象的拥有权,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 foo = 1;
    ...
}

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

  • bool has_foo() const:如果 oneof case 为 kFoo,则返回 true
  • Bar foo() const:如果 oneof case 为 kFoo,则返回字段的当前值。否则,返回默认值。
  • void set_foo(Bar value):
    • 如果同一个 oneof 中的任何其他 oneof 字段已设置,则调用 clear_example_name()
    • 设置此字段的值并将 oneof case 设置为 kFoo
    • has_foo() 将返回 truefoo() 将返回 value,并且 example_name_case() 将返回 kFoo
    • 在调试模式下(即未定义 NDEBUG),如果 value 与为 Bar 定义的任何值都不匹配,则此方法将中止进程。
  • void clear_foo():
    • 如果 oneof case 不是 kFoo,则不会发生任何更改。
    • 如果 oneof case 为 kFoo,则清除字段的值和 oneof case。has_foo() 将返回 falsefoo() 将返回默认值,并且 example_name_case() 将返回 EXAMPLE_NAME_NOT_SET

Oneof 嵌入消息字段

给定消息类型

message Bar {}

对于 oneof 字段定义

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

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

  • bool has_foo() const:如果 oneof case 为 kFoo,则返回 true。
  • const Bar& foo() const:如果 oneof case 为 kFoo,则返回字段的当前值。否则,返回一个其字段均未设置的 Bar(可能是 Bar::default_instance())。
  • Bar* mutable_foo():
    • 如果同一个 oneof 中的任何其他 oneof 字段已设置,则调用 clear_example_name()
    • 将 oneof case 设置为 kFoo 并返回指向存储字段值的可变 Bar 对象的指针。如果在调用之前 oneof case 不是 kFoo,则返回的 Bar 将其字段均未设置(即,它将与新分配的 Bar 相同)。
    • 调用此方法后,has_foo() 将返回 truefoo() 将返回对 Bar 的同一实例的引用,并且 example_name_case() 将返回 kFoo

  • void clear_foo():
    • 如果 oneof case 不是 kFoo,则不会发生任何更改。
    • 如果oneof情况等于kFoo,则释放该字段并清除oneof情况。has_foo()将返回falsefoo()将返回默认值,example_name_case()将返回EXAMPLE_NAME_NOT_SET
  • void set_allocated_foo(Bar* bar):
    • 调用 clear_example_name()
    • 如果Bar指针不为NULL:将Bar对象设置为该字段,并将oneof情况设置为kFoo。消息将获取已分配的Bar对象的拥有权,has_foo()将返回true,example_name_case()将返回kFoo
    • 如果指针为NULL,则has_foo()将返回falseexample_name_case()将返回EXAMPLE_NAME_NOT_SET。(行为类似于调用clear_example_name()
  • Bar* release_foo():
    • 如果 oneof case 不是 kFoo,则返回 NULL
    • 如果oneof情况是kFoo,则清除oneof情况,释放该字段的拥有权并返回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
  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);
}

添加数据最简单的方法是使用正常的映射语法,例如

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

给定一个像这样的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

给定一个像这样的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字段集使用指针(消息或字符串),则释放对象,并将oneof情况设置为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中更改。只要整数适合int32,就可以安全地将任何整数转换为proto3枚举值。在解析proto3消息时,无效的枚举值也将被保留,并由枚举字段访问器返回。

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

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

扩展 (仅限 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;
  optional Bar message_bar = 125;
}

对于单数扩展字段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));

对于消息扩展字段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_ext声明为嵌套在Baz中。它可以按如下方式使用

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

Arena 分配

Arena分配是仅限C++的功能,可帮助您优化内存使用并提高使用协议缓冲区时的性能。在您的.proto中启用Arena分配会为您的C++生成的代码添加用于处理Arena的额外代码。您可以在Arena分配指南中找到有关Arena分配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方法的实现,如下所示

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

还会生成以下静态方法

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

存根

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

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

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

存根还将服务的每个方法实现为通道的包装器。调用其中一个方法只会调用channel->CallMethod()

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

插件插入点

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

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

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