
Введение
В этой статье я расскажу об использование библиотеки с открытым исходным кодом yaml-cpp в С++ проектах для сохранения данных в виде YAML-документа. Библиотека имеет лицензию MIT. Вот ссылка на официальный репозиторий.
Итак, данный туториал является логическим продолжением моей другой статьи про чтение YAML-конфигов. В ней мы рассматривали:
какие наиболее популярные библиотеки для работы с YAML представлены в экосистеме C++
почему для задач работы с конфигам я выбрал библиотеку yaml-cpp
как в С++/CMake проекте подключить библиотеку yaml-cpp
какой инструментарий предлагает yaml-cpp для организации чтения YAML-документов из файлов и строк и как его использовать
Также в той статье мы разработали небольшой С++/CMake проект (с подробным описанием) в качестве примера реализации чтения YAML-конфигов в С++ приложении.
В этой же статье мы завершим:
обзор инструментария, предоставляемого yaml-cpp, для генерации YAML-текста
разработку приложения-примера, начатую в прошлой статье, и добавим в нее функционал сохранения/записи настроек в YAML-файл или YAML-строку
Особое внимание будет уделено вопросам форматирования(и представления) выходного YAML-текста, поскольку это влияет на:
восприятие конечного YAML-документа человеком
на совместимость с другими программами, парсеры которых хуже поддерживают спецификации YAML
При написании данной статьи я использовал версию библиотеки yaml-cpp 0.8.0 и коммит "47cd272". Если использовать версию, помеченную тэгом 0.8.0, то на свежих версиях CMake, скорее всего (а в CMake 4.2.0 точно), не получится собрать yaml-cpp.
При написании данной статьи я исходил из того, что читатель знаком с языком C++, системой сборки проектов CMake и имеет представление о формате представления данных YAML.
Интерфейс yaml-cpp
Чтобы эффективно применять библиотеку yaml-cpp для записи данных в формате YAML имеет смысл разобраться с:
особенностями создания объектов YAML::Node и записи данных в них
функционалом, предоставляемым типом YAML::Emitter
возможностями библиотеки по управлению форматированием выходных YAML документов
Я черпал информацию о создании узлов с официального туториала yaml-cpp, а о возможностях генерации YAML-текста здесь. К сожалению, там продемонстрированы и описаны далеко не все возможности, поэтому мне приходилось и в исходный код проекта заглядывать и в отладчике изучать детали работы с yaml-cpp.
Запись данных в YAML::Node
В статье про чтение YAML-конфигов тип YAML::Node мы рассматривали с точки зрения считывания его содержимого в требуемые типы данных С++, а в этом туториале будем решать обратную задачу.
Объект типа YAML::Node, созданный с помощью конструктора без аргументов, не содержит пока информации о том, какие YAML-объекты он будет хранить. В примере ниже это подробно продемонстрировано:
// Создаем корневой узел
YAML::Node root;
// Он пустой, но существует (создан)
assert(root.IsNull() && root.IsDefined());
// Причем, этот узел не имеет пока типа
// Это не Map, не Sequence и не Scalar
assert(!root.IsMap() && !root.IsSequence() && !root.IsScalar());
// Как только обратимся к узлу по ключу (т.е. при вызове operator[])
// узел root сразу получит тип Map.
// Даже если не присваивать никакого значения.
root["map"];
assert(root.IsMap());
// А вот дочерний узел пока без типа (как и узел root после создания)
assert(!root["map"].IsMap() &&
!root["map"].IsSequence() &&
!root["map"].IsScalar());То есть, тип YAML-узла может измениться в зависимости от контекста взаимодействия с ним. Продолжим:
// Если теперь у дочернего узла вызвать метод push_back(), автоматически
// тип дочернего узла (root["map"]) станет последовательностью
root["map"].push_back("one");
root["map"].push_back("two");
assert(root["map"].IsSequence());При добавлении к узлу root["map"] значения с помощью метода push_back() ему присвоился тип последовательности. Вот YAML-текст, получившийся на текущий момент:
map:
- one
- twoЧто будет с типом узла, если теперь к нему обратиться по ключу-строке?
// Если ранее мы обращались к root["map"] как к массиву (вызывали
// push_back()), а теперь обратимся с требованием создать еще два
// дочерних узла (bool и number)
root["map"]["bool"] = true;
root["map"]["number"] = "123";
// То тип root["map"] автоматически расширится с Sequence до Map
// поскольку мы стали добавлять в последовательность уже пары
// типа (key : value), а не отдельные элементы-значения
assert(root["map"].IsMap());YAML-текст, соответствующий коду, станет таким:
map:
0: one
1: two
bool: true
number: 123Помимо того, что сменился тип c Sequence на Map, к элементам, теперь уже бывшей последовательности, автоматически добавились ключи в виде индексов (начиная со значения 0), чтобы словарь был синтаксически корректным.
Еще один пример, для демонстрации преобразования типа YAML-узла с последовательности на словарь:
YAML::Node seq_map_node;
// Присвоен тип Sequence
seq_map_node[0] = 0;
assert(seq_map_node.IsSequence()); // [0]
// Пока еще тип Sequence
seq_map_node.push_back(1); // [0, 1]
assert(seq_map_node.IsSequence());
// Обращение к элементу по индексу seq_map_node.size() = 2
// пока еще Sequence
seq_map_node[seq_map_node.size()] = 2; // [0, 1, 2]
assert(seq_map_node.IsSequence());
// Обратились к элементу по индексу [seq_map_node.size() + 1] = [4],
// то уже получится Map
seq_map_node[seq_map_node.size() + 1] = 3; // {0: 0, 1: 1, 2: 2, 4: 3}
assert(seq_map_node.IsMap());Узел всегда создается при обращении по ключу и, при этом, если ключ:
не целочисленный, то узел станет ассоциативным массивом (Map)
целочисленный, то при:
IntegerKey >= 0 && IntegerKey <= Node.size() - тип Sequence узлу присвоится при первом обращении и сохранится при последующих
IntegerKey > Node.size() - тип узла преобразуется на Map
Конечно же, yaml-cpp предоставляет возможность создавать YAML::Node и с помощью обычного конструктора, принимающего С++ типы данных в качестве аргумента:
// Примеры с созданием узлов с содержимым типа Scalar
YAML::Node str_node("String");
assert(str_node.IsScalar());
assert(str_node.as<std::string>() == "String");
YAML::Node int_node(101);
assert(int_node.IsScalar());
assert(int_node.as<int>() == 101);
// Создаем узел Sequence из std::vector
auto vec = std::vector<int>({1,2,3});
YAML::Node sec_node(vec);
assert(sec_node.IsSequence());
assert(sec_node.as<std::vector<int>>() == vec);
// Создаем узел Map из std::map
auto map = std::map<int, char>({{1, 'a'}, {2, 'b'}});
YAML::Node map_node(map);
assert(map_node.IsMap());
assert(map_node.as<decltype(map)>() == map);Также узел можно заполнить и из YAML-текста, однако, если в нем будут содержаться ошибки, то нужно быть готовым поймать исключение:
YAML::Node root;
root["seq"] = YAML::Load("['a','b','c']"); // Из YAML-текста получаем Sequence-узел
root["map"] = YAML::Load("{x: X, y: Y, z: Z}"); // Из YAML-текста получаем Map-узел
// Важно, что при таком создание узлов, в них сохранится кроме значений
// еще и стиль (однострочный или многострочный), в которым были
// представлены входные данные
assert(root["seq"].IsSequence());
assert(root["map"].IsMap());
assert(root["seq"].size() == 3);
assert(root["map"].size() == 3);
assert(root["seq"][0].as<char>() == 'a');
assert(root["map"]["x"].as<std::string>() == "X");Код выше соответствует такому YAML-тексту:
seq: [a, b, c]
map: {x: X, y: Y, z: Z}А вот пример как создать подстановку с помощью YAML-псевдонима (*) и YAML-якоря(&):
YAML::Node root;
root["seq"] = YAML::Load("['a','b','c']");
root["map"] = YAML::Load("{x: X, y: Y, z: Z}");
root["alias"] = root["seq"];Результат:
seq: &1 [a, b, c]
map: {x: X, y: Y, z: Z}
alias: *1Видно, что при создании нового узла путем присваивания ему уже существующего (в этом же YAML-дереве) узла автоматически создается YAML-подстановка (ссылка и якорь).
Конструирование YAML-документа с помощью YAML::Node рассмотрели, переходим теперь к возможностям формирования YAML-текста.
Генерация YAML с использованием возможностей форматирования YAML::Node
Для генерации YAML-текста библиотека yaml-cpp предоставляет класс YAML::Emitter. Интерфейса работы с объектами этого класса очень похож на таковой потока вывода std::ostream.
YAML::Emitter оснащен богатым набором функций:
перегрузки оператора << для поддерживаемых типов (для пользовательских мы можем написать свои)
метод c_str() для получения содержимого потока вывода в виде строки
метод good() для получения состояния потока вывода
группы методов для локальных и глобальных настроек форматирования
В предыдущем подразделе мы разобрали основные способы формирования YAML-документа с помощью дерева из YAML::Node. Вот как можно теперь получить для готового дерева соответствующее YAML-представление:
YAML::Emitter out;
YAML::Node node;
node["root"]["map"]["k1"] = "v1";
node["root"]["map"]["k2"] = "v2";
node["root"]["seq"] = std::vector<int>({1,2,3,4});
// Можно сразу направить YAML::Node в объект YAML::Emitter
out << node;
// Убедимся, что все ок
assert(out.good());
// Как и у std::string у YAML::Emitter есть метод c_str()
std::cout << out.c_str() << std::endl;Такой код выведет на экран следующий текст:
root:
map:
k1: v1
k2: v2
seq:
- 1
- 2
- 3
- 4Но что, если требуется выводить последовательность и ассоциативный массив в однострочном формате? Хорошая новость в том, что на это можно повлиять как на уровне объектов YAML::Node (метод SetStyle()), так и с помощью YAML::Emitter.
YAML::Node::SetStyle() позволяет задать оба стиля форматирования:
Block (блочный)
Flow (однострочный)
Пример форматирования только для root["map"] и root["seq"] из кода выше:
YAML::Node node;
node["root"]["map"]["k1"] = "v1";
node["root"]["map"]["k2"] = "v2";
node["root"]["seq"] = std::vector<int>({1,2,3,4});
// Задаем однострочный формат представления для узлов seq и map
node["root"]["seq"].SetStyle(YAML::EmitterStyle::Flow);
node["root"]["map"].SetStyle(YAML::EmitterStyle::Flow);
YAML::Emitter out;
out << node;
std::cout << out.c_str() << std::endl;результат:
root:
map: {k1: v1, k2: v2}
seq: [1, 2, 3, 4]А можно было для корневого узла root задать однострочный стиль:
YAML::Node node;
node["root"]["map"]["k1"] = "v1";
node["root"]["map"]["k2"] = "v2";
node["root"]["seq"] = std::vector<int>({1,2,3,4});
// Задаем однострочный формат представления для всей иерархии узлов
node.SetStyle(YAML::EmitterStyle::Flow);
YAML::Emitter out;
out << node;
std::cout << out.c_str() << std::endl;на выходе получим весь документ в однострочном формате:
{root: {map: {k1: v1, k2: v2}, seq: [1, 2, 3, 4]}}Стиль форматирования, установленный для узла, распространяется на сам узел и все его дочерние узлы. На этом возможности по форматированию выходного YAML-текста с помощью YAML::Node исчерпаны.
Код генерации YAML на основе этого подхода получается лаконичным и ясным, но возможности форматирования ограничены только настройками стиля.
Генерация YAML с использованием возможностей форматирования YAML::Emitter
Второй способ генерации YAML-текста заключается в использовании возможностей связки YAML::Emitter и манипуляторов типа YAML::EMITTER_MANIP.
Вот основные из манипуляторов:
BeginDoc/EndDoc - для работы с многодокументным YAML-текстом
BeginMap/EndMap - задают начало и конец ассоциативного массива
BeginSec/EndSeq - задают начало и конец последовательности
Key/Value - задают ключ и значение
и вспомогательные:
Auto/Flow/Block - управление стилем форматирования
Anchor/Alias - работа с якорями и ссылками
Comment - запись комментариев
Hex/Dec/Oct - представление целых чисел
SingleQuoted/DoubleQuoted/Literal - представление строк
Это далеко не все и в документации (ее особо и нет, только примеры) они не описаны (во всяком случае, я не смог найти). Лучше всего, при необходимости, заглянуть в файл исходного кода yaml-cpp/emittermanip.h и поэкспериментировать с ними.
Важно. По умолчанию, YAML::Emitter формирует выходной YAML-текст в кодировке UTF-8. Этим можно управлять с помощью метода SetOutputCharset().
Манипуляторы распространяют свое действие от узла, на котором их включили и далее вниз по иерархии. Например, если мы применили манипулятор Flow перед BeginMap, то однострочный стиль завершит свое действие только после соответствующего вызова EndMap.
Код с использованием этой связки (YAML::Emitter и манипуляторов) выглядит так:
YAML::Emitter out;
// Корневой Map для создания в нем вложенного словаря root
out << YAML::BeginMap;
// Сам узел root
out << YAML::Key << "root";
out << YAML::Value;
// Узел root тоже будет типа Map
out << YAML::BeginMap;
// Создаем узел seq для хранения Sequence root["seq"]
out << YAML::Key << "seq";
out << YAML::Value;
// Задаем однострочный стиль узла-хранителя Sequence
out << YAML::Flow;
// Начало последовательности
out << YAML::BeginSeq;
// Заполняем последовательность значениями
out << 1 << 2 << 3;
// Конец последовательности
out << YAML::EndSeq;
// Второй дочерний узел root["number"]
out << YAML::Key << "number";
out << YAML::Value << "101";
// Завершаем Map вложенный в root
out << YAML::EndMap;
// Завершаем root Map
out << YAML::EndMap;
std::cout << out.c_str() << std::endl;Результат кода в консоли будет:
root:
seq: [1, 2, 3]
number: 101Для облегчения восприятия кода я использовал табуляцию, но на мой взгляд, этот способ генерации YAML не так так элегантен, как подход с YAML::Node. А если в чем-то проиграли, значит в чем-то должны и выиграть. И вот какие возможности теперь у нас появились:
YAML::Emitter out;
// Устанавливаем размер отступов
out.SetIndent(8);
// Контролируем стиль заполнения булевый полей
// on/off yes/no true/false
out.SetBoolFormat(YAML::EMITTER_MANIP::YesNoBool);
out << YAML::Comment("Начало документа");
out << YAML::BeginMap;
out << YAML::Key << "root";
out << YAML::Anchor("rootAlias");
out << YAML::Value;
out << YAML::BeginMap;
// Создаем узел root["bool"]
out << YAML::Key << "bool";
out << YAML::Value << true;
// Второй дочерний узел root["float"]
out << YAML::Key << "float";
// Задаем точность при печати чисел с плавающей точкой
out << YAML::Value << YAML::DoublePrecision(3) << 3.14;
out << YAML::Key << "hex";
// Пишем в шестнадцатеричном виде число
out << YAML::Value << YAML::EMITTER_MANIP::Hex << 255;
// Завершаем Map вложенный в root
out << YAML::EndMap;
out << YAML::Key << "copy";
out << YAML::Alias("rootAlias");
// Завершаем root Map
out << YAML::EndMap;
out << YAML::EMITTER_MANIP::Newline;
out << YAML::Comment("Конец документа");И в результате получили YAML с :
комментариями
именованными якорем и ссылкой
заданным размером отступов 8
"yes/no" для булевого поля
шестнадцатеричным представлением целого числа (255)
# Начало документа
root: &rootAlias
bool: yes
float: 3.14
hex: 0xff
copy: *rootAlias
# Конец документаВажно. Тем, кому потребуется выводить в YAML числа с плавающей точкой, нужно иметь в виду, что если явно не задать точность, как в примере выше, то можно на выходе получить текст вида "3.1400000000000001". При считывании такого значения с помощью yaml-cpp проблем не будет, но лучше такие моменты контролировать.
Еще одна очень полезная возможность заключается в том, что мы можем предоставить перегрузку operator << для своей структуры данных:
struct Person {
std::string full_name;
int age;
};
YAML::Emitter& operator << (YAML::Emitter& out, const Person& p) {
out << YAML::Flow;
out << YAML::BeginMap;
out << "full_name" << p.full_name;
out << "age" << p.age;
out << YAML::EndMap;
return out;
}
void check_overload() {
Person p{"Иван Иванович", 37};
YAML::Emitter out;
out << p;
std::cout << out.c_str();
}
// Будет сформировано
// {full_name: Иван Иванович, age: 37}Последний пример, демонстрирует работу с манипуляторами BeginDoc/EndDoc для многодокументного вывода и DoubleQuoted/SingleQuoted для управления кавычками:
YAML::Emitter out;
out << YAML::BeginDoc << YAML::BeginMap;
out << YAML::Key << "doc1_key";
out << YAML::Value << YAML::DoubleQuoted << "double qouted text";
out << YAML::Comment("doc 1");
out << YAML::EndMap << YAML::EndDoc;
out << YAML::BeginDoc << YAML::BeginMap;
out << YAML::Key << "doc2_key";
// Пропускаем YAML::Value, и тоже будет работать аналогично блоку с doc1_key выше.
out << YAML::SingleQuoted << "single qouted text";
out << YAML::Comment("doc 2");
out << YAML::EndMap << YAML::EndDoc;
std::cout << out.c_str() << std::endl;Выходной YAML-текст:
---
doc1_key: "double qouted text" # doc 1
...
---
doc2_key: 'single qouted text' # doc 2
...В примере кода выше (строки 11-12), следует отметить, что после YAML::Key можно явно не указывать YAML::Value. YAML::Emitter, по умолчанию, после манипулятора YAML::Key ожидает, что следующая порция данных будет относиться к значению.
Более того, можно и YAML::Key пропускать. В пределах YAML::BeginMap/YAML::EndMap YAML::Emitter ожидает ввода пар ключ/значение. Однако, в таком коде потом легко и самому запутаться и тем, кто будет его поддерживать.
Важно. Библиотека yaml-cpp поддерживает спецификацию YAML 1.2, поэтому при генерации YAML-документов, которые в дальнейшем могут считывать другие программы, нужно иметь в виду, что далеко не все парсеры поддерживают эту спецификацию.
В итоге YAML::Emitter и манипуляторы дают возможность гибко настраивать формат выходного YAML-текста, а платой за это будет более многословный код.
Микс из обоих подходов
Мы рассмотрели оба подхода для генерации YAML-текста по отдельности, но их можно комбинировать, учитывая преимущества и недостатки каждого.
YAML::Emitter out;
YAML::Node primes(std::vector<int>{2,3,5,7,11});
primes.SetStyle(YAML::EmitterStyle::Flow);
out.SetDoublePrecision(3); // Задаем точность вывода чисел с плавающей
out << YAML::BeginMap;
out << "math";
out << YAML::BeginMap;
// YAML::Emitter ожидает пары Key/Value, тут все корректно
out << "pi" << 3.14;
out << "e" << 2.718;
// Пишем уже заполенный последовательностью YAML::Node
out << "primes" << primes;
out << YAML::EndMap;
out << YAML::EndMap;
assert(out.good());
std::cout << out.c_str() << std::endl;Код выведет ожидаемый YAML-текст:
math:
pi: 3.14
e: 2.72
primes: [2, 3, 5, 7, 11]Комбинируя оба подхода по генерации YAML-текста, можно добиться оптимального баланса между простотой реализации на С++ и качеством форматирования выходного YAML-документа.
Важно. При использовании смешанного режима нужно учитывать, что настройки форматирования для YAML::Emitter не будут распространяться на YAML::Node. А настройки форматирования, YAML::Node, как мы помним, позволяют управлять только стилем вывода (Flow, Block).
Обработка ошибок
При записи данных в объекты YAML::Emitter легко допустить ошибки, например, перепутать порядок порядок открывающих/закрывающих манипуляторов. В случае ошибки, объект YAML::Emitter перестанет воспринимать очередные попытки записи данных.
YAML::Emitter предоставляет два метода для контроля ошибок:
good() - проверяет состояние потока YAML
GetLastError() - возвращает описание ошибки
При ошибках записи данных в YAML::Node могут генерироваться исключения. Вот код с примерами получения информации о некоторых ошибках:
// Узлу присвоится тип Scalar
YAML::Node root(100);
try {
// Обраще��ие c узлом типа Scalar как с Map выбросит исключение
root["key"] = 200;
} catch (const YAML::Exception& ex) {
// Будет выведено "operator[] call on a scalar (key: "key")"
std::cerr << ex.what() << std::endl;
}
YAML::Emitter out;
assert(out.good());
out << YAML::EndMap; // Вызов EndMap без предварительного BeginMap
assert(!out.good());
// В err_str запишется "unexpected end map token"
std::string err_str = out.GetLastError();К этому моменту, надеюсь, у меня получилось дать представление об основных функциональных возможностях библиотеки yaml-cpp по части генерации YAML-документов.
Реализация сохранения С++ структуры с конфигом в YAML-файл
В практической части прошлой статьи мы разрабатывали простое С++/CMake приложение yaml-demo, которое считывало YAML-конфиг из файла(или строки) в соответствующую С++ структуру для хранения этих настроек. А в этой статье завершим разработку yaml-demo, добавив возможность сохранять настройки программы в YAML-формате. Ссылка на исходный код всего проекта будет приведена в конце статьи.
Итак, нам нужно С++ структуру с настройками программы:
struct ServerSettings {
std::string proto;
std::uint16_t port{0};
};
struct LogSettings {
std::string level;
std::string folder;
};
struct MtlsSettings {
bool use_mtls_auth{false};
std::vector<float> versions;
std::string ca_cert;
std::string server_cert;
std::string server_key;
};
struct DemoConfig {
std::vector<ServerSettings> servers;
LogSettings logs;
MtlsSettings mtls;
};Сохранить в YAML-файл конфигурации в следующем виде:
servers:
- protocol: socks5
port: 1080
- protocol: http(s)
port: 2080
logging:
level: debug
folder: './log'
mtls_auth:
enabled: on
tls:
versions: [1.2, 1.3]
certificates:
ca_cert: ca.pem
server_cert: server-cert.pem
private_key: server-key.pemТакже добавлю еще требование, чтоб выходной YAML посимвольно соответствовал представленному выше тексту.
Начнем с того, что напишем по одной свободной функции для формирования YAML-строки для каждого из разделов конфига.
В проекте это файл yaml_demo/lib/yaml_config/src/demo_config_manager.cpp.
servers:
/* Запись секции servers
servers:
- protocol: socks5
port: 1080
- protocol: http(s)
port: 2080
*/
void write_to_yaml_stream(YAML::Emitter& out,
const std::vector<ServerSettings>& servers)
{
using namespace YAML;
out << Key << "servers";
out.SetLocalIndent(2);
out << BeginSeq;
for (const auto& [proto, port] : servers)
out << BeginMap << "protocol" << proto << "port" << port << EndMap;
out << EndSeq;
out << Newline << Newline;
}logging:
/* Запись секции logging
logging:
level: debug
folder: './log'
*/
void write_to_yaml_stream(YAML::Emitter& out, const LogSettings& logs)
{
using namespace YAML;
out << Key << "logging" << BeginMap;
out << "level" << logs.level;
out << "folder" << SingleQuoted << logs.folder;
out << EndMap;
out << Newline << Newline;
}mtls_auth:
/* Запись секции mtls_auth
mtls_auth:
enabled: on
tls:
versions: [1.2, 1.3]
certificates:
ca_cert: ca.pem
server_cert: server-cert.pem
private_key: server-key.pem
*/
void write_to_yaml_stream(YAML::Emitter& out, const MtlsSettings& mtls)
{
using namespace YAML;
out << Key << "mtls_auth" << BeginMap;
// OnOffBool - включает запись on|off вместо true|false
out << "enabled" << OnOffBool << mtls.use_mtls_auth;
out << "tls";
out << BeginMap << "versions" << Flow << FloatPrecision(3);
// Автоматически сработает перегрузка для std::vector<float>
// Поэтому можно явно не указывать BeginSec|EndSeq
out << mtls.versions;
out << EndMap;
out << "certificates" << BeginMap;
out << "ca_cert" << mtls.ca_cert;
out << "server_cert" << mtls.server_cert;
out << "private_key" << mtls.server_key;
out << EndMap;
out << EndMap;
out << Newline;
}Все эти функции получают на вход ссылку на объект YAML::Emitter и ссылку на структуру с настройками для дальнейшей записи в корень YAML-документа своей части настроек.
Функция to_string() создает корневой Map YAML::документа, задает глобальные настройки для YAML::Emitter и делегирует заполнение основных разделов конфига описанным выше функциям.
// Функция из структуры с конфигом формирует и возвращает строку
// с YAML представлением конфига
std::string to_string(const DemoConfig& cfg)
{
YAML::Emitter out;
out.SetIndent(4); // Задаем отступы 4 символа
out << YAML::BeginMap;
write_to_yaml_stream(out, cfg.servers);
write_to_yaml_stream(out, cfg.logs);
write_to_yaml_stream(out, cfg.mtls);
out << YAML::EndMap;
// Если были какие-либо ошибки при записи, информируем пользователя
if (!out.good())
throw std::runtime_error(out.GetLastError());
return out.c_str();
}Функции, которые мы к этому моменту написали могут генерировать исключения, поэтому поместим их вызовы в другую функцию save_to_yaml(), которая будет отвечать за перехват и обработку исключений:
// Формирует для пользователя доступную информацию об ошибке
template <typename T>
std::string gen_save_err_msg(std::string_view source, const T& ex)
{
std::ostringstream oss;
oss << "An error occurred while saving settings to [" << source << "]\n";
oss << "Error details: " << ex.what();
return oss.str();
};
// В source передается информация о том куда записать YAML-документ
// Это будет использовано при выводе описания ошибки (gen_save_err_msg)
// Внутри лямбды save_yaml_fn будет происходить вызов to_string(),
// который сгенерирует для DemoConfig соответсвующий YAML-текст
template <typename F>
bool save_to_yaml(std::string_view source, F save_yaml_fn)
{
try {
return save_yaml_fn();
}
catch (const YAML::Exception& ex) {
std::cerr << gen_save_err_msg(source, ex);
}
catch (const std::exception& ex) {
std::cerr << gen_save_err_msg(source, ex);
}
catch (...) {
std::cerr << gen_save_err_msg(source, std::runtime_error{"Unknown error"});
}
return false;
}save_yaml_fn (19 строка) - это лямбда функция, которая определена в методах класса DemoConfigManager. Она инкапсулирует логику сохранения результирующего YAML-документа из объекта YAML::Emitter в файл или строку и описана ниже:
bool DemoConfigManager::save_to_file(const std::string& path, const DemoConfig& cfg)
{
auto save_yaml_fn = [&path, &cfg]() {
std::ofstream ofs(path, std::ios::trunc);
if (!ofs.is_open())
throw std::runtime_error{"File open error"};
ofs << to_string(cfg);
if (!ofs.good())
throw std::runtime_error{"File write error"};
return true;
};
return save_to_yaml(path, save_yaml_fn);
}
bool DemoConfigManager::save_to_string(std::string& out_str, const DemoConfig& cfg)
{
auto save_yaml_fn = [&out_str, &cfg]() {
out_str = to_string(cfg);
return true;
};
return save_to_yaml(out_str, save_yaml_fn);
}Сам класс DemoConfigManager отвечает за предоставление интерфейса работы с конфигом приложению yaml-demo (yaml_demo/lib/yaml_config/include/yaml_config/demo_config_manager.h):
class DemoConfigManager {
public:
bool load_from_file(const std::string& path, DemoConfig& cfg);
bool load_from_string(const std::string& str, DemoConfig& cfg);
bool is_loaded() const { return is_loaded_; }
// Сохранение в YAML-файл
bool save_to_file(const std::string& path, const DemoConfig& cfg);
// Сохранение в YAML-строку
bool save_to_string(std::string& out_str, const DemoConfig& cfg);
private:
bool is_loaded_{false};
};Наконец, применение DemoConfigManager в основном приложении (yaml_demo/src/main.cpp):
#include <yaml_config/demo_config.h>
#include <yaml_config/demo_config_manager.h>
#include <iostream>
const std::string g_yaml_cfg_string{
"servers:\n"
" - protocol: socks5\n"
" port: 1080\n"
" - protocol: http(s)\n"
" port: 2080\n\n"
"logging:\n"
" level: debug\n"
" folder: './log'\n\n"
"mtls_auth:\n"
" enabled: on\n"
" tls:\n"
" versions: [1.2, 1.3]\n"
" certificates:\n"
" ca_cert: ca.pem\n"
" server_cert: server-cert.pem\n"
" private_key: server-key.pem\n"
};
const std::string g_yaml_cfg_file_path{"config.yml"};
const std::string g_yaml_cfg_out_file_path{"config_out.yml"};
int main()
{
demo::DemoConfigManager cfg;
// Чтение YAML-настроек из строки
demo::DemoConfig settingsFromString;
std::cout << "=== Read from string ===\n";
cfg.load_from_string(g_yaml_cfg_string, settingsFromString);
if (cfg.is_loaded())
std::cout << settingsFromString << std::endl;
// Чтение YAML-настроек из файла
demo::DemoConfig settingsFromFile;
std::cout << "=== Read from file ===\n";
cfg.load_from_file(g_yaml_cfg_file_path, settingsFromFile);
if (cfg.is_loaded())
std::cout << settingsFromFile << std::endl;
std::cout << std::endl;
// Запись YAML-настроек в файл
if (cfg.save_to_file(g_yaml_cfg_out_file_path, settingsFromString))
std::cout << "Saving config to "
<< g_yaml_cfg_out_file_path << " was successful";
std::cout << std::endl;
// Запись YAML-настроек в строку
std::string yaml_cfg_string;
if (cfg.save_to_string(yaml_cfg_string, settingsFromString))
std::cout << "Saving config to yaml string was successful";
std::cout << std::endl;
// Сравниваем сформированную строку с эталоном
if (yaml_cfg_string == g_yaml_cfg_string)
std::cout << "The resulting YAML string is equivalent to the original one.";
// Выводим результат сохранения конфига в YAML-строку
std::cout << yaml_cfg_string;
return 0;
}После запуска yaml-demo выполняет следующие действия:
считывает в С++ структуру настройки из строки и из файла config.yml
сохраняет настройки из С++ структуры в строку и в файл config_out.yml
промежуточные результаты выводит в стандартный поток вывода
Часть вывода программы:

Как видно по результату работы программы, на экран выводится точно такой же YAML-текст, который был изначально считан из конфигурационного файла config.yml.
Используя возможности форматирования yaml-cpp, мы сформировали не просто корректный YAML-документ, но и добились того, чтоб он выглядел как нам нужно (отступы, блочный/однострочный стили, кавычки, точность данных с плавающей запятой, вывод булевого поля в режиме on/off).
Полный исходных код примера доступен по ссылке на GitHub.
Работоспособность проверена на:
Debian 13 (Qt Creator 16.0.1, CMake 3.31.6, GCC 14.2.0, Ninja 1.12.1)
Manjaro 26.0.0 (Qt Creator 18.0.1, CMake 4.2.1, GCC 15.2.1, Ninja 1.13.2)
Windows 11 (Visual Studio 2022 Community Edition, CMake 3.36.1, Ninja 1.11.1)
Заключение
В этой статье были продемонстрированы возможности библиотеки yaml-cpp для решения задач генерации YAML-документов. В практической части туториала мы завершили разработку учебного приложения yaml-demo (начатую в прошлой статье), добавив в него возможность сохранения конфигурации в YAML-файл или YAML-строку.
Также я поделился нюансами, с которыми сталкивался сам при работе с yaml-cpp. Надеюсь, статья будет полезной и позволит тем, кому эта тема интересна, сэкономить время при решении подобных задач.
Буду рад конструктивной критике и обязательно ее учту при подготовке новых статей.
