TL;DR
というわけで、作ったので宣伝します。Boostを使っているプロジェクトでTOMLを使いたい人はぜひ使って下さい。C++98, 11, 14, 17でテスト済みです。ライセンスはBoostと同じゆるいライセンスです。
TOML v0.4.0に対応していますが、加えてd3d6f32にある機能にも対応しています。
2018/7/29 追記:
7/11にTOML v0.5.0がリリースされました。追加の宣伝ですが、Boost.tomlは現時点(7/29)では唯一v0.5.0をサポートしているTOMLパーサー/エンコーダーです。
背景
皆さん、設定ファイルはどうしていますか? ちゃんと文法を定めるのは面倒ですが、文法無しでノリでやっていると大変になってくるところでもあります。
TOMLは人間にも書きやすく、読みやすいことを目指した設定ファイル用言語です。既にある程度広まっており、(個人の経験により偏っていますが)例えば
- Rustのパッケージマネージャ、Cargo
- Vimのプラグインマネージャ、dein
- GitLab CIのgitlab-runner
- 静的サイトジェネレータ、Hugo
- etc...
など多数のプロジェクトが設定ファイルとして使っています。
これをC++プロジェクトで使いたかったので、紆余曲折を経て3つもTOMLパーサを作ってしまいました。紆余曲折は気が向けば追記します。
Boostを使わないバージョンのパーサも作っていて、ここにあります(こちらはC++11以降を要求します)。
使い方
TOMLファイルの読み込みとデータの取り出し
以下レポジトリのREADMEにも書いていますが、かいつまんで説明します。
以下のようなTOMLファイルがあったとしましょう。
title = "TOML Example"
[owner]
name = "Tom Preston-Werner"
dob = 1979-05-27T07:32:00-08:00
これをBoost.tomlを使って読み込むには、以下のようにします。
const toml::table file = toml::parse("example.toml");
std::cout << "the title is -> " << toml::get<std::string>(file.at("title"));
const toml::table& owner = toml::get<toml::table>(file.at("owner"));
std::cout << "owner's name is -> " << toml::get<std::string>(owner.at("name"));
std::cout << "date of birth -> " << toml::get<toml::offset_datetime>(owner.at("dob"));
toml::parse
にファイル名を渡すと、TOMLファイル内のデータがtable
として返ってきます。もちろんstd::ifstream
などのストリームを渡すこともできます。
そして、toml::table
はboost::container::flat_map
のエイリアスなので、operator[]
やat
、count
やbegin/end
などが使えます。
何から何へのマップかというと、toml::key
からtoml::value
です。toml::key
は単にstd::string
で、toml::value
は内部にboost::variant
としてTOMLのデータ型のどれかを持っているクラスです。
このtoml::value
から値を取り出す方法はいくつかありますが、一番使い勝手が良いのがtoml::get
だと思います。これはtemplate
引数で渡した型にTOMLのデータ型からキャストできる場合、その値に変換して返します。もし持っている型と全く同じ型が渡された場合、左辺値参照を返します。
この関数の強力な点は、toml::array
を中身ごと変換して取り出せるということでしょう。こんなtoml::array
があったとします。
ports = [ 8001, 8001, 8002 ]
これをあなたの好きなコンテナに入れて返すことができます。それがstd::array
であってもです。1
const auto v = toml::get<std::vector<int> >(file.at("ports"));
const auto a = toml::get<std::array<int, 3>>(file.at("ports"));
STL以外のコンテナ? もちろん使えます。ここでコンテナとみなされるのは、iterator
とvalue_type
を持っており、begin
でOutputIterator
を取り出せるクラスです。2
それどころではありません。以下のようなこともできます。
const auto t = toml::get<std::tuple<int, int, int>>(file.at("ports"));
これが何の役に立つのか、と困惑している方のために、これが便利になるケースをご紹介します。以下のようなファイルがあったとします。そしてこのdata
はこのような型を持っていると知っていたとします(むしろ、string
のarray
とinteger
のarray
でなければ例外を投げてよいとしましょう)。
[clients]
data = [ ["gamma", "delta"], [1, 2] ]
勘のいい人はもうどんなコードになるかおわかりでしょうが、これは2つのstd::vector
のstd::pair
で受け取れます。
const auto data = toml::get<std::pair<std::vector<std::string>, std::vector<int>>>(clients.at("data"));
もちろん、Array of Tablesなどは単なるtoml::table
のarray
なので、以下のようなファイルがあったとき……
[[fruit]]
name = "apple"
[[fruit]]
name = "banana"
……このようにして読み込めます。
const auto fruit = toml::get<std::vector<toml::table>>(file.at("fruit"));
また、dotted keysはある種のインラインテーブルになります。
physical.color = "orange"
physical.shape = "round"
上記は、以下のようにして取り出します。
const auto& physical = toml::get<toml::table>(file.at("physical"));
const auto color = toml::get<std::string>(physical.at("color"));
const auto shape = toml::get<std::string>(physical.at("shape"));
ちなみに、C++17コンパイラを持っている場合、std::string
の代わりにstd::string_view
も使えます。C++17を使えない場合でも、心配しないで下さい。我々にはBoostがついています。まったく同じやり方で、boost::string_ref
とboost::string_view
が使えます。
データの型を確認する・確認せずにデータを取り出す
さて、ここまではデータの型がわかっている前提で話をしてきました。しかしそれは常にわかるものではありません。
まず、Boost.tomlは型を確認するためのenum
を持っています。以下のように使えます。
if (file.at("unknown").is(toml::value::string_tag)) {/* do some stuff ...*/}
else if(file.at("unknown").is(toml::value::integer_tag)) {/* do some stuff ...*/}
もちろんswitch-case
を使ったほうが良い場面も多いでしょう。
しかしこれを毎度書くのは少し面倒です。Boost.tomlはBoostによって強化されているので、boost::variant
と同じ要領でvisitor
を使うことができます。
struct printer : boost::static_visitor<void> {
template<typename T>
void operator()(const T& v) {std::cout << v << std::endl;}
};
toml::apply_visitor(printer(), file["unknown"]);
その前に、そもそもデータがあるかどうか確認するべきかもしれません。toml::table
はboost::container::flat_map
なので、count
やfind
が当然使えます。
if(file.count("key") == 1) {
const auto& key = toml::get<std::string>(file.at("key"));
}
日時と時刻
Boost.tomlでは、offset_datetime
、local_datetime
、date
、time
の全てをサポートしています。
Boost.Date_Timeの機能を使って、Boost.tomlは日時操作を簡単にできるようにしています。
toml::value d(2018, toml::Jun, 1);
toml::value t(toml::hours(18) + toml::minutes(30));
toml::value dt(d, t);
さらに、std::tm
やstd::chrono
として取り出せるようになっています。
const auto tp = toml::get<std::chrono::system_clock::time_point>(v.at("datetime"));
const auto tm = toml::get<std::tm>(v.at("datetime"));
Boost.chronoは(いくつかのマクロ定義をしなければ)ビルドが必要なので、まだ対応していません。これで失敗した時に生じるコンパイルエラーは少し複雑なので、どうしたら使いやすくできるかを考えているところです。
DateTimeも一部機能(to_string
やfrom_string
など)はリンクが必要なので、それを使った場合コンパイルが失敗する可能性があります。このときは単にリンクエラーになるでしょう。
TOMLファイルを書き出す
Boost.tomlは出力にも対応しています。toml::value
だけでなく、全てのTOML型(toml::string
、toml::table
など)がサポートされています。
toml::format
関数を使うか、単に出力ストリームに流しこむだけで出力ができます。
toml::value v;
std::string str = toml::format(v);
std::cout << v << std::endl;
tomlのliteral文字列を使う
実はtoml::string
は単なるstd::string
ではなく、basic
とliteral
のフラグを持っています。
通常、この差異は気にならないようになっています。toml::get<std::string>
は何事もなかったかのように参照を返しますし、toml::string
をstd::string
で初期化するとbasic文字列として初期化されます。
ただし、literal文字列を使うと指定することもできます。
toml::value v0("foo"); // It will be a basic string by default.
toml::value v1("foo", toml::string::basic);
toml::value v2("bar", toml::string::literal);
multiline stringを使う・arrayを折り返す・inline tableを使う
人間は文字列や配列が長くなってくると、折り返したくなるものです。
Boost.tomlは折り返しをサポートしています。一行ごとの文字数を決めると、それを超える場合string
やarray
は複数行に分割されます。
toml::value str("too long string would be splitted to multiple lines, "
"and the threshould can be passed to toml::format function. "
"By default, the threshold is 80.");
std::cout << toml::format(str);
// """
// too long string would be splitted to multiple lines, and the threshould can be \
// passed to toml::format function. By default, the threshold is 80.\
// """
toml::value ary{
"If an array has many elements so the result of toml::format become longer",
"than a threshold toml::format will print them in multi-line array."
};
std::cout << toml::format(ary);
// [
// "If an array has many elements so the result of toml::format become longer",
// "than a threshold toml::format will print them in multi-line array.",
// ]
toml::string
については、文字列が改行を持っていたり、literal文字列が'
を持っていたりすると、上の状況以外でもmulti-line stringになります。
また、Array of Tablesの場合に限り、tableがinline tableになることがあります。一行の最大文字数以内に収まるならinline tableのarrayになり、そうでないなら通常のArray of Tablesになります。
折り返す閾値は、デフォルトで80になっていますが、toml::format(value, 100)
のように値を渡すことで変更できます。
また、ストリームを使っている場合、std::setw()
を使うことも出来ます。あまり変な値を入れると出力がおかしくなりますが。
何故Boost?
「紆余曲折」している間にC++11で書いていたんですが、どうしても面倒な部分がいくつかありました。例えばC++11にはstd::variant
がないので、union
で頑張らないといけません。C++11でunion
に関しての制限がいくつか解除されているので可能ではあるのですが、面倒には違いありませんでした。
もう一つの大きな理由は、std::allocator
が不完全型を許容しないということです。これはどういうことかというと、例えば以下のような構造体を作ろうとしていると思って下さい。
struct X {
int i;
std::vector<X> array;
};
array
の宣言時点では、X
はまだ不完全型です。なのでstd::vector<X>
は使えません。
この問題が生じるため、C++11版ではstd::vector
へのポインタを持っていました。
しかしstd::vector
は本来不完全型を許容できるはずです。実際、C++17では不完全型の最小限のサポートが入りました。
ただ、Boost.Containerを使えば、STLと同じインターフェースのコンテナに不完全型を入れることができます。
このおかげで、toml::value
の実装が非常にシンプルになりました。
struct value {
boost::variant<boolean, integer, string, // ...
boost::container::vector<value>,
boost::container::flat_map<key, value>
> storage_;
};
これがBoostを使うことにした最大の理由です。C++98でも使えるようにしたのは、折角だから、くらいの理由です。
Boost.PropertyTreeの存在は知っていましたが、上記のような実装にしたかったので使っていません。
まとめ
作りました。
もしバグを発見したりした場合、ぜひIssueで知らせて下さい。PRも歓迎です!