C++開発者向け:yaml-cppで学ぶYAML操作の基本

C++開発者向け:yaml-cppで学ぶYAML操作の基本

はじめに:なぜC++でYAMLを扱うのか

現代のソフトウェア開発において、設定ファイル、データ交換、ロギングなど、様々な用途でデータの構造化が求められます。古くからINIファイルやXMLが使われてきましたが、近年ではJSONやYAMLといった軽量なマークアップ言語が広く普及しています。

特にYAML (YAML Ain’t Markup Language) は、「人間にとって読みやすいデータ形式」を標榜しており、その簡潔な構文と柔軟性から、設定ファイル記述言語として、あるいは複雑な構造を持つデータの表現形式として人気を集めています。Python, Ruby, Javaなど多くの言語でYAMLを扱うライブラリが存在し、C++においても同様です。

C++は高いパフォーマンスと低レベルな制御を必要とするアプリケーション開発に広く用いられます。このようなC++アプリケーションにおいて、設定の読み込み、他のシステムとのデータ交換、あるいは複雑なデータ構造の永続化といった場面でYAMLを利用するニーズは少なくありません。しかし、C++標準ライブラリにはYAMLを直接扱う機能は含まれていません。そのため、外部ライブラリを利用する必要があります。

C++でYAMLを扱うためのライブラリはいくつか存在しますが、中でもyaml-cppは、多くのプラットフォームで利用可能であり、比較的新しいC++の機能(C++11以降)を活用して、直感的かつ安全なAPIを提供している点で広く利用されています。yaml-cppはヘッダーオンリーライブラリとしても、ビルドが必要なライブラリとしても利用でき、依存関係も少ないためプロジェクトへの組み込みも比較的容易です。

本記事では、C++開発者の皆さんに向けて、このyaml-cppライブラリを使ったYAMLの基本的な読み込みと書き込み、さらには少し進んだ操作方法について、豊富なコード例を交えながら詳細に解説します。YAMLの基本的な構文にも触れつつ、yaml-cppのAPIを使いこなすための知識を習得することを目指します。

1. yaml-cppの導入

yaml-cppをプロジェクトで利用するためには、まずライブラリを入手し、ビルド環境に組み込む必要があります。yaml-cppは多くのシステムで利用されており、インストール方法は様々ですが、ここでは一般的な方法をいくつか紹介します。

yaml-cppは、CMakeを使ったビルドシステムを採用しています。そのため、CMakeに慣れているとスムーズに導入できます。また、現代的なC++開発ではパッケージマネージャーを利用することが一般的です。ConanやvcpkgといったC++向けのパッケージマネージャーを使えば、依存関係の解決も含めて簡単にyaml-cppをプロジェクトに追加できます。

1.1 パッケージマネージャーを利用する場合

  • vcpkg:
    bash
    vcpkg install yaml-cpp:<triplet>
    # 例: vcpkg install yaml-cpp:x64-linux

    プロジェクトのCMakeLists.txtでvcpkgのツールチェインファイルを使用するように設定すれば、find_package(yaml-cpp CONFIG REQUIRED) でライブラリを検出できます。

  • Conan:
    プロジェクトのconanfile.txtやconanfile.pyにyaml-cpp/x.y.z (バージョンは適宜変更) を追加し、conan install を実行します。Conanジェネレーターを使えば、CMakeや他のビルドシステム向けのインクルードパスやリンク設定が自動生成されます。

パッケージマネージャーを使う方法は、依存関係の管理やクロスプラットフォーム対応が容易になるため推奨されます。

1.2 ソースコードからビルドする場合 (CMake)

パッケージマネージャーを使わない場合や、特定のバージョン/設定でビルドしたい場合は、GitHubからソースコードをダウンロードして自分でビルドします。

  1. ソースコードのクローン:
    bash
    git clone https://github.com/jbeder/yaml-cpp.git
    cd yaml-cpp

  2. ビルドディレクトリの作成とCMakeの実行:
    bash
    mkdir build
    cd build
    cmake ..

    デフォルトでは共有ライブラリ (SHARED_LIBS) と静的ライブラリ (BUILD_SHARED_LIBS) の両方がビルドされます。-DBUILD_SHARED_LIBS=OFF のようにオプションを指定することで静的ライブラリのみをビルドすることも可能です。また、-DYAML_CPP_BUILD_TESTS=OFF でテストのビルドをスキップできます。

  3. ビルドとインストール:
    bash
    cmake --build .
    # 必要であればインストール
    # sudo cmake --install .

    インストールしなくても、ビルドディレクトリ内のライブラリとヘッダーファイルを参照することで利用することも可能です。プロジェクトのCMakeLists.txtで、yaml-cppのビルドディレクトリを指定してインクルードパスやリンク設定を行う必要があります。

1.3 プロジェクトへの組み込み (CMakeLists.txt)

CMakeベースのプロジェクトにyaml-cppを組み込む際のCMakeLists.txtの基本的な例を示します。

“`cmake

最低限必要なCMakeバージョン

cmake_minimum_required(VERSION 3.1)

プロジェクト名

project(YamlExample)

C++標準バージョンの指定 (yaml-cppはC++11以降を推奨)

set(CMAKE_CXX_STANDARD 11)
set(CMAKE_CXX_STANDARD_REQUIRED ON)

yaml-cppを検出

vcpkgやConanを使っている場合はこれで十分

find_package(yaml-cpp CONFIG REQUIRED)

ソースファイル

add_executable(yaml_example main.cpp)

yaml-cppライブラリをリンク

target_link_libraries(yaml_example PRIVATE yaml-cpp)

もしソースコードからビルドしてインストールしていない場合、以下のようにディレクトリを指定する必要があるかもしれません

add_subdirectory(/path/to/yaml-cpp-source-or-build-dir) # add_subdirectoryで追加する場合

target_link_libraries(yaml_example PRIVATE yaml-cpp)

“`

find_package(yaml-cpp CONFIG REQUIRED) が成功すると、yaml-cpp::yaml-cpp というターゲットが利用可能になります。これを target_link_libraries で実行可能ファイル(またはライブラリ)にリンクすることで、yaml-cppのヘッダーファイルがインクルードパスに追加され、ライブラリがリンクされます。

導入が完了すれば、いよいよyaml-cppを使ったYAML操作に進むことができます。

2. YAMLの基本的な読み込み

yaml-cppを使ってYAMLファイルを読み込む最も基本的な方法は、YAML::LoadFile 関数を使用することです。文字列としてYAMLデータを扱う場合は、YAML::Load 関数を使用します。これらの関数は、YAMLドキュメント全体を表す YAML::Node オブジェクトを返します。

2.1 ファイルからの読み込み (YAML::LoadFile)

YAMLファイル config.yaml が以下のような内容であるとします。

“`yaml

アプリケーション設定

database:
host: localhost
port: 5432
username: admin
password: secure_password

server:
address: 0.0.0.0
port: 8080
timeout: 30

features:
– name: logging
enabled: true
– name: metrics
enabled: false

users:
admin:
id: 1
active: true
guest:
id: 2
active: false

empty_value: # null
null_value: null
undefined_value:
“`

このファイルを読み込むC++コードは以下のようになります。

“`cpp

include

include

include

int main() {
try {
YAML::Node config = YAML::LoadFile(“config.yaml”);

    // ファイルの読み込みに成功した場合
    std::cout << "YAML file loaded successfully." << std::endl;

    // ここからノードにアクセスしてデータを取得する

} catch (const YAML::Exception& e) {
    // YAMLパースエラーなどが発生した場合
    std::cerr << "Error loading YAML file: " << e.what() << std::endl;
    return 1;
} catch (const std::exception& e) {
    // その他の標準例外
    std::cerr << "An unexpected error occurred: " << e.what() << std::endl;
    return 1;
}

return 0;

}
“`

YAML::LoadFile はファイルが見つからない場合や、YAMLの構文エラーがある場合に YAML::Exception をスローします。そのため、try-catch ブロックで囲んでエラーハンドリングを行うことが推奨されます。

2.2 文字列からの読み込み (YAML::Load)

YAMLデータがファイルではなく、文字列としてメモリ上にある場合 (std::stringなど) は、YAML::Load 関数を使用します。

“`cpp

include

include

include

int main() {
std::string yaml_string = R”(
user:
name: John Doe
age: 30
)”; // Raw String Literals (C++11) を使用

try {
    YAML::Node user_info = YAML::Load(yaml_string);

    // 文字列のパースに成功した場合
    std::cout << "YAML string loaded successfully." << std::endl;

    // ここからノードにアクセスしてデータを取得する

} catch (const YAML::Exception& e) {
    std::cerr << "Error loading YAML string: " << e.what() << std::endl;
    return 1;
}

return 0;

}
“`

こちらもパースエラーが発生する可能性があるため、try-catch ブロックでエラーハンドリングを行います。

3. YAML::Node オブジェクトの基本

YAML::LoadFileYAML::Load が返す YAML::Node は、YAMLドキュメント内の個々の要素(スカラー、シーケンス、マップ、null、未定義ノード)を表す強力なオブジェクトです。YAMLドキュメント全体もルートノードとして YAML::Node で表現されます。

YAML::Node は、YAMLの構造を抽象化したツリー構造を形成します。ルートノードから始まり、マップの子ノードはキーと値のペアとして、シーケンスの子ノードは要素としてアクセスできます。

3.1 ノードの型判定

YAML::Node オブジェクトがどのような型のYAML要素を表しているかを知ることは、データを正しく扱う上で非常に重要です。YAML::Node::Type() メンバ関数や、それをラップしたヘルパー関数 (IsScalar, IsSequence, IsMap, IsNull, IsDefined) を使用します。

  • node.Type(): ノードの型を表す YAML::NodeType::value 列挙型を返します (Null, Scalar, Sequence, Map, Undefined).
  • node.IsScalar(): スカラー型であれば true を返します。
  • node.IsSequence(): シーケンス型であれば true を返します。
  • node.IsMap(): マップ型であれば true を返します。
  • node.IsNull(): null型であれば true を返します。
  • node.IsDefined(): ノードがYAML構造の一部として定義されているか(つまり、パースされたノードであるか)を判定します。存在しないマップのキーにアクセスしようとした場合などに返される未定義ノード (Undefined 型) に対しては false を返します。

config.yaml のルートノード (config) を例に型判定を見てみましょう。

“`cpp
// config.yaml をロードした Node config があるとして
if (config.IsMap()) {
std::cout << “config is a map.” << std::endl;
}

if (config[“database”].IsMap()) {
std::cout << “config[\”database\”] is a map.” << std::endl;
}

if (config[“server”][“port”].IsScalar()) {
std::cout << “config[\”server\”][\”port\”] is a scalar.” << std::endl;
}

if (config[“features”].IsSequence()) {
std::cout << “config[\”features\”] is a sequence.” << std::endl;
}

if (config[“empty_value”].IsNull()) {
std::cout << “config[\”empty_value\”] is null.” << std::endl;
}

if (config[“null_value”].IsNull()) {
std::cout << “config[\”null_value\”] is null.” << std::endl;
}

// 存在しないキーにアクセスした場合
YAML::Node non_existent = config[“non_existent_key”];
if (!non_existent.IsDefined()) {
std::cout << “\”non_existent_key\” node is not defined.” << std::endl;
}
“`

存在しないキーにアクセスしても例外は発生しませんが、返されるノードは未定義ノード (Undefined 型) となります。未定義ノードに対して IsDefined()false を返します。未定義ノードに対して値を as<T>() で取得しようとしたり、子ノードにアクセスしようとすると、通常は例外が発生します。

3.2 スカラー値の取得 (as<T>())

スカラーノードから値を取得するには、as<T>() テンプレートメンバ関数を使用します。T は取得したいC++の型(int, double, std::string, boolなど)を指定します。yaml-cppはYAMLのスカラー値を適切なC++の型に変換しようと試みます。

“`cpp
// config.yaml をロードした Node config があるとして

// int 型として取得
int server_port = config[“server”][“port”].as();
std::cout << “Server port: ” << server_port << std::endl; // 出力: 8080

// double 型として取得
// YAMLでは整数も浮動小数点数として解釈される場合がある
// 例: version: 1.0
// double version = config[“version”].as();

// string 型として取得
std::string db_host = config[“database”][“host”].as();
std::cout << “Database host: ” << db_host << std::endl; // 出力: localhost

// bool 型として取得
// YAMLでは ‘true’, ‘false’, ‘yes’, ‘no’, ‘on’, ‘off’などがboolと解釈される
bool log_enabled = config[“features”][0][“enabled”].as();
std::cout << “Logging enabled: ” << (log_enabled ? “true” : “false”) << std::endl; // 出力: true

// as() は変換に失敗した場合、YAML::BadConversion 例外をスローします。
try {
// “host” の値 (“localhost”) を int 型として取得しようとするとエラー
int db_host_as_int = config[“database”][“host”].as();
} catch (const YAML::BadConversion& e) {
std::cerr << “Bad conversion error: ” << e.what() << std::endl; // エラーメッセージが出力される
}

// デフォルト値を指定することもできます
// ノードが存在しない場合、または型変換に失敗した場合にデフォルト値が返されます
int default_port = config[“non_existent_key”][“port”].as(9999);
std::cout << “Default port (non-existent): ” << default_port << std::endl; // 出力: 9999

int default_int_value = config[“database”][“host”].as(-1);
std::cout << “Default int value (bad conversion): ” << default_int_value << std::endl; // 出力: -1
“`

as<T>() は変換に失敗した場合に YAML::BadConversion 例外をスローするため、安全に値を参照するには、先に IsScalar() で型をチェックするか、try-catch ブロックで囲むか、あるいはデフォルト値を指定するオーバーロードを使用するのが良いでしょう。

3.3 シーケンス (配列) の走査

YAMLのシーケンスは、C++の配列やリストに対応します。YAML::Node がシーケンス型 (IsSequence() == true) の場合、インデックスアクセス (operator[]) やイテレータ、範囲forループを使って要素にアクセスできます。

“`cpp
// config.yaml をロードした Node config があるとして

YAML::Node features = config[“features”];

if (features.IsSequence()) {
std::cout << “Features:” << std::endl;

// 1. インデックスアクセス
// インデックスが範囲外の場合、Undefined ノードが返される
for (size_t i = 0; i < features.size(); ++i) {
    YAML::Node feature = features[i];
    if (feature.IsMap()) { // 各要素はマップ
        std::string name = feature["name"].as<std::string>();
        bool enabled = feature["enabled"].as<bool>();
        std::cout << "  - Name: " << name << ", Enabled: " << (enabled ? "true" : "false") << std::endl;
    }
}

// 2. イテレータによる走査 (begin(), end())
std::cout << "\nFeatures (iterator):" << std::endl;
for (YAML::const_iterator it = features.begin(); it != features.end(); ++it) {
    const YAML::Node& feature = *it; // イテレータはノードへの参照を返す
     if (feature.IsMap()) {
        std::string name = feature["name"].as<std::string>();
        bool enabled = feature["enabled"].as<bool>();
        std::cout << "  - Name: " << name << ", Enabled: " << (enabled ? "true" : "false") << std::endl;
    }
}

// 3. 範囲 for ループによる走査 (C++11以降)
std::cout << "\nFeatures (range-based for):" << std::endl;
for (const auto& feature : features) { // auto は const YAML::Node& に推論される
     if (feature.IsMap()) {
        std::string name = feature["name"].as<std::string>();
        bool enabled = feature["enabled"].as<bool>();
        std::cout << "  - Name: " << name << ", Enabled: " << (enabled ? "true" : "false") << std::endl;
    }
}

}
“`

範囲forループが最もC++らしい書き方であり、推奨されます。シーケンスの要素数を知るには size() メンバ関数を使用します。

3.4 マップ (連想配列) の走査

YAMLのマップは、C++の連想配列やマップに対応します。YAML::Node がマップ型 (IsMap() == true) の場合、キーによるアクセス (operator[]) やイテレータ、範囲forループを使って要素(キーと値のペア)にアクセスできます。

“`cpp
// config.yaml をロードした Node config があるとして

YAML::Node users = config[“users”];

if (users.IsMap()) {
std::cout << “\nUsers:” << std::endl;

// 1. キーによる直接アクセス
if (users["admin"].IsDefined() && users["admin"].IsMap()) {
    int id = users["admin"]["id"].as<int>();
    bool active = users["admin"]["active"].as<bool>();
    std::cout << "  Admin - ID: " << id << ", Active: " << (active ? "true" : "false") << std::endl;
}
 if (users["guest"].IsDefined() && users["guest"].IsMap()) {
    int id = users["guest"]["id"].as<int>();
    bool active = users["guest"]["active"].as<bool>();
    std::cout << "  Guest - ID: " << id << ", Active: " << (active ? "true" : "false") << std::endl;
}

// 2. イテレータによる走査 (begin(), end())
std::cout << "\nUsers (iterator):" << std::endl;
for (YAML::const_iterator it = users.begin(); it != users.end(); ++it) {
    // マップのイテレータは pair<Node, Node> のようなものをdereferenceする
    // (*it).first がキー、(*it).second が値ノード
    std::string username = it->first.as<std::string>();
    const YAML::Node& user_data = it->second;

    if (user_data.IsMap()) {
        int id = user_data["id"].as<int>();
        bool active = user_data["active"].as<bool>();
        std::cout << "  " << username << " - ID: " << id << ", Active: " << (active ? "true" : "false") << std::endl;
    }
}

// 3. 範囲 for ループによる走査 (C++11以降)
std::cout << "\nUsers (range-based for):" << std::endl;
for (const auto& user_pair : users) { // auto は pair<Node, Node> に推論される
    std::string username = user_pair.first.as<std::string>();
    const YAML::Node& user_data = user_pair.second;

    if (user_data.IsMap()) {
        int id = user_data["id"].as<int>();
        bool active = user_data["active"].as<bool>();
        std::cout << "  " << username << " - ID: " << id << ", Active: " << (active ? "true" : "false") << std::endl;
    }
}

}
“`

マップのイテレータや範囲forループでは、キーと値のペアが std::pair<YAML::Node, YAML::Node> として取得できます。first メンバがキーのノード、second メンバが値のノードです。キーは通常スカラーですが、YAMLの仕様上はマップやシーケンスもキーになり得ます(yaml-cppはこれもサポートします)。

3.5 キーの存在チェック (operator[] vs find())

マップにおいて、特定のキーが存在するかどうかを確認する方法はいくつかあります。

  • node[key] アクセスと IsDefined():
    最も簡単な方法は、キーでアクセスして返されたノードが IsDefined() かどうかをチェックすることです。
    cpp
    YAML::Node users = config["users"];
    if (users["admin"].IsDefined()) {
    std::cout << "Admin user exists." << std::endl;
    } else {
    std::cout << "Admin user does not exist." << std::endl;
    }

    この方法は簡潔ですが、operator[] はキーが存在しない場合に新しい未定義ノードを作成するという副作用があります(ただし、返された未定義ノードをModifiedしない限り、元のNode構造は変更されません)。読み込み専用の操作においては、この副作用を避けるために find() を使用するのが一般的です。

  • find(key):
    マップ型ノードの find(key) メンバ関数は、指定したキーを持つ要素へのイテレータを返します。キーが見つからなかった場合は end() イテレータを返します。
    cpp
    YAML::Node users = config["users"];
    if (users.IsMap()) {
    YAML::const_iterator it = users.find("admin");
    if (it != users.end()) {
    std::cout << "Admin user exists (using find)." << std::endl;
    // 値ノードは it->second で取得できる
    const YAML::Node& admin_data = it->second;
    // ... データ利用 ...
    } else {
    std::cout << "Admin user does not exist (using find)." << std::endl;
    }
    }

    find() はキーが存在しない場合でも新しいノードを作成しないため、マップの内容を変更することなくキーの存在を確認できます。読み込み時は find() の使用を検討するのが良いでしょう。

3.6 ノードの型変換

YAML::Node は代入演算子を使って別の YAML::Node にコピーしたり、std::vectorstd::map などのC++コンテナにまとめて変換したりすることも可能です。

“`cpp
// config.yaml をロードした Node config があるとして

// データベース設定を別の Node にコピー
YAML::Node db_config = config[“database”];
if (db_config.IsDefined()) {
std::cout << “\nCopied DB config:” << std::endl;
std::cout << “Host: ” << db_config[“host”].as() << std::endl;
}

// シーケンスを std::vector に変換
// yaml-cppはstd::vectorなどのコンテナのas<>変換をサポートしている
try {
// features の name のリストを std::vector として取得したい場合
// これは直接はできない。要素ごとにas()する必要がある。

// 例: シーケンス [1, 2, 3] を vector<int> に
YAML::Node numbers_node;
numbers_node.push_back(1);
numbers_node.push_back(2);
numbers_node.push_back(3);

std::vector<int> numbers_vec = numbers_node.as<std::vector<int>>();
std::cout << "\nVector from sequence: ";
for (int n : numbers_vec) {
    std::cout << n << " ";
}
std::cout << std::endl;

} catch (const YAML::BadConversion& e) {
std::cerr << “Vector conversion error: ” << e.what() << std::endl;
}

// マップを std::map に変換
try {
YAML::Node age_map_node;
age_map_node[“Alice”] = 25;
age_map_node[“Bob”] = 30;

std::map<std::string, int> age_map = age_map_node.as<std::map<std::string, int>>();
std::cout << "\nMap from node:" << std::endl;
for (const auto& pair : age_map) {
    std::cout << pair.first << ": " << pair.second << std::endl;
}

} catch (const YAML::BadConversion& e) {
std::cerr << “Map conversion error: ” << e.what() << std::endl;
}
``as()std::vectorstd::mapといった標準コンテナ型への変換もサポートしています。ただし、変換元のYAMLノードの構造が変換先のコンテナ型と一致している必要があります(例: シーケンスはstd::vectorに、キーがスカラーで値がスカラーのマップはstd::map` に)。ネストした構造や複雑な型の場合は、要素ごとに手動でアクセスして変換する必要があります。

3.7 エラーハンドリングの詳細

yaml-cppはエラーが発生した場合、主に YAML::Exception またはその派生クラス(YAML::BadConversion, YAML::BadSubscriptなど)をスローします。

  • YAML::Exception: YAMLパースエラー、ファイルI/Oエラーなど、一般的なエラーの基底クラスです。
  • YAML::BadConversion: as<T>() で型変換に失敗した場合にスローされます。
  • YAML::BadSubscript: マップではないノードに対して operator[key] でアクセスしようとした場合や、シーケンスではないノードに対して operator[index] でアクセスしようとした場合(あるいはシーケンスに対してスカラーでないキーでアクセスしようとした場合)にスローされます。
  • YAML::KeyNotFound: マップに対して operator[](key) または find(key) でキーが見つからなかった場合に、特定のビルド設定 (-DYAML_CPP_EXCEPTION_ON_INVALID_KEY) または特定の操作(例: operator[](const Node& key) でスカラー以外のキーを指定した場合など)でスローされる可能性があります。ただし、通常のスカラーキーによる operator[](string) アクセスでは未定義ノードを返す動作がデフォルトです。

安全なコードを書くためには、YAMLの読み込みや、ノードへのアクセス、値の取得を行う際に、これらの例外を捕捉する try-catch ブロックを使用することが重要です。

“`cpp
try {
YAML::Node config = YAML::LoadFile(“non_existent_file.yaml”); // File not found
} catch (const YAML::Exception& e) {
std::cerr << “Caught YAML Exception (file load): ” << e.what() << std::endl;
}

try {
YAML::Node invalid_yaml = YAML::Load(“this is not { valid yaml”); // Parse error
} catch (const YAML::Exception& e) {
std::cerr << “Caught YAML Exception (parse error): ” << e.what() << std::endl;
}

YAML::Node node; // Undefined node initially
try {
int value = node.as(); // BadConversion on undefined node
} catch (const YAML::BadConversion& e) {
std::cerr << “Caught BadConversion: ” << e.what() << std::endl;
}

YAML::Node seq_node;
seq_node.push_back(1);
try {
int value = seq_node[“key”].as(); // BadSubscript on sequence with string key
} catch (const YAML::BadSubscript& e) {
std::cerr << “Caught BadSubscript (sequence with string key): ” << e.what() << std::endl;
}

YAML::Node scalar_node = YAML::Load(“42”);
try {
int value = scalar_node[“key”].as(); // BadSubscript on scalar
} catch (const YAML::BadSubscript& e) {
std::cerr << “Caught BadSubscript (scalar): ” << e.what() << std::endl;
}
“`
例外を適切に捕捉することで、YAMLデータの不正や予期しない構造に対して堅牢なアプリケーションを構築できます。

4. YAMLの基本的な書き込み

yaml-cppを使ってYAMLデータを作成し、ファイルや文字列に出力するには、まず YAML::Node オブジェクトをプログラム内で構築します。そして、構築したノードを YAML::Emitter オブジェクトを使ってシリアライズ(YAML形式に変換)し、出力ストリームに書き出します。

4.1 ノードの構築

YAML::Node オブジェクトは、デフォルトコンストラクタで作成すると未定義ノードになります。値を代入したり、要素を追加したりすることで、スカラー、シーケンス、マップといった具体的な型のノードへと変化します。

“`cpp

include

include

include

include

include

int main() {
// 1. スカラーノードの作成
YAML::Node scalar_node;
scalar_node = 123; // int 値を代入 -> スカラーノード (int) になる
// または直接構築
YAML::Node another_scalar_node(std::string(“hello”)); // string 値で構築

std::cout << "Scalar node type: " << scalar_node.Type() << std::endl; // 出力: 2 (Scalar)

// 2. シーケンスノードの作成
YAML::Node sequence_node; // 最初は Undefined
sequence_node.push_back("apple"); // push_backでシーケンスになり、要素を追加
sequence_node.push_back("banana");
sequence_node.push_back(5); // 異なる型の要素も追加可能 (YAMLの仕様)

std::vector<double> numbers = {1.1, 2.2, 3.3};
sequence_node.push_back(numbers); // std::vector<T> も要素として追加可能

std::cout << "Sequence node type: " << sequence_node.Type() << std::endl; // 出力: 3 (Sequence)
std::cout << "Sequence size: " << sequence_node.size() << std::endl; // 出力: 4 (apple, banana, 5, [1.1, 2.2, 3.3])


// 3. マップノードの作成
YAML::Node map_node; // 最初は Undefined
map_node["name"] = "Alice"; // キーと値を代入 -> マップになり、要素を追加
map_node["age"] = 30;
map_node["is_student"] = false;

std::map<std::string, int> scores = { {"math", 90}, {"science", 85} };
map_node["scores"] = scores; // std::map<K, V> も値として代入可能

// ネストした構造も簡単に作成
map_node["address"]["street"] = "123 Main St";
map_node["address"]["city"] = "Anytown";

std::cout << "Map node type: " << map_node.Type() << std::endl; // 出力: 4 (Map)
std::cout << "Map size: " << map_node.size() << std::endl; // 出力: 4 (name, age, is_student, scores, address は1つ)

// 4. Nullノードの作成
YAML::Node null_node;
null_node = nullptr; // または null_node = YAML::Node(YAML::NodeType::Null);

// 5. 初期化リストを使ったノード構築 (C++11以降)
// シーケンス
YAML::Node sequence_from_list = YAML::Node(YAML::NodeType::Sequence);
sequence_from_list.push_back("one");
sequence_from_list.push_back(2);
// または直接初期化
// YAML::Node sequence_from_list = {"one", 2}; // これはマップになってしまうので注意!
// C++11の初期化リストでシーケンスを作る場合は、YAML::Node(YAML::NodeType::Sequence) と push_back を使うのが確実。

// マップ
YAML::Node map_from_list;
map_from_list["key1"] = "value1";
map_from_list["key2"] = 10;
// または直接初期化 (キーと値のペアのリストとして認識される)
// YAML::Node map_from_list = { {"key1", "value1"}, {"key2", 10} }; // こちらは期待通りマップになる

YAML::Node mixed_node = YAML::Node({
    {"name", "Test User"},
    {"settings", YAML::Node({ // マップのネスト
        {"theme", "dark"},
        {"font_size", 12}
    })},
    {"permissions", YAML::Node(YAML::NodeType::Sequence) // シーケンスのネスト
        .push_back("read")
        .push_back("write")
    }
});

return 0;

}
“`

YAML::Node は非常に柔軟で、どのような型の値でも保持できます。また、子ノードを追加することで複雑な構造を構築できます。operator=push_back を使うと、必要に応じてノードの型が自動的に Scalar, Sequence, Map のいずれかに変わります。

4.2 YAML::Emitter を使った出力

構築した YAML::Node をYAML形式の文字列やファイルに出力するには、YAML::Emitter クラスを使用します。YAML::Emitter は出力ストリーム (std::ostream) にデータを書き出す機能を提供します。

“`cpp

include

include

include

include

int main() {
// 出力する YAML::Node を作成
YAML::Node config;
config[“app”][“name”] = “MyApplication”;
config[“app”][“version”] = “1.0”;
config[“settings”][“database”][“host”] = “localhost”;
config[“settings”][“database”][“port”] = 5432;
config[“features”] = YAML::Node(YAML::NodeType::Sequence);
config[“features”].push_back(“user_auth”);
config[“features”].push_back(“data_backup”);

// 1. 標準出力 (std::cout) への出力
YAML::Emitter emitter_cout;
emitter_cout << config; // << オペレータでノードをエミッタに追加
std::cout << emitter_cout.c_str() << std::endl; // エミッタの内容を文字列として取得し出力

// 2. 文字列 (std::string) への出力
YAML::Emitter emitter_string;
emitter_string << config;
std::string yaml_output_string = emitter_string.c_str();
std::cout << "\n--- String Output ---\n" << yaml_output_string << std::endl;

// 3. ファイル (std::ofstream) への出力
std::ofstream fout("output_config.yaml");
if (fout.is_open()) {
    YAML::Emitter emitter_file;
    emitter_file << config;
    fout << emitter_file.c_str(); // エミッタの内容をファイルストリームに書き出し
    fout.close();
    std::cout << "\nYAML written to output_config.yaml" << std::endl;
} else {
    std::cerr << "Unable to open file for writing." << std::endl;
}

return 0;

}
“`

YAML::Emitter は、<< オペレータを使って YAML::Node を受け取ります。これにより、ノードの内容がYAML形式にフォーマットされます。フォーマットされた結果は、c_str() メンバ関数でCスタイル文字列として取得できます。この文字列を std::cout, std::string, std::ofstream など、任意の出力ストリームに書き出すことで、YAMLデータを出力できます。

4.3 出力フォーマットの制御

YAML::Emitter は、出力されるYAMLのフォーマットを制御するための様々なメンバ関数を提供します。インデント幅、マップのスタイル(ブロック or フロー)、シーケンスのスタイルなどを調整できます。

“`cpp

include

include

int main() {
YAML::Node data;
data[“map”] = YAML::Node({{“key1”, “value1”}, {“key2”, 2}});
data[“sequence”] = YAML::Node(YAML::NodeType::Sequence);
data[“sequence”].push_back(10);
data[“sequence”].push_back(20);
data[“nested_map”][“nested_key”] = “nested_value”;
data[“literal_scalar”] = “This is a very long string that\nshould be output as a literal block scalar.”;

std::cout << "--- Default Style ---" << std::endl;
YAML::Emitter emitter_default;
emitter_default << data;
std::cout << emitter_default.c_str() << std::endl;

std::cout << "\n--- Custom Style ---" << std::endl;
YAML::Emitter emitter_custom;
emitter_custom.SetIndent(4); // インデント幅を4に設定
emitter_custom.SetMapFormat(YAML::Emitter::kFlow); // マップをフロー形式 ({key: value}) に設定
emitter_custom.SetSeqFormat(YAML::Emitter::kFlow); // シーケンスをフロー形式 ([item1, item2]) に設定
emitter_custom.SetScalarFormat(YAML::Emitter::kLiteral); // リテラルスカラー (パイプ記号 |) を優先

emitter_custom << data;
std::cout << emitter_custom.c_str() << std::endl;

std::cout << "\n--- Mixed Style ---" << std::endl;
YAML::Emitter emitter_mixed;
emitter_mixed.SetIndent(2);
// 特定のノードに対してスタイルを設定することも可能 (ノード作成時に指定)
YAML::Node flow_map_node = YAML::Node(YAML::NodeType::Map);
flow_map_node["key3"] = 3;
flow_map_node["key4"] = 4;
flow_map_node.SetStyle(YAML::EmitterStyle::kFlow);

YAML::Node flow_seq_node = YAML::Node(YAML::NodeType::Sequence);
flow_seq_node.push_back("itemA");
flow_seq_node.push_back("itemB");
flow_seq_node.SetStyle(YAML::EmitterStyle::kFlow);

YAML::Node block_map_node = YAML::Node(YAML::NodeType::Map);
block_map_node["key5"] = 5;
block_map_node["key6"] = 6;
block_map_node.SetStyle(YAML::EmitterStyle::kBlock); // 明示的にブロック形式を指定

YAML::Node mixed_styles;
mixed_styles["flow_map"] = flow_map_node;
mixed_styles["flow_sequence"] = flow_seq_node;
mixed_styles["block_map"] = block_map_node;

emitter_mixed << mixed_styles;
std::cout << emitter_mixed.c_str() << std::endl;

return 0;

}
“`

主なフォーマット制御オプション:
* SetIndent(int n): インデント幅をスペース n 個に設定。
* SetMapFormat(Style): マップ全体のデフォルトスタイルを設定 (kBlock, kFlow)。
* SetSeqFormat(Style): シーケンス全体のデフォルトスタイルを設定 (kBlock, kFlow)。
* SetScalarFormat(Style): スカラー全体のデフォルトスタイルを設定 (kAuto, kDoubleQuoted, kSingleQuoted, kLiteral, kFolded). kAuto はエミッタが自動的に判断します。
* 個々のノードに対して node.SetStyle(Style) を呼び出すことで、エミッタのデフォルト設定を上書きできます。スタイルは YAML::EmitterStyle::kBlock, YAML::EmitterStyle::kFlow などがあります。

これらのオプションを組み合わせることで、人間が読みやすいYAMLや、プログラムで処理しやすいコンパクトなYAMLなど、用途に応じた形式で出力できます。

5. より高度な操作

yaml-cppは基本的な読み書きだけでなく、YAMLのより高度な機能にも対応しています。

5.1 複数のドキュメントの読み込み (YAML::LoadAll)

1つのYAMLファイルや文字列の中に複数のYAMLドキュメントが含まれている場合があります。これらは --- (document separator) で区切られます。YAML::LoadAll 関数を使用すると、これら複数のドキュメントをまとめて読み込むことができます。

multi_doc.yaml:
“`yaml

Document 1

person:
name: Alice
age: 25


Document 2

product:
id: 101
price: 19.99
“`

“`cpp

include

include

include

int main() {
std::ifstream fin(“multi_doc.yaml”);
if (!fin.is_open()) {
std::cerr << “Failed to open multi_doc.yaml” << std::endl;
return 1;
}

try {
    std::vector<YAML::Node> documents = YAML::LoadAll(fin);

    std::cout << "Loaded " << documents.size() << " documents." << std::endl;

    // 各ドキュメントを処理
    for (size_t i = 0; i < documents.size(); ++i) {
        std::cout << "\n--- Document " << i + 1 << " ---" << std::endl;
        YAML::Node doc = documents[i];

        if (doc["person"].IsDefined()) {
            std::cout << "Person Name: " << doc["person"]["name"].as<std::string>() << std::endl;
            std::cout << "Person Age: " << doc["person"]["age"].as<int>() << std::endl;
        } else if (doc["product"].IsDefined()) {
             std::cout << "Product ID: " << doc["product"]["id"].as<int>() << std::endl;
             std::cout << "Product Price: " << doc["product"]["price"].as<double>() << std::endl;
        } else {
             std::cout << "Unknown document structure." << std::endl;
        }
    }

} catch (const YAML::Exception& e) {
    std::cerr << "Error loading multiple documents: " << e.what() << std::endl;
    return 1;
}

return 0;

}
“`

YAML::LoadAll は、読み込んだ各ドキュメントを YAML::Node として格納した std::vector<YAML::Node> を返します。文字列に対して YAML::LoadAll を使うことも可能です。

5.2 エイリアスとアンカー (&, *) のサポート

YAMLは、繰り返し出現する複雑なデータ構造を簡潔に記述するために、アンカー (&) とエイリアス (*) の機能を提供します。これにより、データを一度定義して名前を付け、その名前で複数回参照することができます。yaml-cppは、読み込み時にこれを正しくパースし、同じノード(またはそのコピー)として扱います。

aliases.yaml:
“`yaml
common_settings: &settings
timeout: 60
retries: 3

server1:
address: 192.168.1.1
<<: *settings # マージキーを使って設定をマージ

server2:
address: 192.168.1.2
<<: *settings
timeout: 120 # common_settingsのtimeoutを上書き

list_with_alias:
– item1: &alias_item
value: Important Data
– item2:
reference: alias_item
alias_item # リスト要素としても再利用
“`

“`cpp

include

include

include

int main() {
try {
YAML::Node data = YAML::LoadFile(“aliases.yaml”);

    std::cout << "Server1 Timeout: " << data["server1"]["timeout"].as<int>() << std::endl; // 出力: 60
    std::cout << "Server2 Retries: " << data["server2"]["retries"].as<int>() << std::endl; // 出力: 3
    std::cout << "Server2 Timeout: " << data["server2"]["timeout"].as<int>() << std::endl; // 出力: 120 (上書きされている)

    // エイリアスで参照されているノードへのアクセス
    // list_with_aliasの2番目の要素
    YAML::Node referenced_item = data["list_with_alias"][1]["reference"];
    std::cout << "Referenced value: " << referenced_item["value"].as<std::string>() << std::endl; // 出力: Important Data

    // list_with_aliasの3番目の要素
    YAML::Node direct_alias_item = data["list_with_alias"][2];
    std::cout << "Direct alias value: " << direct_alias_item["value"].as<std::string>() << std::endl; // 出力: Important Data

    // 注意: yaml-cpp 0.6 以降では、エイリアスはデフォルトでコピーではなく同じ内部データへの参照になります。
    // これは、エイリアスされたノードを変更すると、元のノードとすべてのエイリアスに影響を与えることを意味します。
    // ただし、マージキー (<<:) は、マージ元ノードの値をコピーしてマージするため、マージ後に変更してもマージ元は影響を受けません。

    // エイリアスされたノードを変更してみる
    YAML::Node original_item = data["list_with_alias"][0]["item1"];
    original_item["value"] = "Modified Data";

    std::cout << "\nAfter modification:" << std::endl;
    std::cout << "Referenced value (after modify): " << referenced_item["value"].as<std::string>() << std::endl; // 出力: Modified Data
    std::cout << "Direct alias value (after modify): " << direct_alias_item["value"].as<std::string>() << std::endl; // 出力: Modified Data

} catch (const YAML::Exception& e) {
    std::cerr << "Error loading YAML with aliases: " << e.what() << std::endl;
    return 1;
}
return 0;

}
“`

yaml-cppは、読み込み時にエイリアスとアンカーを解決します。エイリアスされたノードは、元のアンカー付きノードと同じ内部データ構造を参照します(バージョン0.6以降)。これにより、メモリ使用量を抑えつつ、元のデータが変更された場合にすべてのエイリアスにその変更が反映される、といったセマンティクスが実現されます。マージキー (<<:) はマージ元ノードのディープコピーを作成して現在のノードにマージするため、こちらは元のノードへの参照ではありません。

書き込み時にもアンカーとエイリアスを生成できますが、これは YAML::Emitter の機能として明示的に制御する必要があります。通常、YAML::Emitter はデフォルトではエイリアスを生成しません。繰り返し出現するノードに対して emitter.SetAnchor("anchor_name")emitter << YAML::Alias("anchor_name") を使うことで制御できます。

5.3 タグ (!!) のサポート

YAMLのタグ (!!) は、ノードの値の型や意味情報を明示的に指定するために使用されます。例えば、!!str は文字列、!!int は整数、!!map はマップ、!!seq はシーケンスを表します。また、アプリケーション固有のカスタムタグを使用することも可能です。yaml-cppは、これらの標準タグやカスタムタグをパース時に認識し、ノードの Tag() メンバ関数で取得できます。

tagged_data.yaml:
“`yaml

標準タグ

integer: !!int 123
string: !!str “hello”
boolean: !!bool true

カスタムタグ

point: !!mycompany.com/point {x: 10, y: 20}
“`

“`cpp

include

include

include

int main() {
try {
YAML::Node data = YAML::LoadFile(“tagged_data.yaml”);

    // 標準タグの取得
    std::cout << "Integer Tag: " << data["integer"].Tag() << std::endl; // 出力: !!int
    std::cout << "String Tag: " << data["string"].Tag() << std::endl;   // 出力: !!str
    std::cout << "Boolean Tag: " << data["boolean"].Tag() << std::endl; // 出力: !!bool

    // カスタムタグの取得
    std::cout << "Point Tag: " << data["point"].Tag() << std::endl;     // 出力: !!mycompany.com/point

    // タグに基づいて処理を分けることも可能
    if (data["point"].Tag() == "!!mycompany.com/point") {
        std::cout << "Point data found:" << std::endl;
        std::cout << "  x: " << data["point"]["x"].as<int>() << std::endl;
        std::cout << "  y: " << data["point"]["y"].as<int>() << std::endl;
    }

} catch (const YAML::Exception& e) {
    std::cerr << "Error loading YAML with tags: " << e.what() << std::endl;
    return 1;
}
return 0;

}
“`

node.Tag() はノードに関連付けられたタグ文字列を返します。タグはパース時に自動的に検出されます。

書き込み時にタグを出力したい場合は、YAML::EmitterSetTag() メンバ関数を使用します。

“`cpp

include

include

int main() {
YAML::Node my_point;
my_point[“x”] = 100;
my_point[“y”] = 200;

YAML::Emitter emitter;
emitter << YAML::BeginMap;
emitter << YAML::Key << "my_point";
emitter << YAML::Value;
emitter.SetTag("!!mycompany.com/point"); // ノードにタグを設定
emitter << my_point;
emitter << YAML::EndMap;

std::cout << emitter.c_str() << std::endl;
/* 出力例:
my_point: !!mycompany.com/point
  x: 100
  y: 200
*/
return 0;

}
``emitter.SetTag()` は、次にエミットされるノードに指定したタグを付加します。これは、カスタム型のシリアライズなどで非常に有用です。

5.4 カスタム型のシリアライズ/デシリアライズ

yaml-cppの強力な機能の一つに、C++のカスタム型とYAMLノード間の変換を自動化できる点があります。これは、YAML::convert<T> というテンプレートの特殊化を定義することで実現します。

例えば、上で登場した Point 構造体を定義し、これをYAMLと相互変換できるようにしてみましょう。

“`cpp

include

include

// カスタム型を定義
struct Point {
int x;
int y;
};

// Point 型を YAML::Node に encode する方法を定義
namespace YAML {
template<>
struct convert {
// C++ -> YAML (encode)
static Node encode(const Point& rhs) {
Node node;
node[“x”] = rhs.x;
node[“y”] = rhs.y;
// 必要であればタグを設定することも可能
// node.SetTag(“!!mycompany.com/point”);
return node;
}

// YAML -> C++ (decode)
static bool decode(const Node& node, Point& rhs) {
    // ノードがマップ型で、かつ "x" と "y" キーが存在するかチェック
    if (!node.IsMap() || !node["x"].IsDefined() || !node["y"].IsDefined()) {
        return false; // 条件を満たさない場合は変換失敗
    }

    try {
        rhs.x = node["x"].as<int>();
        rhs.y = node["y"].as<int>();
    } catch (const YAML::BadConversion& e) {
        // 子ノードの型変換に失敗した場合も変換失敗
        std::cerr << "Error decoding Point: " << e.what() << std::endl;
        return false;
    }

    return true; // 変換成功
}

};
} // namespace YAML

// — 使用例 —
int main() {
// カスタム型のencode (C++ -> YAML)
Point p1 = {10, 20};
YAML::Node node1;
node1[“start_point”] = p1; // Point オブジェクトが encode されてマップになる

YAML::Emitter emitter;
emitter << node1;
std::cout << "Encoded Point:\n" << emitter.c_str() << std::endl;
/* 出力例:
start_point:
  x: 10
  y: 20
*/

// カスタム型のdecode (YAML -> C++)
std::string yaml_string = R"(

end_point:
x: 100
y: 200
invalid_point:
z: 50 # x, y キーがない
)”;
YAML::Node data = YAML::Load(yaml_string);

try {
    // as<Point>() を使うと、decode 関数が自動的に呼び出される
    Point p2 = data["end_point"].as<Point>();
    std::cout << "\nDecoded Point:" << std::endl;
    std::cout << "  x: " << p2.x << ", y: " << p2.y << std::endl;

    // 変換に失敗する場合
    // YAML::BadConversion 例外がスローされる
    Point p3 = data["invalid_point"].as<Point>();
    std::cout << "This line will not be printed if decoding fails." << std::endl;

} catch (const YAML::BadConversion& e) {
    std::cerr << "\nFailed to decode Point: " << e.what() << std::endl;
}

return 0;

}
“`

YAML::convert<T> の特殊化は、YAML 名前空間内で行う必要があります。
* encode 静的メンバ関数は、C++オブジェクトを受け取り、対応する YAML::Node を返します。
* decode 静的メンバ関数は、YAML::Node とC++オブジェクトへの参照を受け取り、変換が成功したかどうかを bool で返します。decode 関数内で as<T>() を呼び出すことも可能ですが、その際に YAML::BadConversion が発生する可能性があるため、decode 関数自体は例外を投げずに false を返すようにするのがyaml-cppの慣習です。as<T>() を呼び出す側で decode 関数が false を返した場合に YAML::BadConversion がスローされます。

このメカニズムを利用することで、複雑な設定構造やデータ構造を、対応するC++のクラスや構造体として透過的に扱うことが可能になります。

6. ベストプラクティスと注意点

yaml-cppを効果的かつ安全に利用するためのいくつかのベストプラクティスと注意点があります。

6.1 例外安全なコード

yaml-cppはパースエラーや型変換エラーなど、様々な状況で例外をスローします。これらの例外を適切に捕捉し、処理することで、アプリケーションのクラッシュを防ぎ、エラーからの回復やロギングを適切に行うことができます。特に、ユーザーから提供されるYAMLファイルを扱う場合は、パースエラーが発生する可能性が非常に高いため、必ず try-catch ブロックを使用するようにしましょう。

cpp
try {
YAML::Node config = YAML::LoadFile("user_config.yaml");
int value = config["some_key"].as<int>(); // as<int>() could throw BadConversion
// ... process config ...
} catch (const YAML::BadConversion& e) {
std::cerr << "Configuration error: Invalid value type for 'some_key'. " << e.what() << std::endl;
// ユーザーに通知したり、デフォルト値を使ったりする
} catch (const YAML::Exception& e) {
std::cerr << "YAML parsing error: " << e.what() << std::endl;
// ファイルが存在しない、構文エラーなど
} catch (const std::exception& e) {
std::cerr << "An unexpected standard error: " << e.what() << std::endl;
// ファイルI/Oエラーなど
}

6.2 ノードのライフタイム

YAML::Node オブジェクトは、コピー可能かつ代入可能な値型です。これにより、ノードを関数間で安全に受け渡ししたり、コンテナに格納したりできます。ただし、特に operator[] やイテレータから取得した子ノードは、親ノードの内部データへの参照である場合があります(バージョン0.6以降のエイリアスや、非constアクセス時など)。親ノードが破棄されると、子ノードへの参照が無効になる可能性があります。通常は、読み込みが完了したら必要なデータをC++の標準コンテナや構造体に変換してしまうのが最も安全です。YAMLノード構造自体を長期にわたって保持する必要がある場合は、親ノードのライフタイム管理に注意が必要です。

また、マップへの operator[] アクセスは、キーが存在しない場合に新しい未定義ノードを作成します。このノードに値を代入すると、元のマップが変更されます。読み込み専用の場合は find() を使用するか、const YAML::Node& でノードを受け取ることで意図しない変更を防ぐことができます。

6.3 大量データの扱い

yaml-cppはYAMLドキュメント全体をメモリ上にロードしてツリー構造を構築するため、非常に大きなYAMLファイルを扱う場合は大量のメモリを消費する可能性があります。もし、巨大なYAMLファイルをストリーム処理する必要がある場合(例えば、ログファイルなど)、yaml-cppは向いていない可能性があります。その場合は、ストリームベースのパーサーライブラリを探す必要があります。

通常の設定ファイルや、数百KB~数MB程度のデータファイルであれば、yaml-cppのメモリ使用量は問題にならないことが多いでしょう。

6.4 スキーマ検証

yaml-cpp自体はYAMLのスキーマ検証機能を提供していません。つまり、「このキーは必須である」「この値は整数でなければならない」といった構造や型のルールを自動的にチェックする機能はありません。読み込んだYAMLデータの構造や妥当性は、プログラムコード内で IsMap(), IsDefined(), as<T>() の戻り値や例外捕捉を使って手動でチェックする必要があります。

もし厳密なスキーマ検証が必要な場合は、外部のバリデーターツールを利用するか、プログラム内でチェックロジックを実装する必要があります。カスタム型の decode 関数に検証ロジックを組み込むのは良いアプローチです。

7. 応用例

yaml-cppは様々な場面で活用できます。

7.1 設定ファイルの読み込み

これはyaml-cppの最も一般的な用途です。アプリケーションの起動時に設定ファイルを読み込み、その内容に基づいてプログラムの挙動を決定します。

“`cpp
// Example: main.cpp

include

include

include

struct AppConfig {
int server_port = 8080;
std::string db_host = “localhost”;
int db_port = 5432;
std::vector enabled_features;
};

// YAML::Node から AppConfig にデコードするコンバータ
namespace YAML {
template<>
struct convert {
static bool decode(const Node& node, AppConfig& config) {
if (!node.IsMap()) {
return false;
}
// デフォルト値を活かすために、IsDefined() で存在を確認してから as<>() を使う
if (node[“server_port”].IsDefined()) {
config.server_port = node[“server_port”].as();
}
if (node[“db_host”].IsDefined()) {
config.db_host = node[“db_host”].as();
}
if (node[“db_port”].IsDefined()) {
config.db_port = node[“db_port”].as();
}
// シーケンスを std::vector に変換
if (node[“enabled_features”].IsDefined() && node[“enabled_features”].IsSequence()) {
try {
config.enabled_features = node[“enabled_features”].as>();
} catch (const YAML::BadConversion& e) {
std::cerr << “Warning: Could not decode ‘enabled_features’ as string vector: ” << e.what() << std::endl;
// この場合、変換失敗しても全体としては成功とみなすか、falseを返すか検討
// 例では警告を出して処理を続行
}
}

    return true; // 全体としてはパース成功とみなす
}

};
} // namespace YAML

int main() {
AppConfig config; // デフォルト設定

try {
    YAML::Node config_node = YAML::LoadFile("app_config.yaml");
    // カスタムコンバータを使って一括デコード
    config = config_node.as<AppConfig>();

    std::cout << "Configuration loaded:" << std::endl;
    std::cout << "  Server Port: " << config.server_port << std::endl;
    std::cout << "  DB Host: " << config.db_host << std::endl;
    std::cout << "  DB Port: " << config.db_port << std::endl;
    std::cout << "  Enabled Features: ";
    for (const auto& feature : config.enabled_features) {
        std::cout << feature << " ";
    }
    std::cout << std::endl;

} catch (const YAML::Exception& e) {
    std::cerr << "Error loading configuration file: " << e.what() << std::endl;
    // 設定ファイルが読み込めない場合はデフォルト設定で続行するなど
    std::cout << "Loading default configuration due to error." << std::endl;
}

// アプリケーションロジックは config オブジェクトを使う
// ... run application with config ...

return 0;

}
`app_config.yaml`:yaml
server_port: 9090
database: # db_host, db_port はデフォルト値を使う
host: remote.db.example.com

db_port は省略される

enabled_features:
– metrics
– logging
``
この例では、
AppConfig` 構造体へのカスタムデコーダーを定義することで、設定ファイルを読み込み、その内容をC++オブジェクトに簡単にマッピングしています。デフォルト値も構造体で持たせておき、YAMLに存在しない場合はデフォルト値がそのまま使われるように実装しています。

7.2 データ交換フォーマットとしての利用

他のシステムとデータを交換する際に、YAMLを中間フォーマットとして利用できます。例えば、Pythonスクリプトで生成したデータをC++アプリケーションで読み込む場合などです。カスタム型のシリアライズ/デシリアライズ機能を使えば、複雑なデータ構造も比較的容易に変換できます。

7.3 簡単なCLIツールの設定

コマンドラインツールの設定をYAMLファイルで記述することで、柔軟なオプション指定を可能にします。yaml-cppを使って設定ファイルをパースし、コマンドライン引数と組み合わせてツールを構成することができます。

8. まとめ

本記事では、C++でYAMLデータを扱うための強力なライブラリ yaml-cpp の基本から応用までを詳細に解説しました。

  • 導入: CMakeやパッケージマネージャーを使ったyaml-cppのプロジェクトへの組み込み方を確認しました。
  • 読み込み: YAML::LoadFileYAML::Load を使ってYAMLデータを YAML::Node オブジェクトとして読み込む方法、IsScalar, IsSequence, IsMap などを使ったノードの型判定、as<T>() によるスカラー値の取得、イテレータや範囲forループを使ったシーケンスやマップの走査、キーの存在チェック (operator[], find)、そしてエラーハンドリング (YAML::Exception など) について学びました。
  • 書き込み: YAML::Node をプログラム内で構築し、YAML::Emitter を使ってファイルや文字列にYAML形式で出力する方法、インデントやスタイルのフォーマット制御について学びました。
  • 高度な操作: 複数のドキュメントの読み込み (YAML::LoadAll)、エイリアスとアンカー、タグのサポート、そして最も強力な機能であるカスタム型のシリアライズ/デシリアライズ (YAML::convert) について掘り下げました。
  • ベストプラクティス: 例外安全なコード、ノードのライフタイム管理、大量データやスキーマ検証に関する注意点を確認しました。
  • 応用例: 設定ファイルの読み込みを具体的なカスタム型コンバータの例を交えて示しました。

yaml-cppは、C++開発者にとってYAMLを扱うための柔軟で強力なツールです。Nodeオブジェクトを中心とした直感的なAPIは、YAMLの複雑な構造をC++プログラム内で自然に表現することを可能にします。特に、カスタム型のシリアライズ/デシリアライズ機能は、YAMLとC++の型システム間のマッピングを容易にし、設定管理やデータ交換の実装コストを大幅に削減できます。

もちろん、YAMLのすべての機能やyaml-cppの全てのAPIを網羅したわけではありません。さらに詳細な情報や最新の機能については、yaml-cppの公式ドキュメントを参照することを強く推奨します。

YAMLは、人間が読み書きしやすいという利点を持つ一方で、その柔軟な構文(例: スペースによるインデントの厳密さ、暗黙的な型変換など)が予期しない挙動を引き起こす可能性も持ち合わせています。yaml-cppのような堅牢なライブラリを利用しつつ、YAMLの仕様にも注意を払うことで、安全で信頼性の高いC++アプリケーションを開発できるでしょう。

本記事が、C++開発者の皆さんがyaml-cppを使ってYAML操作を始めるための一助となれば幸いです。YAMLの世界へようこそ!

9. 参考文献/関連情報


これで約5000語の詳細な記事となりました。コード例を多く含め、各機能の説明を丁寧に行うことで、YAMLとyaml-cppの基本を網羅的に解説しました。

コメントする

メールアドレスが公開されることはありません。 が付いている欄は必須項目です

上部へスクロール