C++チュートリアル

このチュートリアルは、C++プログラマが Protocol Buffers を使う際の入門を提供します。 簡単なサンプルアプリケーションを見ていきながら、 について示します。

このドキュメントは、Protocol Buffers を C++ で使うための包括的な手引きではありません。 より詳細な情報については、Protocol Buffers 言語ガイドC++ APIリファレンスC++生成コードガイドエンコーディングリファレンスを参照してください。

なぜ Protocol Buffers を使うのか?

これから使っていくのは、知人のコンタクト方法をファイルへ読み書きができる簡単な「アドレスブック」アプリケーションです。 アドレスブックではそれぞれの人の名前、ID、メールアドレス、電話番号を記述できます。

こういった構造化データをどうやって直列化し、元に戻しますか? この問題を解くためにはいくつかの方法があります。

Protocol Buffers はこの問題を正確に解決するための柔軟で、効率的で、自動化されたソリューションです。 Protocol Buffers では、あなたは保存したいデータ構造を .proto に記述します。 Protocol Buffers のコンパイラはその .proto ファイルから、データを効率的なバイナリ形式への自動的な符号化/復号を実装したクラスを生成します。 生成されたクラスはそのプロトコルのバッファを組み上げるフィールドのゲッターとセッターを提供し、そのプロトコルバッファの読み書きの詳細をユニットとして面倒みてくれます。重要なことは、Protocol Buffers のフォーマットは、古い形式のフォーマットでエンコードされたデータを依然として読めるような方法で、長期間に渡るフォーマットを拡張するアイデアをサポートしていることです。

サンプルコードはどこにあるか

サンプルコードは、ソースコードパッケージの "examples" ディレクトリの下にあります。ここからダウンロードしてください

プロトコルフォーマットを定義する

アドレスブックアプリケーションを作るに当たり、.proto ファイルから始める必要があります。 .proto ファイルの定義は簡単です: 直列化したいデータ構造毎に *メッセージ* を追加して、そのメッセージのフィールドの名前と型を指定します。これが、メッセージを定義した .proto ファイルである addressbook.proto です。

package tutorial;

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

  enum PhoneType {
    MOBILE = 0;
    HOME = 1;
    WORK = 2;
  }

  message PhoneNumber {
    required string number = 1;
    optional PhoneType type = 2 [default = HOME];
  }

  repeated PhoneNumber phone = 4;
}

message AddressBook {
  repeated Person person = 1;
}

見て分かるように、文法は C++ や Java に似ています。 このファイルのそれぞれのパートをたどり、何をやっているのかを見ていきましょう。

.proto ファイルは package宣言から始まっています。これは、別のプロジェクト間での名前の衝突を避ける手助けをしてくれるものです。 C++ では、生成されたクラスはパッケージ名と一致する名前空間に置かれます。

次にはメッセージの定義があります。 メッセージは型付けあっれたフィールドの集合を含みます。 フィールドの型には、bool, int32, float, double, string を含む多くの標準的で単純なデータタイプが利用可能です。 別のメッセージ型をフィールド型として使うことで、それ以上の構造体をメッセージに追加することができます。 上の例では、Personメッセージは PhoneNumberメッセージを含み、AddressBookメッセージは Personメッセージを含んでいます。 メッセージ型を他のメッセージの中にネストして定義することもできます。 同じく例では、PhoneNumer型は Personの中で定義されているのが分かります。 フィールドの値を事前に定義された値のリストのどれかになるようにしたい場合には、enum型を使うことができます。 ここでは、電話番号は MOBILE, HOME, WORK のどれかになるように指定しています。

個々の要素に付いている " = 1" や " = 2" といったマーカーは、バイナリ円コーディングで使われるユニークな「タグ」を識別しています。 1-15 のタグ番号は、それより大きな番号をエンコードするよりも 1byteだけ小さくすみます。 最適化のために、これらのタグはよく使われたり repeatedな要素に使い、16以上のタグはあまり使われない optinal な要素に使うように決めることができます。 repeatedフィールドの各要素はそれぞれタグをエンコードするので、repeated要素はこの最適化のすぐれた候補です。

それぞれのフィールドは、以下の修飾語のいずれかで注釈されなければなりません:
required

フィールドの値は必ず与えられなければなりません。与えられない場合は、そのメッセージは「初期化されていない」とみなされます。libprotobufがデバッグモードでコンパイルされていたら、メッセージの直列化/非直列化はアサーション失敗を起こします。最適化ビルドではこのチェックはスキップされて、メッセージは書きだされます。しかし、初期化されていないメッセージの解析は常に失敗します(parseメソッドがfalseを返します)。これ以外は、requiredフィールドは optinalフィールドと全く同じように振る舞います。

optional

このフィールドはセットされることもされないこともある。optionalフィールドがセットされなかったら、デフォルト値が使われる。上の例の PhoneNumber の type フィールドでやっているように、単純な型に対してはデフォルト値を指定することができる。指定されていない場合は、システムのデフォルト値が使われる: 数値型ではゼロ、文字列型ではは空文字列、真偽値型には偽である。組み込まれたメッセージのデフォルト値は常に、メッセージの「デフォルトインスタンス」または「プロトタイプ」と呼ばれるもので、フィールド値が何もセットされていないものである。明示的に値がセットされていない optional(またはrequird)フィールドの値を取得するためにアクセサを呼ぶと、常にデフォルト値を返す。

repeated

このフィールドは、任意の回数(ゼロを含む)繰り返される。プロトコルバッファ中では、repeated値の順序は保存される。repeatedフィールドは動的なサイズの配列のように考えられる。

注: **requiredは永遠です** フィールドを required にする際は注意深くなるべきです。 ある時に required フィールドの書き込みや送信をやめたくなったとしても、 そのフィールドを optional に変更するのは問題となります。 古い読み取り器は、そのフィールドが無いメッセージを不完全なものとみなし、 意識せずにそのメッセージを拒絶したり捨てたりするかもしれません。 optional にする代わりに、アプリケーションカスタムのバッファ検証ルーチンを書くことを考えるべきです。 Google の何人かのエンジニアは、required を使うことは利点よりも害悪が多いという結論に達しました。 彼らは optional と repeated のみを使うやり方を好んでいます。 しかしながら、この考えは普遍的ではありません。

全ての可能なフィールド型を含む、.protoファイルを書くための完全な手引きは、[[pb_proto][Protocol Buffers 言語ガイド]]で見つけられます。しかしながら、クラス継承のような機能を探すのは止めましょう… Protocol Buffers にはそのような機能はありません。

プロトコルバッファをコンパイルする

これで .proto ができました。次は AddressBook(と PersonPhoneNumber)メッセージを読み書きするクラスを生成することが必要です。 これを行うには、プロトコルバッファのコンパイラである protoc を .proto ファイルに適用します:
  1. コンパイラをインストールしていなかったら、パッケージをダウンロードして README に従ってください。

  2. ソースディレクトリ(アプリケーションのソースコードのある場所―指定しなければカレントディレクトリになります)、出力ディレクトリ(生成コードの置かれる場所。ほとんどは$SRC_DIRと同じ)、ソースディレクトリからの .protoファイルへの相対パスを指定してコンパイラを起動します。この場合は、
    protoc -I=$SRC_DIR --cpp_out=$DST_DIR addressbook.proto
    になります。C++クラスがほしいので、--cpp_outオプションを使います。他の言語にも同様のオプションが提供されています。
これは出力ディレクトリに次のファイルを生成します:
  • addressbook.pb.h. 生成クラスの宣言のあるヘッダ。
  • addressbook.pb.cc. クラスの実装。

Protocol Buffers の API

生成されたコードを見て、どんなクラスや関数が作られたのか調べていきます。 tutorial.pb.hを覗くと、tutorial.protoで書いたメッセージそれぞれのクラスがあるのが分かります。Personクラスに注目してみれば、コンパイラがそれぞれのフィールドのアクセサを生成したのが分かります。例えば、name, id, email, phoneフィールド用に、次のメソッドができています:

  // name
  inline bool has_name() const;
  inline void clear_name();
  inline const ::std::string& name() const;
  inline void set_name(const ::std::string& value);
  inline void set_name(const char* value);
  inline ::std::string* mutable_name();

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

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

   // phone
  inline int phone_size() const;
  inline void clear_phone();
  inline const ::google::protobuf::RepeatedPtrField< ::tutorial::Person_PhoneNumber >& phone() const;
  inline ::google::protobuf::RepeatedPtrField< ::tutorial::Person_PhoneNumber >* mutable_phone();
  inline const ::tutorial::Person_PhoneNumber& phone(int index) const;
  inline ::tutorial::Person_PhoneNumber* mutable_phone(int index);
  inline ::tutorial::Person_PhoneNumber* add_phone();

見て分かるように、ゲッターはフィールド名をちょうど小文字にした名前で、セッターはその先頭 set_が付いた名前です。同様に、一個要素(required or optional)のフィールドには has_メソッドがあり、フィールドがセットされていたら真を返します。最後に、それぞれのフィールドは clear_メソッドがあり、フィールドを空の状態へ戻します。

数値の idフィールドは上述のセッターだけがありますが、nameemail フィールドは文字列なので、二つの追加メソッドがあります。 文字列の実ポインタを返す mutable_ゲッターと、もう一つのセッターです。 emailがセットされていなくても、mutable_email()を呼べることに注意してください。これは自動的に空文字列に初期化されます。 一個要素のメッセージ型フィールドには、mutable_メソッドがありますが、set_メソッドはありません。

repeatedフィールドにもいくつかの特別なメソッドがあります。 repeated な phone フィールドを見てみれば、次のことができるのが分かります:
  • repeatedフィールドの _size(言い換えると、この Phone にいくつの PhoneNumber が関連付けられているか)を調べる。
  • 指定インデックスの PhoneNumber を得る
  • 指定インデックスの PhoneNumber を更新する
  • 編集した PhoneNumber をメッセージに追加する(repeatedなスカラー型には、直接新しい値を渡せる add_ があります)

それぞれのフィールド定義にプロトコルコンパイラがどんなメンバを生成するかの性格な情報は、[[pb_ref_cpp-generated][C++生成コードリファレンス]]を参照してください。

Enum とネストしたクラス

生成コードには、.proto の列挙型に対応して PhoneType列挙型が含まれています。 この型には Person::PhoneType で参照でき、値には Person::MOBILE, Person::HOME, Person::WORK で参照できます(実装の詳細はもう少し複雑ですが、列挙型を使う分には理解する必要はありません)。

同様に、コンパイラは Person::PhoneNumber というネストクラスを生成します。 コードを見てみれば、「本当の」クラスは Person_PhoneNumber なのが分かりますが、Person の中の typedef がネストされたクラスのように扱えるようにしています。 この方法で違いが生じる唯一のケースは、別のファイルでクラスを前方宣言したいときです。 C++ ではネストされた型を前方宣言することはできませんが、Person_PhoneNumber は前方宣言することができます。

標準のメッセージメソッド

それぞれのメッセージクラスは、メッセージ全体をチェックしたり取り扱ったりするいくつかのメソッドを含んでいます。

  • bool IsInitialized() const;: 全てのrequiredフィールドがセットされたか確認する。
  • string DebugString() const;: メッセージの人間の読める表現を返す。デバッグで便利。
  • void CopyFrom(const Person& form);: 与えられたメッセージの値で、このメッセージを上書きする。
  • void Clear();: 全ての要素を空状態へ戻す。

Messageインターフェースを実装した、これらのメソッドと下のセクションで述べられる I/Oメソッドは、全ての C++ のプロトコルバッファクラスで共有されます。詳しくは、Messageの完全な APIドキュメント を見てください。

解析と直列化

最後に、それぞれのプロトコルバッファクラスには、メッセージを バイナリ形式を用いて読み書きするメソッドがあります。

bool SerializeToString(string* output) const;
メッセージを直列化して、与えられた string に格納する。バイト列はテキストではなくてバイナリなのに注意。我々は stringクラスを、便利なコンテナとしてのみ使っている。
bool ParseFromString(const string& data);
メッセージを与えられた string から解析する。
bool SerializeToOstream(ostream* output) const;
メッセージを与えられた C++ ostream へ書き出す。
bool ParseFromIStream(istream* input);
メッセージを与えられた C++ istream から解析する。

Protocol Buffers とオブジェクト指向設計

Protocol Buffers のクラスは、基本的には(C++の構造体のような)単純なデータホルダーです。 これらは、オブジェクトモデルでの良きファーストクラス市民にはなりません。 生成クラスに裕福な振る舞いを追加したかったら、アプリケーション特化のクラスで生成されたクラスをラップするのが最適の方法です。 (他のプロジェクトから再利用するとかで、).proto ファイル設計のコントロールが不要だったら、プロトコルバッファをラップするのも良いアイデアです。 この場合にラッパークラスは、アプリケーション特有の環境により適合したインターフェースを作るのに使えます。 いくつかのデータとメソッドを隠蔽したり、便利な関数を公開したりといったことです。 *生成されたクラスを継承して、新たな振る舞いを追加するべきではありません。* このやり方は内部のメカニズムを破壊しますし、オブジェクト指向の優れたプラクティスでもありません。

Writing A Message

いよいよプロトコルバッファのクラスを使ってみましょう。 アドレスブックアプリケーションにさせたい最初のことは、人物データをアドレスブックファイルへ書き出すことです。 これをするためには、プロトコルバッファのクラスのインスタンスを生成して、それを出力ストリームへ書き出す必要があります。

AddressBookをファイルから読み出し、ユーザーの入力した新たな Person を追加し、新たな AddressBook をファイルへ再び書き戻すプログラムがこれです。

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

// ユーザーの入力を元に Person メッセージを埋める。
void PromptForAddress(tutorial::Person* person) {
  cout << "Enter person ID number: ";
  int id;
  cin >> id;
  person->set_id(id);
  cin.ignore(256, '\n');

  cout << "Enter name: ";
  getline(cin, *person->mutable_name());

  cout << "Enter email address (blank for none): ";
  string email;
  getline(cin, email);
  if (!email.empty()) {
    person->set_email(email);
  }

  while (true) {
    cout << "Enter a phone number (or leave blank to finish): ";
    string number;
    getline(cin, number);
    if (number.empty()) {
      break;
    }

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

    cout << "Is this a mobile, home, or work phone? ";
    string type;
    getline(cin, type);
    if (type == "mobile") {
      phone_number->set_type(tutorial::Person::MOBILE);
    } else if (type == "home") {
      phone_number->set_type(tutorial::Person::HOME);
    } else if (type == "work") {
      phone_number->set_type(tutorial::Person::WORK);
    } else {
      cout << "Unknown phone type.  Using default." << endl;
    }
  }
}

// メイン関数:
//   ファイルからアドレス帳の全体を読み込み、
//   ユーザー入力を元に 1つの person を追加し、
//   同じファイルへ書き出す。
int main(int argc, char* argv[]) {
  // コンパイルしたバージョンとリンクするバージョンが互換性あることを確認。
  GOOGLE_PROTOBUF_VERIFY_VERSION;

  if (argc != 2) {
    cerr << "Usage:  " << argv[0] << " ADDRESS_BOOK_FILE" << endl;
    return -1;
  }

  tutorial::AddressBook address_book;

  {
    // 既存のアドレス帳を読み込む
    fstream input(argv[1], ios::in | ios::binary);
    if (!input) {
      cout << argv[1] << ": File not found.  Creating a new file." << endl;
    } else if (!address_book.ParseFromIstream(&input)) {
      cerr << "Failed to parse address book." << endl;
      return -1;
    }
  }

  // アドレスを追加する。
  PromptForAddress(address_book.add_person());

  {
    // 更新したアドレス帳をディスクへ書き出す。
    fstream output(argv[1], ios::out | ios::trunc | ios::binary);
    if (!address_book.SerializeToOstream(&output)) {
      cerr << "Failed to write address book." << endl;
      return -1;
    }
  }

  return 0;
}

GOOGLE_PROTOBUF_VERIFY_VERSIONマクロに注意してください。 厳密には必要ではありませんが、C++ Protocol Buffers ライブラリを使用する前にこのマクロを実行することは良いプラクティスです。 コンパイルしたヘッダのバージョンと互換性の無いライブラリバージョンへ偶然にリンクしないようチェックしてくれます。 バージョンの不一致が検出されるとプログラムは停止します。 全ての .pb.cc ファイルは起動時に自動的にこのマクロを呼び出します。

メッセージを読み込む

当然のことながら、情報を取り出せないアドレスブックは使い物になりません! これは、上の例で作られたファイルから読み込んで、その中の全ての情報を表示する例です。

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

// AddressBook 中の全ての people に対して、その情報を出力する。
void ListPeople(const tutorial::AddressBook& address_book) {
  for (int i = 0; i < address_book.person_size(); i++) {
    const tutorial::Person& person = address_book.person(i);

    cout << "Person ID: " << person.id() << endl;
    cout << "  Name: " << person.name() << endl;
    if (person.has_email()) {
      cout << "  E-mail address: " << person.email() << endl;
    }

    for (int j = 0; j < person.phone_size(); j++) {
      const tutorial::Person::PhoneNumber& phone_number = person.phone(j);

      switch (phone_number.type()) {
        case tutorial::Person::MOBILE:
          cout << "  Mobile phone #: ";
          break;
        case tutorial::Person::HOME:
          cout << "  Home phone #: ";
          break;
        case tutorial::Person::WORK:
          cout << "  Work phone #: ";
          break;
      }
      cout << phone_number.number() << endl;
    }
  }
}

// メイン関数:
//   アドレス帳の全体を読み込み、その中の全ての情報を出力する。
int main(int argc, char* argv[]) {
  // コンパイルしたバージョンとリンクするバージョンが互換性あることを確認。
  GOOGLE_PROTOBUF_VERIFY_VERSION;

  if (argc != 2) {
    cerr << "Usage:  " << argv[0] << " ADDRESS_BOOK_FILE" << endl;
    return -1;
  }

  tutorial::AddressBook address_book;

  {
    // 既存のアドレス帳を読み込む
    fstream input(argv[1], ios::in | ios::binary);
    if (!address_book.ParseFromIstream(&input)) {
      cerr << "Failed to parse address book." << endl;
      return -1;
    }
  }

  ListPeople(address_book);

  return 0;
}

プロトコルバッファを拡張する

遅かれ早かれ、プロトコルバッファを使うコードをリリースしてから、プロトコルバッファの定義を「改善」したくなるのは疑いありません。 まず間違いなくそうしたいでしょうけれど、新しいバッファに後方互換性を、古いバッファに前方互換性を持たせたかったら、従うべきいくつかのルールがあります。 プロトコルバッファの新しいバージョンでは:
  • 既存のフィールドのタグ番号を変えることは *禁止* です。
  • requiredフィールドの追加や削除は *禁止* です。
  • optional または repeated フィールドを消し *てもよい* です。
  • 新しい optional または repeated フィールドを追加 *してもよい* ですが、新しいタグ番号(消去されたフィールドではなく、このプロトコルバッファで今まで使われていないタグ番号です)を使わなければなりません。

(このルールには いくつかの例外があります。しかしそれらは使われることは稀です。)

これらのルールに従えば、古いコードは新しいメッセージを読み、新しいフィールドを単に無視します。 古いコードでは、消された optionalフィールドは単純にデフォルト値になり、消された repeatedフィールドは空になります。 新しいコードも古いメッセージを透過的に読みます。 ですが、新しい optional フィールドは古いメッセージでは存在しないことを記憶に留めておいてください。 has_ を使って、それらのフィールドが明示的にセットされているかチェックするか、タグ番号の後の [default = value]構文で合理的なデフォルト値を .proto ファイルに与える必要があるでしょう。 optional要素にデフォルト値が指定されていない場合、型に従ったデフォルト値が使われます。string では、デフォルト値は空文字列です。真偽値では、デフォルト値は偽です。数値型では、デフォルト値はゼロです。 新たな repeated フィールドを追加した場合は _has フラグが無いので、その値が(新たなコードで)空のままになっているのか、(古いコード)でセットされなかったのかを区別できないことに注意してください。

もっと速くする

デフォルトでは、プロトコルバッファのコンパイラはほとんどの機能(解析や直列化)の実装にリフレクションを使うことでファイルを小さくしようとします。 ですが、コンパイラはメッセージ型で最適化されたコードを生成することもできます。 ほとんどの場合で、一桁くらいの性能増加となりますが、コードサイズは二倍になります。 プロトコルバッファのライブラリで多くの時間を費やしているとプロファイラが示していたら、最適化モードを変えてみるべきです。 .proto ファイルに以下の行を追加するだけです:
option optimize_for = SPEED;

プロトコルコンパイラを掛け直せば、解析や直列化などのかなり速いコードを生成します。

これをしても依然として満足できなければ、C++ でメッセージオブジェクトを再利用するのが別の良い方法です。 再利用するメッセージは、クリアした時もメモリのどこかへ保持しておきます。 連続した同じ型の同じ構造のたくさんのメッセージを扱うのでしたら、同じメッセージオブジェクトを再利用してメモリアロケータの負荷を取り除くのは良い考えです。 ですが、信頼できないデータを解析する時は注意してください! 悪意あるユーザーは全然違う構造のメッセージ列をあなたのアプリケーションへ送ることができます(例えば、それぞれは異なった 未知のフィールドの集合にできます)。 個々のメッセージは小さいですが、オブジェクトには将来のメッセージで再利用されないメモリが割り当てられることになります。長期的には、それらを合計したメモリ量は大きなものとなります。 安全側に倒すなら、信頼できないメッセージの解析には、毎回新しいオブジェクトを割り当てるべきでしょう。

高度な利用方法

プロトコルバッファには、簡単なアクセサと直列化を超えた使い方があります。 その他のできることを調べるには、C++ API リファレンスを探索することは避けられません。

メッセージクラスで提供される、鍵となる一つの機能は *リフレクション* です。 特別のメッセージ型に対応したコードを書かずとも、メッセージのフィールドを反復して、それらの値を取り扱うことができます。 リフレクションの有用な使い道の一つは、プロトコルメッセージと XML や JSON などの他の円コーディングとの間の変換です。 リフレクションのさらに進んだ使い方は、同じ型の二つのメッセージの違いを見つけることや、メッセージの内容に一致する表現を記述できる「プロトコルメッセージの正規表現」のようなものの開発でしょう。 イマジネーションを働かせれば、プロトコルバッファをあなたが最初に期待したよりもっと広い問題へ適用することもできるでしょう。

リフレクションは、Message::Reflection インターフェースで提供されています。