C++の応用 - YAML

提供:MochiuWiki - SUSE, Electronic Circuit, PCB
ナビゲーションに移動 検索に移動

概要

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;
 }