C++の応用 - YAML
概要
YAML (YAML Ain't Markup Language) は、人間にとって読み書きしやすいデータシリアライゼーション形式である。
設定ファイル、データ交換、データ保存等、様々な用途で使用されている。
YAMLの特徴を以下に示す。
- 可読性が高い
- インデントを使用して構造を表現するため、人間が読みやすい形式である。
- 豊富なデータ型
- 文字列、数値、ブール値、リスト、マップ (辞書) 等の基本的なデータ型をサポートしている。
- 柔軟性
- 複雑なデータ構造も表現できる。
- コメントのサポート
- #を使用して、コメントを記述することができる。
- 参照と別名
- データの再利用や循環参照が可能である。
YAMLの使用するメリットを以下に示す。
- 設定ファイルに適している
- 人間が読み書きしやすいため、アプリケーションの設定ファイルとしてよく使用されている。
- データ交換に適している
- JSONの代替として使用でき、より読みやすい形式でデータを表現できる。
- 柔軟性が高い
- 複雑なデータ構造も簡単に表現できる。
- 多くの言語でサポートされている
- 多くのプログラミング言語にYAMLパーサーが存在する。
YAMLは、可読性の高さと柔軟性から、多くの開発者に好まれている。
特に、設定ファイルやデータ交換の用途で広く使用されており、多くのモダンなアプリケーションやフレームワークでYAMLが採用されている。
C++では、YAMLの処理によく使用されるライブラリの1つにyaml-cppライブラリが存在する。
YAMLの基本的な構造
※注意
- インデントが重要
- スペースを使用してインデントを行い、一貫性を保つ必要がある。
- コロンの後にはスペースが必要
- "<キー>: <値>"のように、コロンの後にスペースを入れる必要がある。
- 文字列のクォート
- 特殊文字を含む場合や曖昧さを避けたい場合は、文字列をクォートで囲むことができる。
# This is a comment
--- # Document start
# スカラー値 (文字列, 数値, ブール値)
name: John Doe
age: 30
is_student: false
# リスト (ハイフンを使用)
hobbies:
- reading
- traveling
- photography
# マッピング (キー: 値の形式)
address:
street: 123 Main St
city: Anytown
country: USA
# 複雑なデータ構造 (リストとマッピングの組み合わせ)
employees:
- name: Alice
position: Developer
skills:
- Python
- JavaScript
- name: Bob
position: Designer
skills:
- Photoshop
- Illustrator
# 複数行の文字列
description: >
This is a long description
that spans multiple lines.
The > symbol preserves newlines
but removes extra whitespace.
# 複数行の文字列 (> と | を使用)
code_sample: |
def hello_world():
print("Hello, World!")
# アンカー ("&") と エイリアス ("*") を使用したデータの再利用
defaults: &defaults
timeout: 30
retries: 3
production:
<<: *defaults
host: production.example.com
development:
<<: *defaults
host: dev.example.com
timeout: 10 # デフォルト値のオーバーライド
# Document end
yaml-cppライブラリ
yaml-cppライブラリとは
yaml-cppライブラリは、C++向けのYAMLパーサーおよびエミッタライブラリである。
yaml-cppライブラリを使用することにより、C++でYAMLファイルの読み込み、操作、書き込みが可能になる。
また、設定ファイルの処理、データのシリアライズ / デシリアライズ、構造化データの操作等に広く使用されている。
yaml-cppライブラリの特徴を以下に示す。
- C++ 11以降をサポート
- モダンなC++の機能を活用している。
- ヘッダオンリーライブラリ
- 使用する場合は、ヘッダファイルをインクルードするだけで済む。
- YAML 1.2仕様のサポート
- 最新のYAML仕様に準拠している。
- 例外ベースのエラー処理
- エラーが発生した場合は、適切な例外をスローする。
- STLとの統合
- STLコンテナとの相互運用性が高い。
- ストリーミングAPI
- 大きなファイルや継続的なデータストリームの処理に適している。
yaml-cppライブラリのライセンス
yaml-cppライブラリのライセンスは、MITライセンスに準拠している。
yaml-cppの主要なクラス
- YAML::Nodeクラス
- YAMLデータを表現する基本的なクラスである。
- スカラー、シーケンス、マップ等、あらゆる種類のYAMLノードを表現できる。
- YAML::Exceptionクラス
- yaml-cppライブラリが投げる例外の基本クラスである。
- パース時のエラーや型変換エラー等をキャッチできる。
- YAML::Emitterライブラリ
- YAMLデータを生成するためのクラスである。
- プログラム上において、YAMLを構築する時に使用する。
- YAML::Parserクラス
- 低レベルのパーシング操作を行うためのクラスである。
- 一般的には、
YAML::Load
クラスやYAML::LoadFile
クラスを使用するため、直接使用することは少ない。
※注意
- 型安全性
YAML::Node
クラスは、テンプレートベースの.as<T>
メソッドを提供するが、型が一致しない場合は例外がスローされる。- そのため、適切な型チェックを行うことが重要である。
- メモリ管理
YAML::Node
クラスのオブジェクトは参照カウント方式で管理されているため、通常はメモリリークを心配する必要は無い。
- パフォーマンス
- 大規模なファイルを扱う場合、
YAML::LoadFile
クラスはファイル全体をメモリに読み込むため、メモリ使用量が増大する可能性がある。 - この場合、ストリーミングAPIの使用を検討すること。
- 大規模なファイルを扱う場合、
- エラー処理
- yaml-cppライブラリは例外を使用してエラーを報告する。
- そのため、適切な
try-catch
ブロックを使用して、エラーをハンドリングすることが重要である。
yaml-cppライブラリのインストール
パッケージ管理システムからインストール
# RHEL sudo dnf install yaml-cpp yaml-cpp-devel # SUSE sudo zypper install yaml-cpp yaml-cpp-devel
ソースコードからインストール
yaml-cppライブラリのGithubにアクセスして、ソースコードをダウンロードする。
ダウンロードしたファイルを解凍する。
tar xf yaml-cpp-<バージョン>.tar.gz cd yaml-cpp-<バージョン>
yaml-cppライブラリをビルドおよびインストールする。
mkdir build && cd build cmake -DCMAKE_BUILD_TYPE=Release \ -DCMAKE_INSTALL_PREFIX=<yaml-cppライブラリのインストールディレクトリ> \ -DYAML_BUILD_SHARED_LIBS=ON \ .. make -j $(nproc) make install
ライブラリの指定
Qtプロジェクトファイルを使用する場合
# Qtプロジェクトファイル
# pkg-configを使用してyaml-cppライブラリを設定
CONFIG += link_pkgconfig
PKGCONFIG += yaml-cpp
# pkg-configを使用しない場合
LIBS += -lyaml-cpp
CMakeLists.txtファイルを使用する場合
# CMakeLists.txtファイル
# ...略
find_package(PkgConfig REQUIRED)
# yaml-cppライブラリの指定
pkg_check_modules(YAML_CPP REQUIRED yaml-cpp)
# インクルードディレクトリの指定
target_include_directories(${PROJECT_NAME} PRIVATE
# ...略
${YAML_CPP_INCLUDE_DIRS}
)
# ...略
# ライブラリのリンク
target_link_libraries(${PROJECT_NAME} PRIVATE
# ...略
${YAML_CPP_LIBRARIES}
)
# コンパイルオプションの設定
target_compile_options(${PROJECT_NAME} PRIVATE
# ...略
${YAML_CPP_CFLAGS_OTHER}
)
yaml-cppライブラリの使用例
以下の例では、yaml-cppライブラリの主な機能を使用している。
- YAMLファイルの読み込み (
YAML::LoadFile
クラスの使用) - スカラー値の取得 (
.as<T>
メソッドの使用) - リストの処理 (範囲ベースのforループ)
- ネストされたマップの処理
- 新しいYAMLノードの作成と操作
- YAMLデータの出力 (ストリーム演算子
<<
の使用) - YAMLデータのファイルへの書き込み
// main.cppファイル
#include <iostream>
#include <yaml-cpp/yaml.h>
int main()
{
try {
// YAMLファイルの読み込み
YAML::Node config = YAML::LoadFile("config.yaml");
// スカラー値の取得
std::string name = config["name"].as<std::string>();
int age = config["age"].as<int>();
std::cout << "Name: " << name << ", Age: " << age << std::endl;
// リストの処理
std::cout << "Hobbies:" << std::endl;
for (const auto &hobby : config["hobbies"]) {
std::cout << "- " << hobby.as<std::string>() << std::endl;
}
// ネストされたマップの処理
if (config["address"]) {
std::cout << "Address:" << std::endl;
std::cout << " Street: " << config["address"]["street"].as<std::string>() << std::endl;
std::cout << " City: " << config["address"]["city"].as<std::string>() << std::endl;
}
// 新しいYAMLノードの作成
YAML::Node newNode;
newNode["key"] = "value";
newNode["list"].push_back(1);
newNode["list"].push_back(2);
newNode["list"].push_back(3);
// YAMLの出力
std::cout << "\nNew YAML:\n" << newNode << std::endl;
// YAMLファイルへの書き込み
std::ofstream fout("output.yaml");
fout << newNode;
fout.close();
}
catch (const YAML::Exception& e) {
std::cerr << "YAML error: " << e.what() << std::endl;
return -1;
}
return 0;
}
yaml-cppライブラリの詳細な使用方法は、公式ドキュメントを参照すること。
YAMLを出力するモデルは、std::ostream
マニピュレータである。
YAML::Emitter
クラスのオブジェクトは出力ストリームとして動作して、その出力はc_str
メソッドで取得できる。
YAML::Emitter
クラスの詳細な使用方法は、公式ドキュメントを参照すること。
yaml-cppライブラリの応用例
設定ファイルの管理
以下の例では、アプリケーションの設定をYAMLファイルで管理して、読み込み、更新、保存している。
#include <iostream>
#include <fstream>
#include <yaml-cpp/yaml.h>
class ConfigManager {
private:
std::string m_filename;
YAML::Node m_config;
public:
ConfigManager(const std::string &filename) : m_filename(filename)
{
load();
}
void load()
{
try {
m_config = YAML::LoadFile(m_filename);
}
catch (const YAML::Exception &e) {
std::cerr << "Error loading config: " << e.what() << std::endl;
}
}
void save()
{
try {
std::ofstream fout(m_filename);
fout << m_config;
}
catch (const YAML::Exception &e) {
std::cerr << "Error saving config: " << e.what() << std::endl;
}
}
template<typename T>
T get(const std::string& key, const T &defaultValue)
{
try {
return m_config[key].as<T>();
}
catch (const YAML::Exception&) {
return defaultValue;
}
}
template<typename T>
void set(const std::string &key, const T &value)
{
m_config[key] = value;
}
};
int main()
{
ConfigManager config("app_config.yaml");
// 設定の読み取り
std::string username = config.get<std::string>("username", "default_user");
int port = config.get<int>("port", 8080);
std::cout << "Username: " << username << std::endl;
std::cout << "Port: " << port << std::endl;
// 設定の更新
config.set("username", "new_user");
config.set("port", 9000);
// 設定の保存
config.save();
return 0;
}
データ変換
以下の例では、YAML形式のデータを読み込み、新しい構造のYAMLデータとして出力している。
これは、例えば、ユーザデータを処理して統計情報を生成する場合に有用である。
#include <iostream>
#include <vector>
#include <algorithm>
#include <yaml-cpp/yaml.h>
struct UserData {
std::string name;
int age;
std::vector<std::string> skills;
};
YAML::Node processUserData(const YAML::Node& input)
{
std::vector<UserData> users;
for (const auto& user : input["users"]) {
UserData userData;
userData.name = user["name"].as<std::string>();
userData.age = user["age"].as<int>();
userData.skills = user["skills"].as<std::vector<std::string>>();
users.push_back(userData);
}
// データ処理: 平均年齢とスキルの集計
int totalAge = 0;
std::map<std::string, int> skillCount;
for (const auto &user : users) {
totalAge += user.age;
for (const auto &skill : user.skills) {
skillCount[skill]++;
}
}
double averageAge = users.empty() ? 0 : static_cast<double>(totalAge) / users.size();
// 結果のYAMLノードを作成
YAML::Node output;
output["total_users"] = users.size();
output["average_age"] = averageAge;
YAML::Node skillStats;
for (const auto &[skill, count] : skillCount) {
skillStats[skill] = count;
}
output["skill_statistics"] = skillStats;
return output;
}
int main()
{
try {
YAML::Node input = YAML::LoadFile("user_data.yaml");
YAML::Node output = processUserData(input);
std::cout << "Processed Data:\n" << output << std::endl;
std::ofstream fout("user_statistics.yaml");
fout << output;
}
catch (const YAML::Exception &e) {
std::cerr << "Error processing YAML: " << e.what() << std::endl;
return -1;
}
return 0;
}
階層的データ構造の操作
以下の例では、yaml-cppライブラリを使用して、複雑な階層的データ構造を操作している。
組織構造を表すYAMLデータを読み込み、特定の条件に基づいて検索や更新を行う。
#include <iostream>
#include <functional>
#include <yaml-cpp/yaml.h>
// 再帰的にYAMLノードを探索する関数
void traverseNode(YAML::Node& node, const std::function<void(YAML::Node&)>& callback)
{
if (node.IsMap()) {
for (auto it = node.begin(); it != node.end(); ++it) {
callback(it->second);
traverseNode(it->second, callback);
}
}
else if (node.IsSequence()) {
for (auto& item : node) {
callback(item);
traverseNode(item, callback);
}
}
}
// 特定の条件に一致するノードを検索する関数
std::vector<YAML::Node> findNodes(YAML::Node& root, const std::function<bool(const YAML::Node&)>& predicate)
{
std::vector<YAML::Node> results;
traverseNode(root, [&](YAML::Node &node) {
if (predicate(node)) {
results.push_back(node);
}
});
return results;
}
int main()
{
try {
YAML::Node org = YAML::LoadFile("organization.yaml");
// 例: 全ての部門の予算を10%増加
traverseNode(org, [](YAML::Node& node) {
if (node["budget"]) {
double budget = node["budget"].as<double>();
node["budget"] = budget * 1.1;
}
});
// 例: 特定の役職を持つ従業員を検索
auto managers = findNodes(org, [](const YAML::Node& node) {
return node["position"] && node["position"].as<std::string>() == "Manager";
});
std::cout << "Managers found: " << managers.size() << std::endl;
for (const auto &manager : managers) {
std::cout << "Name: " << manager["name"].as<std::string>() << std::endl;
}
// 更新されたデータを保存
std::ofstream fout("updated_organization.yaml");
fout << org;
}
catch (const YAML::Exception &e) {
std::cerr << "Error processing YAML: " << e.what() << std::endl;
return -1;
}
return 0;
}