Основы Protocol Buffer: C++

Базовое введение в работу с protocol buffers для программистов на C++.

Это руководство предоставляет базовое введение для программистов на C++ в работу с protocol buffers. На примере создания простого приложения оно показывает, как:

  • Определять форматы сообщений в файле .proto.
  • Использовать компилятор protocol buffer.
  • Использовать C++ API protocol buffer для записи и чтения сообщений.

Это не исчерпывающее руководство по использованию protocol buffers в C++. Для получения более подробной справочной информации см. Руководство по языку Protocol Buffer, Справочник по C++ API, Руководство по сгенерированному коду на C++ и Справочник по кодированию.

Проблемная область

Пример, который мы будем использовать, — это очень простое приложение "адресная книга", которое может читать и записывать контактные данные людей в файл и из файла. Каждый человек в адресной книге имеет имя, ID, адрес электронной почты и контактный номер телефона.

Как сериализовать и извлекать такие структурированные данные? Есть несколько способов решить эту проблему:

  • Необработанные структуры данных в памяти могут быть отправлены/сохранены в бинарной форме. Со временем это хрупкий подход, поскольку принимающий/читающий код должен быть скомпилирован с точно такой же раскладкой памяти, порядком байтов и т.д. Также, по мере накопления данных в сыром формате и распространения копий программ, настроенных на этот формат, становится очень трудно расширить формат.
  • Вы можете придумать специальный способ кодирования элементов данных в единую строку — например, кодируя 4 целых числа как "12:3:-23:67". Это простой и гибкий подход, хотя он требует написания одноразового кода кодирования и разбора, и разбор накладывает небольшие затраты времени выполнения. Это лучше всего работает для кодирования очень простых данных.
  • Сериализовать данные в XML. Этот подход может быть очень привлекательным, поскольку XML (вроде как) читаем человеком и есть библиотеки привязок для множества языков. Это может быть хорошим выбором, если вы хотите делиться данными с другими приложениями/проектами. Однако XML печально известен своей прожорливостью к месту, и кодирование/декодирование может наложить огромные штрафы на производительность приложений. Кроме того, навигация по DOM-дереву XML значительно сложнее, чем навигация по простым полям в классе в обычных условиях.

Вместо этих вариантов вы можете использовать protocol buffers. Protocol buffers — это гибкое, эффективное, автоматизированное решение, созданное именно для этой проблемы. С protocol buffers вы пишете .proto описание структуры данных, которую хотите сохранить. На основе этого компилятор protocol buffer создает класс, который реализует автоматическое кодирование и разбор данных protocol buffer в эффективном бинарном формате. Сгенерированный класс предоставляет геттеры и сеттеры для полей, составляющих protocol buffer, и заботится о деталях чтения и записи protocol buffer как единого целого. Важно, что формат protocol buffer поддерживает идею расширения формата со временем таким образом, что код все еще может читать данные, закодированные в старом формате.

Где найти пример кода

Пример кода включен в пакет с исходным кодом, в директории "examples".

Определение вашего формата протокола

Чтобы создать ваше приложение "адресная книга", вам нужно начать с файла .proto. Определения в файле .proto просты: вы добавляете сообщение для каждой структуры данных, которую хотите сериализовать, затем указываете имя и тип для каждого поля в сообщении. Вот файл .proto, который определяет ваши сообщения, addressbook.proto.

edition = "2023";

package tutorial;

message Person {
  string name = 1;
  int32 id = 2;
  string email = 3;

  enum PhoneType {
    PHONE_TYPE_UNSPECIFIED = 0;
    PHONE_TYPE_MOBILE = 1;
    PHONE_TYPE_HOME = 2;
    PHONE_TYPE_WORK = 3;
  }

  message PhoneNumber {
    string number = 1;
    PhoneType type = 2;
  }

  repeated PhoneNumber phones = 4;
}

message AddressBook {
  repeated Person people = 1;
}

Как вы можете видеть, синтаксис похож на C++ или Java. Давайте пройдемся по каждой части файла и посмотрим, что она делает.

Файл .proto начинается с объявления edition. Редакции заменяют старые объявления syntax = "proto2" и syntax = "proto3" и предоставляют более гибкий способ развития языка с течением времени.

Далее следует объявление пакета, которое помогает предотвратить конфликты имен между разными проектами. В C++ ваши сгенерированные классы будут помещены в пространство имен, соответствующее имени пакета.

После объявления пакета идут ваши определения сообщений. Сообщение — это просто агрегат, содержащий набор типизированных полей. Многие стандартные простые типы данных доступны в качестве типов полей, включая bool, int32, float, double и string. Вы также можете добавить дальнейшую структуру в ваши сообщения, используя другие типы сообщений в качестве типов полей — в приведенном выше примере сообщение Person содержит сообщения PhoneNumber, а сообщение AddressBook содержит сообщения Person. Вы даже можете определять типы сообщений, вложенные внутрь других сообщений — как вы можете видеть, тип PhoneNumber определен внутри Person. Вы также можете определить типы перечислений, если хотите, чтобы одно из ваших полей имело одно из предопределенного списка значений — здесь вы хотите указать, что номер телефона может быть одного из нескольких типов.

Маркеры " = 1", " = 2" на каждом элементе идентифицируют уникальный номер поля, который поле использует в бинарном кодировании. Номера полей 1-15 требуют на один байт меньше для кодирования, чем более высокие номера, поэтому в качестве оптимизации вы можете решить использовать эти номера для часто используемых или повторяющихся элементов, оставляя номера полей 16 и выше для реже используемых элементов.

Поля могут быть одним из следующих:

  • singular (сингулярные): По умолчанию поля являются необязательными (optional), что означает, что поле может быть установлено, а может и не быть. Если сингулярное поле не установлено, используется значение по умолчанию, зависящее от типа: ноль для числовых типов, пустая строка для строк, false для bool, и первое определенное значение перечисления для перечислений (которое должно быть 0). Обратите внимание, что вы не можете явно установить поле в singular. Это описание неповторяющегося поля.

  • repeated (повторяющиеся): Поле может повторяться любое количество раз (включая ноль). Порядок повторяющихся значений будет сохранен. Думайте о повторяющихся полях как о динамически sized arrays (массивах переменного размера).

В старых версиях protobuf существовало ключевое слово required, но оно оказалось хрупким и не поддерживается в современных protobuf (хотя в редакциях есть функция, которую вы можете использовать, чтобы включить его, для обратной совместимости).

Вы найдете полное руководство по написанию файлов .proto — включая все возможные типы полей — в Руководстве по языку Protocol Buffer. Однако не ищите возможности, похожие на наследование классов — protocol buffers этого не делают.

Компиляция ваших Protocol Buffers

Теперь, когда у вас есть .proto, следующее, что нужно сделать, — это сгенерировать классы, которые вам понадобятся для чтения и записи сообщений AddressBook (и, следовательно, Person и PhoneNumber). Для этого вам нужно запустить компилятор protocol buffer protoc на вашем .proto:

  1. Если вы не установили компилятор, следуйте инструкциям в Установка компилятора Protocol Buffer.

  2. Теперь запустите компилятор, указав исходную директорию (где находится исходный код вашего приложения — используется текущая директория, если вы не предоставили значение), целевую директорию (куда вы хотите, чтобы сгенерированный код попал; часто та же, что и $SRC_DIR) и путь к вашему .proto. В этом случае:

    protoc -I=$SRC_DIR --cpp_out=$DST_DIR $SRC_DIR/addressbook.proto
    

    Поскольку вы хотите классы C++, вы используете опцию --cpp_out — похожие опции предоставляются для других поддерживаемых языков.

Это генерирует следующие файлы в указанной вами целевой директории:

  • addressbook.pb.h, заголовочный файл, который объявляет ваши сгенерированные классы.
  • addressbook.pb.cc, который содержит реализацию ваших классов.

API Protocol Buffer

Давайте посмотрим на некоторый сгенерированный код и увидим, какие классы и функции компилятор создал для вас. Если вы посмотрите в addressbook.pb.h, вы можете увидеть, что у вас есть класс для каждого сообщения, которое вы указали в addressbook.proto. Присмотревшись к классу Person, вы можете увидеть, что компилятор сгенерировал методы доступа для каждого поля. Например, для полей name, id, email и phones у вас есть эти методы:

  // name
  bool has_name() const; // Только для явного присутствия
  void clear_name();
  const ::std::string& name() const;
  void set_name(const ::std::string& value);
  ::std::string* mutable_name();

  // id
  bool has_id() const;
  void clear_id();
  int32_t id() const;
  void set_id(int32_t value);

  // email
  bool has_email() const;
  void clear_email();
  const ::std::string& email() const;
  void set_email(const ::std::string& value);
  ::std::string* mutable_email();

  // phones
  int phones_size() const;
  void clear_phones();
  const ::google::protobuf::RepeatedPtrField< ::tutorial::Person_PhoneNumber >& phones() const;
  ::google::protobuf::RepeatedPtrField< ::tutorial::Person_PhoneNumber >* mutable_phones();
  const ::tutorial::Person_PhoneNumber& phones(int index) const;
  ::tutorial::Person_PhoneNumber* mutable_phones(int index);
  ::tutorial::Person_PhoneNumber* add_phones();

Как вы можете видеть, геттеры имеют точно такое же имя, как поле в нижнем регистре, и сеттерные методы начинаются с set_. Также есть методы has_ для сингулярных полей, которые имеют явное отслеживание присутствия; они возвращают true, если это поле было установлено. Наконец, каждое поле имеет метод clear_, который сбрасывает поле обратно в его состояние по умолчанию.

В то время как числовое поле id имеет только базовый набор методов доступа, описанный выше, поля name и email имеют пару дополнительных методов, потому что они являются строками — mutable_ геттер, который позволяет вам получить прямой указатель на строку, и дополнительный сеттер. Обратите внимание, что вы можете вызвать mutable_email(), даже если email еще не установлен; он будет автоматически инициализирован пустой строкой. Если бы у вас было повторяющееся поле сообщения в этом примере, оно также имело бы метод mutable_, но не имело бы метода set_.

Повторяющиеся поля также имеют некоторые специальные методы — если вы посмотрите на методы для повторяющегося поля phones, вы увидите, что вы можете:

  • проверить _size (размер) повторяющегося поля (другими словами, сколько номеров телефонов связано с этим Person).
  • получить указанный номер телефона, используя его индекс.
  • обновить существующий номер телефона по указанному индексу.
  • добавить другой номер телефона в сообщение, который вы затем можете редактировать (повторяющиеся скалярные типы имеют add_, который просто позволяет вам передать новое значение).

Для получения дополнительной информации о том, какие именно члены компилятор протоколов генерирует для любого конкретного определения поля, см. Справочник по сгенерированному коду на C++.

Перечисления и вложенные классы

Сгенерированный код включает перечисление PhoneType, которое соответствует вашему .proto перечислению. Вы можете ссылаться на этот тип как Person::PhoneType и его значения как Person::PHONE_TYPE_MOBILE, Person::PHONE_TYPE_HOME и Person::PHONE_TYPE_WORK (детали реализации немного более сложные, но вам не нужно понимать их, чтобы использовать перечисление).

Компилятор также сгенерировал для вас вложенный класс с именем Person::PhoneNumber. Если вы посмотрите на код, вы увидите, что "настоящий" класс на самом деле называется Person_PhoneNumber, но typedef, определенный внутри Person, позволяет вам обращаться с ним так, как если бы это был вложенный класс. Единственный случай, когда это имеет значение, — это если вы хотите сделать forward-declaration (предварительное объявление) класса в другом файле — вы не можете forward-declare вложенные типы в C++, но вы можете forward-declare Person_PhoneNumber.

Стандартные методы сообщений

Каждый класс сообщений также содержит ряд других методов, которые позволяют вам проверять или манипулировать всем сообщением, включая:

  • bool IsInitialized() const;: проверяет, установлены ли все обязательные поля.
  • string DebugString() const;: возвращает удобочитаемое представление сообщения, особенно полезное для отладки.
  • void CopyFrom(const Person& from);: перезаписывает сообщение значениями данного сообщения.
  • void Clear();: очищает все элементы обратно в пустое состояние.

Эти и методы ввода-вывода, описанные в следующем разделе, реализуют интерфейс Message, общий для всех классов C++ protocol buffer. Для получения дополнительной информации см. полную документацию API для Message.

Разбор и сериализация

Наконец, каждый класс protocol buffer имеет методы для записи и чтения сообщений вашего выбранного типа, используя бинарный формат protocol buffer. К ним относятся:

  • bool SerializeToString(string* output) const;: сериализует сообщение и сохраняет байты в данной строке. Обратите внимание, что байты являются бинарными, а не текстовыми; мы используем класс string только как удобный контейнер.
  • bool ParseFromString(const string& data);: разбирает сообщение из данной строки.
  • bool SerializeToOstream(ostream* output) const;: записывает сообщение в данный C++ ostream.
  • bool ParseFromIstream(istream* input);: разбирает сообщение из данного C++ istream.

Это лишь пара из предоставленных вариантов для разбора и сериализации. См. справочник по API Message для получения полного списка.

{{% alert title="Важно" color="warning" %}} Protocol Buffers и объектно-ориентированный дизайн Классы Protocol buffer — это, по сути, держатели данных (как структуры в C), которые не предоставляют дополнительной функциональности; они не являются хорошими полноправными гражданами в объектной модели. Если вы хотите добавить более богатое поведение в сгенерированный класс, лучший способ сделать это — обернуть сгенерированный класс protocol buffer в специфичный для приложения класс. Обертывание protocol buffers также является хорошей идеей, если вы не контролируете дизайн файла .proto (если, скажем, вы используете его из другого проекта). В этом случае вы можете использовать класс-обертку для создания интерфейса, лучше подходящего для уникальной среды вашего приложения: скрывая некоторые данные и методы, предоставляя удобные функции и т.д. Вы не можете добавить поведение к сгенерированным классам, наследуясь от них, так как они являются final (запечатанными). Это предотвращает нарушение внутренних механизмов и, в любом случае, не является хорошей объектно-ориентированной практикой.

{{% /alert %}}

Написание сообщения

Теперь давайте попробуем использовать ваши классы protocol buffer. Первое, что вы хотите, чтобы ваше приложение "адресная книга" могло делать, — это записывать личные данные в ваш файл адресной книги. Для этого вам нужно создать и заполнить экземпляры ваших классов protocol buffer, а затем записать их в выходной поток.

Вот программа, которая читает AddressBook из файла, добавляет одного нового Person в него на основе пользовательского ввода и записывает новый AddressBook обратно в файл снова. Части, которые напрямую вызывают или ссылаются на код, сгенерированный компилятором протокола, выделены.

#include <iostream>
#include <fstream>
#include <string>
#include "addressbook.pb.h"
using namespace std;

// Эта функция заполняет сообщение Person на основе пользовательского ввода.
void PromptForAddress(tutorial::Person& person) {
  cout << "Введите ID человека: ";
  int id;
  cin >> id;
  person.set_id(id);
  cin.ignore(256, '\n');

  cout << "Введите имя: ";
  getline(cin, *person.mutable_name());

  cout << "Введите адрес электронной почты (пусто для отсутствия): ";
  string email;
  getline(cin, email);
  if (!email.empty()) {
    person.set_email(email);
  }

  while (true) {
    cout << "Введите номер телефона (или оставьте пустым для завершения): ";
    string number;
    getline(cin, number);
    if (number.empty()) {
      break;
    }

    tutorial::Person::PhoneNumber* phone_number = person.add_phones();
    phone_number->set_number(number);

    cout << "Это мобильный, домашний или рабочий телефон? ";
    string type;
    getline(cin, type);
    if (type == "mobile") {
      phone_number->set_type(tutorial::Person::PHONE_TYPE_MOBILE);
    } else if (type == "home") {
      phone_number->set_type(tutorial::Person::PHONE_TYPE_HOME);
    } else if (type == "work") {
      phone_number->set_type(tutorial::Person::PHONE_TYPE_WORK);
    } else {
      cout << "Неизвестный тип телефона. Используется значение по умолчанию." << endl;
    }
  }
}

// Главная функция: Читает всю адресную книгу из файла,
//   добавляет одного человека на основе пользовательского ввода, затем записывает её обратно в тот же
//   файл.
int main(int argc, char* argv[]) {
  // Проверяем, что версия библиотеки, с которой мы слинковались,
  // совместима с версией заголовков, с которой мы компилировали.
  GOOGLE_PROTOBUF_VERIFY_VERSION;

  if (argc != 2) {
    cerr << "Использование:  " << argv[0] << " ФАЙЛ_АДРЕСНОЙ_КНИГИ" << endl;
    return -1;
  }

  tutorial::AddressBook address_book;

  {
    // Читаем существующую адресную книгу.
    fstream input(argv[1], ios::in | ios::binary);
    if (!input) {
      cout << argv[1] << ": Файл не найден. Создается новый файл." << endl;
    } else if (!address_book.ParseFromIstream(&input)) {
      cerr << "Не удалось разобрать адресную книгу." << endl;
      return -1;
    }
  }

  // Добавляем адрес.
  PromptForAddress(*address_book.add_people());

  {
    // Записываем новую адресную книгу обратно на диск.
    fstream output(argv[1], ios::out | ios::trunc | ios::binary);
    if (!address_book.SerializeToOstream(&output)) {
      cerr << "Не удалось записать адресную книгу." << endl;
      return -1;
    }
  }

  // Опционально: Удаляем все глобальные объекты, выделенные libprotobuf.
  google::protobuf::ShutdownProtobufLibrary();

  return 0;
}

Обратите внимание на макрос GOOGLE_PROTOBUF_VERIFY_VERSION. Это хорошая практика — хотя и не строго необходимая — выполнять этот макрос перед использованием библиотеки C++ Protocol Buffer. Он проверяет, что вы не случайно слинковались с версией библиотеки, которая несовместима с версией заголовков, с которой вы компилировали. Если обнаружено несоответствие версий, программа завершится. Обратите внимание, что каждый файл .pb.cc автоматически вызывает этот макрос при запуске.

Также обратите внимание на вызов ShutdownProtobufLibrary() в конце программы. Все, что он делает, — это удаляет любые глобальные объекты, которые были выделены библиотекой Protocol Buffer. Это необязательно для большинства программ, поскольку процесс просто завершится, и ОС позаботится о возврате всей его памяти. Однако, если вы используете проверку утечек памяти, которая требует, чтобы каждый последний объект был освобожден, или если вы пишете библиотеку, которая может быть загружена и выгружена несколько раз одним процессом, то вы, возможно, захотите заставить Protocol Buffers очистить всё.

Чтение сообщения

Конечно, адресная книга была бы не очень полезна, если бы вы не могли получить из нее никакой информации! Этот пример читает файл, созданный приведенным выше примером, и печатает всю информацию в нем.

#include <iostream>
#include <fstream>
#include <string>
#include "addressbook.pb.h"
using namespace std;

// Перебирает всех людей в AddressBook и печатает информацию о них.
void ListPeople(const tutorial::AddressBook& address_book) {
  for (const tutorial::Person& person : address_book.people()) {
    cout << "ID человека: " << person.id() << endl;
    cout << "  Имя: " << person.name() << endl;
    if (person.has_email()) { // Исправлено: проверяем has_email(), а не !has_email()
      cout << "  Адрес электронной почты: " << person.email() << endl;
    }

    for (const tutorial::Person::PhoneNumber& phone_number : person.phones()) {
      switch (phone_number.type()) {
        case tutorial::Person::PHONE_TYPE_MOBILE:
          cout << "  Мобильный телефон #: ";
          break;
        case tutorial::Person::PHONE_TYPE_HOME:
          cout << "  Домашний телефон #: ";
          break;
        case tutorial::Person::PHONE_TYPE_WORK:
          cout << "  Рабочий телефон #: ";
          break;
        case tutorial::Person::PHONE_TYPE_UNSPECIFIED:
        default:
          cout << "  Телефон #: ";
          break;
      }
      cout << phone_number.number() << endl;
    }
  }
}

// Главная функция: Читает всю адресную книгу из файла и печатает всю
//   информацию внутри.
int main(int argc, char* argv[]) {
  // Проверяем, что версия библиотеки, с которой мы слинковались,
  // совместима с версией заголовков, с которой мы компилировали.
  GOOGLE_PROTOBUF_VERIFY_VERSION;

  if (argc != 2) {
    cerr << "Использование:  " << argv[0] << " ФАЙЛ_АДРЕСНОЙ_КНИГИ" << endl;
    return -1;
  }

  tutorial::AddressBook address_book;

  {
    // Читаем существующую адресную книгу.
    fstream input(argv[1], ios::in | ios::binary);
    if (!address_book.ParseFromIstream(&input)) {
      cerr << "Не удалось разобрать адресную книгу." << endl;
      return -1;
    }
  }

  ListPeople(address_book);

  // Опционально: Удаляем все глобальные объекты, выделенные libprotobuf.
  google::protobuf::ShutdownProtobufLibrary();

  return 0;
}

Расширение Protocol Buffer

Рано или поздно после того, как вы выпустите код, использующий ваш protocol buffer, вы несомненно захотите "улучшить" определение protocol buffer. Если вы хотите, чтобы ваши новые буферы были обратно совместимы, а ваши старые буферы были вперед совместимы — а вы почти certainly хотите этого — тогда есть некоторые правила, которым вы должны следовать. В новой версии protocol buffer:

  • вы не должны изменять номера полей любых существующих полей.
  • вы можете удалять сингулярные или повторяющиеся поля.
  • вы можете добавлять новые сингулярные или повторяющиеся поля, но вы должны использовать новые номера полей (то есть номера полей, которые никогда не использовались в этом protocol buffer, даже удаленными полями).

(Есть некоторые исключения из этих правил, но они редко используются.)

Если вы следуете этим правилам, старый код будет happily (с радостью) читать новые сообщения и просто игнорировать любые новые поля. Для старого code (кода) поля, которые были удалены, просто будут иметь свое значение по умолчанию, а удаленные повторяющиеся поля будут пусты. Новый код также будет прозрачно читать старые сообщения. Однако имейте в виду, что новые поля не будут присутствовать в старых сообщениях, поэтому вам нужно будет проверить их присутствие, проверив, имеют ли они значение по умолчанию (например, пустую строку) перед использованием.

Советы по оптимизации

Библиотека C++ Protocol Buffers чрезвычайно сильно оптимизирована. Однако правильное использование может улучшить производительность еще больше. Вот несколько советов, как выжать каждую последнюю каплю скорости из библиотеки:

  • Используйте Арены (Arenas) для выделения памяти. Когда вы создаете много сообщений protocol buffer в короткоживущей операции (например, разбор одного запроса), распределитель памяти системы может стать узким местом. Арены предназначены для смягчения этого. Используя арену, вы можете выполнять множество выделений с низкими накладными расходами и одно освобождение для всех них сразу. Это может значительно улучшить производительность в приложениях, насыщенных сообщениями.

    Чтобы использовать арены, вы выделяете сообщения на объекте google::protobuf::Arena:

    google::protobuf::Arena arena;
    tutorial::Person* person = google::protobuf::Arena::Create<tutorial::Person>(&arena);
    // ... заполняем person ...
    

    Когда объект арены уничтожается, все сообщения, выделенные на нем, освобождаются. Для получения более подробной информации см. Руководство по Аренам.

  • Повторно используйте объекты сообщений, не находящиеся в арене, когда это возможно. Сообщения пытаются сохранять любую память, которую они выделяют, для повторного использования, даже когда они очищаются. Таким образом, если вы обрабатываете много сообщений одного и того же типа и схожей структуры последовательно, рекомендуется повторно использовать один и тот же объект сообщения каждый раз, чтобы снять нагрузку с распределителя памяти. Однако объекты могут стать раздутыми со временем, особенно если ваши сообщения различаются по "форме" или если вы иногда конструируете сообщение, которое намного больше обычного. Вы должны отслеживать размеры ваших объектов сообщений, вызывая метод SpaceUsed, и удалять их, как только они станут слишком большими.

    Повторное использование сообщений в аренах может привести к неограниченному росту памяти. Повторное использование сообщений в куче безопаснее. Однако даже с сообщениями в куче вы все равно можете столкнуться с проблемами high water mark (максимального уровня) полей. Например, если вы видите сообщения:

    a: [1, 2, 3, 4]
    b: [1]
    

    и

    a: [1]
    b: [1, 2, 3, 4]
    

    и повторно используете сообщения, то оба поля будут иметь достаточно памяти для самого большого размера, который они видели. Так что если каждый вход имел только 5 элементов, повторно использованное сообщение будет иметь память для 8.

  • Распределитель памяти вашей системы может быть плохо оптимизирован для выделения множества маленьких объектов из нескольких потоков. Попробуйте использовать Google's TCMalloc вместо него.

Продвинутое использование

Protocol buffers имеют применения, выходящие за рамки простых методов доступа и сериализации. Непременно исследуйте Справочник по C++ API, чтобы увидеть, что еще вы можете сделать с ними.

Одной ключевой особенностью, предоставляемой классами сообщений протокола, является reflection (рефлексия). Вы можете перебирать поля сообщения и манипулировать их значениями без написания вашего кода против какого-либо конкретного типа сообщения. Очень полезный способ использования рефлексии — это преобразование сообщений протокола в другие кодировки и обратно, такие как XML или JSON. Более продвинутое использование рефлексии может заключаться в нахождении разниц между двумя сообщениями одного типа или в разработке своего рода "регулярных выражений для сообщений протокола", в которых вы можете писать выражения, соответствующие определенному содержимому сообщения. Если вы используете свое воображение, возможно применять Protocol Buffers к гораздо более широкому кругу проблем, чем вы могли изначально ожидать!

Рефлексия предоставляется интерфейсом Message::Reflection.