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

# TL;DR

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

TOML v0.4.0に対応していますが、加えて[d3d6f32](https://github.com/toml-lang/toml/tree/d3d6f32b73369a9bfd8411a143718f7a4a84ee2c)にある機能にも対応しています。

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を使わないバージョンのパーサも作っていて、[ここ](https://github.com/ToruNiina/Boost.toml)にあります（こちらはC++11以降を要求します）。

# 使い方

## TOMLファイルの読み込みとデータの取り出し

以下レポジトリのREADMEにも書いていますが、かいつまんで説明します。

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

以下のようなTOMLファイルがあったとしましょう。

```toml
title = "TOML Example"

[owner]
name = "Tom Preston-Werner"
dob = 1979-05-27T07:32:00-08:00
```

これをBoost.tomlを使って読み込むには、以下のようにします。

```cpp
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`があったとします。

```toml
ports = [ 8001, 8001, 8002 ]
```

これをあなたの好きなコンテナに入れて返すことができます。それが`std::array`であってもです。[^1]

```cpp
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]

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

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

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

```toml
[clients]
data = [ ["gamma", "delta"], [1, 2] ]
```

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

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

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

```toml
[[fruit]]
  name = "apple"
[[fruit]]
  name = "banana"
```

……このようにして読み込めます。

```cpp
const auto fruit = toml::get<std::vector<toml::table>>(file.at("fruit"));
```

また、[dotted keys](https://github.com/toml-lang/toml#user-content-keys)はある種のインラインテーブルになります。

```toml
physical.color = "orange"
physical.shape = "round"
```

上記は、以下のようにして取り出します。

```cpp
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`を持っています。以下のように使えます。

```cpp
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`を使うことができます。

```cpp
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`が当然使えます。

```cpp
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は日時操作を簡単にできるようにしています。

```cpp
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`として取り出せるようになっています。

```cpp
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`関数を使うか、単に出力ストリームに流しこむだけで出力ができます。

```cpp
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文字列を使うと指定することもできます。

```cpp
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`は複数行に分割されます。

```cpp
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`が不完全型を許容しないということです。これはどういうことかというと、例えば以下のような構造体を作ろうとしていると思って下さい。

```cpp
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`の実装が非常にシンプルになりました。

```cpp
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_type`か`mapped_type`を持っていると`map`であるとみなされます。また、`std::string`や`std::string_view`も`array`とはみなされません。