Edited at

Boostを使ってC++でTOMLファイルを読み書きできるライブラリを作った

More than 1 year has passed since last update.


TL;DR

というわけで、作ったので宣伝します。Boostを使っているプロジェクトでTOMLを使いたい人はぜひ使って下さい。C++98, 11, 14, 17でテスト済みです。ライセンスはBoostと同じゆるいライセンスです。

TOML v0.4.0に対応していますが、加えてd3d6f32にある機能にも対応しています。

https://github.com/ToruNiina/Boost.toml


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にも書いていますが、かいつまんで説明します。

https://github.com/ToruNiina/Boost.toml

以下のような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::tableboost::container::flat_mapのエイリアスなので、operator[]atcountbegin/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以外のコンテナ? もちろん使えます。ここでコンテナとみなされるのは、iteratorvalue_typeを持っており、beginOutputIteratorを取り出せるクラスです。2

それどころではありません。以下のようなこともできます。

const auto t = toml::get<std::tuple<int, int, int>>(file.at("ports"));

これが何の役に立つのか、と困惑している方のために、これが便利になるケースをご紹介します。以下のようなファイルがあったとします。そしてこのdataはこのような型を持っていると知っていたとします(むしろ、stringarrayintegerarrayでなければ例外を投げてよいとしましょう)。

[clients]

data = [ ["gamma", "delta"], [1, 2] ]

勘のいい人はもうどんなコードになるかおわかりでしょうが、これは2つのstd::vectorstd::pairで受け取れます。

const auto data = toml::get<std::pair<std::vector<std::string>, std::vector<int>>>(clients.at("data"));

もちろん、Array of Tablesなどは単なるtoml::tablearrayなので、以下のようなファイルがあったとき……

[[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_refboost::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::tableboost::container::flat_mapなので、countfindが当然使えます。

if(file.count("key") == 1) {

const auto& key = toml::get<std::string>(file.at("key"));
}


日時と時刻

Boost.tomlでは、offset_datetimelocal_datetimedatetimeの全てをサポートしています。

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::tmstd::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_stringfrom_stringなど)はリンクが必要なので、それを使った場合コンパイルが失敗する可能性があります。このときは単にリンクエラーになるでしょう。


TOMLファイルを書き出す

Boost.tomlは出力にも対応しています。toml::valueだけでなく、全てのTOML型(toml::stringtoml::tableなど)がサポートされています。

toml::format関数を使うか、単に出力ストリームに流しこむだけで出力ができます。

toml::value v;

std::string str = toml::format(v);
std::cout << v << std::endl;


tomlのliteral文字列を使う

実はtoml::stringは単なるstd::stringではなく、basicliteralのフラグを持っています。

通常、この差異は気にならないようになっています。toml::get<std::string>は何事もなかったかのように参照を返しますし、toml::stringstd::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は折り返しをサポートしています。一行ごとの文字数を決めると、それを超える場合stringarrayは複数行に分割されます。

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の存在は知っていましたが、上記のような実装にしたかったので使っていません。


まとめ

作りました。

https://github.com/ToruNiina/Boost.toml

もしバグを発見したりした場合、ぜひIssueで知らせて下さい。PRも歓迎です!






  1. これは新しいコンテナを作っているので、巨大な配列について行うと時間がかかります。その場合、toml::arrayを指定すればboost::container::vector<toml::value>への参照が取り出せるので、無駄なコピーを避けることができます。 



  2. ここで、key_typemapped_typeを持っているとmapであるとみなされます。また、std::stringstd::string_viewarrayとはみなされません。