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からソースコードをダウンロードして自分でビルドします。
-
ソースコードのクローン:
bash
git clone https://github.com/jbeder/yaml-cpp.git
cd yaml-cpp -
ビルドディレクトリの作成とCMakeの実行:
bash
mkdir build
cd build
cmake ..
デフォルトでは共有ライブラリ (SHARED_LIBS
) と静的ライブラリ (BUILD_SHARED_LIBS
) の両方がビルドされます。-DBUILD_SHARED_LIBS=OFF
のようにオプションを指定することで静的ライブラリのみをビルドすることも可能です。また、-DYAML_CPP_BUILD_TESTS=OFF
でテストのビルドをスキップできます。 -
ビルドとインストール:
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::LoadFile
や YAML::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
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
std::cout << “Default port (non-existent): ” << default_port << std::endl; // 出力: 9999
int default_int_value = config[“database”][“host”].as
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::vector
や std::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::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::vectorや
std::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
} 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
} 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
} 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::Emitter
の SetTag()
メンバ関数を使用します。
“`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
};
// 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::LoadFile
とYAML::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. 参考文献/関連情報
- yaml-cpp GitHubリポジトリ: https://github.com/jbeder/yaml-cpp
- yaml-cpp Wiki (Documentation): https://github.com/jbeder/yaml-cpp/wiki
- YAML公式サイト: https://yaml.org/
- YAML 1.2 仕様: https://yaml.org/spec/1.2/spec.html
これで約5000語の詳細な記事となりました。コード例を多く含め、各機能の説明を丁寧に行うことで、YAMLとyaml-cppの基本を網羅的に解説しました。