このドキュメントは、Protocol Buffers を C++ で使うための包括的な手引きではありません。 より詳細な情報については、Protocol Buffers 言語ガイド、C++ APIリファレンス、C++生成コードガイド、エンコーディングリファレンスを参照してください。
これから使っていくのは、知人のコンタクト方法をファイルへ読み書きができる簡単な「アドレスブック」アプリケーションです。 アドレスブックではそれぞれの人の名前、ID、メールアドレス、電話番号を記述できます。
こういった構造化データをどうやって直列化し、元に戻しますか? この問題を解くためにはいくつかの方法があります。
メモリ上のデータ構造をそのままバイナリ形式に送信/保存するやり方があります。 受信/読み込みのコードは正確に同じメモリレイアウトやエンディアンなどでコンパイルされなければならないので、長期的にはこれは脆弱な手法です。また、データを生形式で保存ファイルや、この形式専用のソフトウェアのコピーが拡散してしまうので、フォーマットを拡張することがとても困難になります。
4つの数値を "12:3:-23:67" という風にデータを単一の文字列へ符号化するアドホックな方法を発明することができます。これは単純で柔軟な手法ですが、符号化し解析する一度きりのコードを書く必要があります。また、この解析は多少の実行時コストを伴います。これは単純なデータを符号化する場合には最適です。
データを XML へ直列化する。XML は(ある種の)人間の読めるものですし、多くの言語用のライブラリがあるので、この手法は魅力的なものとなりえます。あなたが他のアプリケーション/プロジェクトとデータを共有したいのなら、これは良い選択です。しかしながら XML は大きなサイズを占めることで悪名が高く、またアプリケーションに符号化/復号には多大なパフォーマンスのペナルティがかかります。また、XML DOMツリーを辿るのは、クラスで普通にフィールドを見るのよりもかなり複雑です。
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要素はこの最適化のすぐれた候補です。
フィールドの値は必ず与えられなければなりません。与えられない場合は、そのメッセージは「初期化されていない」とみなされます。libprotobufがデバッグモードでコンパイルされていたら、メッセージの直列化/非直列化はアサーション失敗を起こします。最適化ビルドではこのチェックはスキップされて、メッセージは書きだされます。しかし、初期化されていないメッセージの解析は常に失敗します(parseメソッドがfalseを返します)。これ以外は、requiredフィールドは optinalフィールドと全く同じように振る舞います。
このフィールドはセットされることもされないこともある。optionalフィールドがセットされなかったら、デフォルト値が使われる。上の例の PhoneNumber の type フィールドでやっているように、単純な型に対してはデフォルト値を指定することができる。指定されていない場合は、システムのデフォルト値が使われる: 数値型ではゼロ、文字列型ではは空文字列、真偽値型には偽である。組み込まれたメッセージのデフォルト値は常に、メッセージの「デフォルトインスタンス」または「プロトタイプ」と呼ばれるもので、フィールド値が何もセットされていないものである。明示的に値がセットされていない optional(またはrequird)フィールドの値を取得するためにアクセサを呼ぶと、常にデフォルト値を返す。
このフィールドは、任意の回数(ゼロを含む)繰り返される。プロトコルバッファ中では、repeated値の順序は保存される。repeatedフィールドは動的なサイズの配列のように考えられる。
全ての可能なフィールド型を含む、.protoファイルを書くための完全な手引きは、[[pb_proto][Protocol Buffers 言語ガイド]]で見つけられます。しかしながら、クラス継承のような機能を探すのは止めましょう… Protocol Buffers にはそのような機能はありません。
コンパイラをインストールしていなかったら、パッケージをダウンロードして README に従ってください。
protoc -I=$SRC_DIR --cpp_out=$DST_DIR addressbook.protoになります。C++クラスがほしいので、--cpp_outオプションを使います。他の言語にも同様のオプションが提供されています。
生成されたコードを見て、どんなクラスや関数が作られたのか調べていきます。 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フィールドは上述のセッターだけがありますが、name と email フィールドは文字列なので、二つの追加メソッドがあります。 文字列の実ポインタを返す mutable_ゲッターと、もう一つのセッターです。 emailがセットされていなくても、mutable_email()を呼べることに注意してください。これは自動的に空文字列に初期化されます。 一個要素のメッセージ型フィールドには、mutable_メソッドがありますが、set_メソッドはありません。
それぞれのフィールド定義にプロトコルコンパイラがどんなメンバを生成するかの性格な情報は、[[pb_ref_cpp-generated][C++生成コードリファレンス]]を参照してください。
生成コードには、.proto の列挙型に対応して PhoneType列挙型が含まれています。 この型には Person::PhoneType で参照でき、値には Person::MOBILE, Person::HOME, Person::WORK で参照できます(実装の詳細はもう少し複雑ですが、列挙型を使う分には理解する必要はありません)。
同様に、コンパイラは Person::PhoneNumber というネストクラスを生成します。 コードを見てみれば、「本当の」クラスは Person_PhoneNumber なのが分かりますが、Person の中の typedef がネストされたクラスのように扱えるようにしています。 この方法で違いが生じる唯一のケースは、別のファイルでクラスを前方宣言したいときです。 C++ ではネストされた型を前方宣言することはできませんが、Person_PhoneNumber は前方宣言することができます。
それぞれのメッセージクラスは、メッセージ全体をチェックしたり取り扱ったりするいくつかのメソッドを含んでいます。
Messageインターフェースを実装した、これらのメソッドと下のセクションで述べられる I/Oメソッドは、全ての C++ のプロトコルバッファクラスで共有されます。詳しくは、Messageの完全な APIドキュメント を見てください。
最後に、それぞれのプロトコルバッファクラスには、メッセージを バイナリ形式を用いて読み書きするメソッドがあります。
Protocol Buffers のクラスは、基本的には(C++の構造体のような)単純なデータホルダーです。 これらは、オブジェクトモデルでの良きファーストクラス市民にはなりません。 生成クラスに裕福な振る舞いを追加したかったら、アプリケーション特化のクラスで生成されたクラスをラップするのが最適の方法です。 (他のプロジェクトから再利用するとかで、).proto ファイル設計のコントロールが不要だったら、プロトコルバッファをラップするのも良いアイデアです。 この場合にラッパークラスは、アプリケーション特有の環境により適合したインターフェースを作るのに使えます。 いくつかのデータとメソッドを隠蔽したり、便利な関数を公開したりといったことです。 *生成されたクラスを継承して、新たな振る舞いを追加するべきではありません。* このやり方は内部のメカニズムを破壊しますし、オブジェクト指向の優れたプラクティスでもありません。
いよいよプロトコルバッファのクラスを使ってみましょう。 アドレスブックアプリケーションにさせたい最初のことは、人物データをアドレスブックファイルへ書き出すことです。 これをするためには、プロトコルバッファのクラスのインスタンスを生成して、それを出力ストリームへ書き出す必要があります。
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;
}
(このルールには いくつかの例外があります。しかしそれらは使われることは稀です。)
これらのルールに従えば、古いコードは新しいメッセージを読み、新しいフィールドを単に無視します。 古いコードでは、消された optionalフィールドは単純にデフォルト値になり、消された repeatedフィールドは空になります。 新しいコードも古いメッセージを透過的に読みます。 ですが、新しい optional フィールドは古いメッセージでは存在しないことを記憶に留めておいてください。 has_ を使って、それらのフィールドが明示的にセットされているかチェックするか、タグ番号の後の [default = value]構文で合理的なデフォルト値を .proto ファイルに与える必要があるでしょう。 optional要素にデフォルト値が指定されていない場合、型に従ったデフォルト値が使われます。string では、デフォルト値は空文字列です。真偽値では、デフォルト値は偽です。数値型では、デフォルト値はゼロです。 新たな repeated フィールドを追加した場合は _has フラグが無いので、その値が(新たなコードで)空のままになっているのか、(古いコード)でセットされなかったのかを区別できないことに注意してください。
option optimize_for = SPEED;
プロトコルコンパイラを掛け直せば、解析や直列化などのかなり速いコードを生成します。
これをしても依然として満足できなければ、C++ でメッセージオブジェクトを再利用するのが別の良い方法です。 再利用するメッセージは、クリアした時もメモリのどこかへ保持しておきます。 連続した同じ型の同じ構造のたくさんのメッセージを扱うのでしたら、同じメッセージオブジェクトを再利用してメモリアロケータの負荷を取り除くのは良い考えです。 ですが、信頼できないデータを解析する時は注意してください! 悪意あるユーザーは全然違う構造のメッセージ列をあなたのアプリケーションへ送ることができます(例えば、それぞれは異なった 未知のフィールドの集合にできます)。 個々のメッセージは小さいですが、オブジェクトには将来のメッセージで再利用されないメモリが割り当てられることになります。長期的には、それらを合計したメモリ量は大きなものとなります。 安全側に倒すなら、信頼できないメッセージの解析には、毎回新しいオブジェクトを割り当てるべきでしょう。
プロトコルバッファには、簡単なアクセサと直列化を超えた使い方があります。 その他のできることを調べるには、C++ API リファレンスを探索することは避けられません。
メッセージクラスで提供される、鍵となる一つの機能は *リフレクション* です。 特別のメッセージ型に対応したコードを書かずとも、メッセージのフィールドを反復して、それらの値を取り扱うことができます。 リフレクションの有用な使い道の一つは、プロトコルメッセージと XML や JSON などの他の円コーディングとの間の変換です。 リフレクションのさらに進んだ使い方は、同じ型の二つのメッセージの違いを見つけることや、メッセージの内容に一致する表現を記述できる「プロトコルメッセージの正規表現」のようなものの開発でしょう。 イマジネーションを働かせれば、プロトコルバッファをあなたが最初に期待したよりもっと広い問題へ適用することもできるでしょう。
リフレクションは、Message::Reflection インターフェースで提供されています。