🔝 Retour au Sommaire
La lecture de fichiers de configuration est le cas d'usage principal de YAML en C++. Un programme charge un fichier .yaml au démarrage, extrait les paramètres nécessaires, et les convertit en structures C++ utilisées par le reste de l'application. Ce flux, conceptuellement simple, recèle des subtilités liées à la richesse (et aux pièges) du format YAML.
Cette section couvre le chargement de fichiers, la navigation dans l'arbre YAML::Node, la conversion vers des types métier, la gestion des documents multiples, et les stratégies de validation défensive face aux comportements surprenants de YAML.
La fonction YAML::LoadFile est le point d'entrée standard. Elle prend un chemin de fichier et retourne un YAML::Node représentant le document complet :
#include <yaml-cpp/yaml.h>
#include <print>
int main() {
YAML::Node config = YAML::LoadFile("config.yaml");
std::print("Type du nœud racine : {}\n",
config.IsMap() ? "map" : "autre");
std::print("Nombre de clés racine : {}\n", config.size());
}Si le fichier n'existe pas ou n'est pas lisible, YAML::LoadFile lève une YAML::BadFile. Si le contenu n'est pas du YAML valide, une YAML::ParserException est levée. Un chargement robuste gère les deux cas :
YAML::Node load_yaml_safe(const std::string& path) {
try {
return YAML::LoadFile(path);
} catch (const YAML::BadFile& e) {
std::print(stderr, "Fichier inaccessible : {}\n", path);
throw;
} catch (const YAML::ParserException& e) {
std::print(stderr, "Syntaxe YAML invalide dans {} (ligne {}, col {}) :\n {}\n",
path, e.mark.line + 1, e.mark.column + 1, e.what());
throw;
}
}Pour les tests unitaires ou les données reçues depuis le réseau, YAML::Load parse une chaîne :
std::string yaml_content = R"(
server:
host: localhost
port: 8080
)";
YAML::Node config = YAML::Load(yaml_content);
std::string host = config["server"]["host"].as<std::string>(); Un fichier YAML peut contenir plusieurs documents séparés par ---. C'est courant dans l'écosystème Kubernetes où un seul fichier regroupe plusieurs manifestes. YAML::LoadAllFromFile retourne un std::vector<YAML::Node>, un par document :
// manifests.yaml contient :
// ---
// kind: Deployment
// metadata:
// name: api
// ---
// kind: Service
// metadata:
// name: api-svc
std::vector<YAML::Node> docs = YAML::LoadAllFromFile("manifests.yaml");
std::print("Nombre de documents : {}\n", docs.size());
for (std::size_t i = 0; i < docs.size(); ++i) {
std::string kind = docs[i]["kind"].as<std::string>("Unknown");
std::string name = docs[i]["metadata"]["name"].as<std::string>("unnamed");
std::print("Document {} : {} / {}\n", i + 1, kind, name);
}L'équivalent depuis une chaîne est YAML::LoadAll(string).
L'opérateur [] sur un nœud de type map retourne le nœud enfant associé à la clé. Si la clé n'existe pas, un nœud Null est retourné — il ne lève pas d'exception mais évalue à false dans un contexte booléen :
YAML::Node config = YAML::LoadFile("config.yaml");
// Accès imbriqué
YAML::Node db_node = config["database"]["host"];
// Le nœud est Null si la clé n'existe pas
YAML::Node missing = config["nonexistent"];
std::print("Existe : {}\n", missing.IsDefined() ? "oui" : "non"); // non Ce comportement diffère de nlohmann/json où operator[] sur un objet non-const crée la clé. Avec yaml-cpp, l'accès en lecture ne modifie jamais l'arbre — c'est un piège en moins.
En revanche, le chaînage d'accès sur un nœud Null retourne un autre nœud Null sans erreur, ce qui peut masquer des problèmes :
// Pas d'erreur même si "database" n'existe pas
// → retourne un nœud Null silencieusement
std::string host = config["database"]["host"].as<std::string>("default");
// host == "default" — mais est-ce parce que "host" est absent,
// ou parce que "database" est absent ? Impossible à distinguer.Pour un diagnostic précis, il faut tester chaque niveau explicitement ou utiliser une validation structurelle préalable (couverte plus loin dans cette section).
Les séquences YAML sont accessibles par index entier, de 0 à size() - 1 :
YAML::Node config = YAML::Load(R"(
ports:
- 8080
- 8443
- 9090
)");
for (std::size_t i = 0; i < config["ports"].size(); ++i) {
std::print("Port {} : {}\n", i, config["ports"][i].as<int>());
}Un accès hors limites retourne un nœud Null au lieu de lever une exception. C'est cohérent avec le comportement des maps, mais demande la même vigilance.
Avant d'extraire une valeur, on peut vérifier le type du nœud :
YAML::Node node = config["server"]["port"];
if (node.IsScalar()) {
int port = node.as<int>();
} else if (node.IsSequence()) {
// Plusieurs ports listés
auto ports = node.as<std::vector<int>>();
} else if (node.IsNull() || !node.IsDefined()) {
// Absent ou explicitement null
std::print("Port non spécifié, utilisation du défaut\n");
}Les prédicats disponibles : IsScalar(), IsSequence(), IsMap(), IsNull(), IsDefined(). La distinction entre IsNull() (clé présente avec valeur ~ ou null) et !IsDefined() (clé absente) est parfois importante pour différencier « l'utilisateur a explicitement mis null » de « l'utilisateur n'a rien spécifié ».
La méthode .as<T>() convertit un nœud scalaire vers le type C++ demandé. yaml-cpp gère nativement les conversions vers les types fondamentaux :
YAML::Node config = YAML::Load(R"(
app:
name: monitoring-agent
version: 3
sampling_rate: 0.75
debug: true
description: ~
)");
std::string name = config["app"]["name"].as<std::string>();
int version = config["app"]["version"].as<int>();
double rate = config["app"]["sampling_rate"].as<double>();
bool debug = config["app"]["debug"].as<bool>(); Si le nœud ne peut pas être converti vers le type demandé, YAML::BadConversion est levée. C'est le cas par exemple si on appelle .as<int>() sur la chaîne "hello".
La surcharge .as<T>(default_value) retourne la valeur par défaut si le nœud n'est pas défini, est null, ou si la conversion échoue :
int port = config["app"]["port"].as<int>(8080);
int workers = config["app"]["workers"].as<int>(4);
std::string log_level = config["app"]["log_level"].as<std::string>("info"); C'est l'équivalent fonctionnel de .value() dans nlohmann/json, mais intégré directement dans la méthode d'extraction. Ce pattern est la manière idiomatique de gérer les champs optionnels avec yaml-cpp — concis et lisible.
Les séquences YAML se convertissent directement en conteneurs STL, à condition que le type élémentaire soit convertible :
YAML::Node config = YAML::Load(R"(
allowed_origins:
- https://app.example.com
- https://admin.example.com
- https://localhost:3000
port_range:
- 8000
- 8100
tags:
env: production
region: eu-west
team: platform
)");
// Séquence → vector
auto origins = config["allowed_origins"].as<std::vector<std::string>>();
// Séquence → vector d'entiers
auto ports = config["port_range"].as<std::vector<int>>();
// Map → std::map
auto tags = config["tags"].as<std::map<std::string, std::string>>();Pour les types métier, la spécialisation YAML::convert<T> présentée en section 24.2 permet une extraction transparente. Voici un exemple complet appliqué à un fichier de configuration réaliste :
struct TlsConfig {
std::string cert_path;
std::string key_path;
bool verify_client;
};
struct ServerConfig {
std::string host;
int port;
int workers;
std::optional<TlsConfig> tls;
std::vector<std::string> allowed_origins;
};
namespace YAML {
template <>
struct convert<TlsConfig> {
static bool decode(const Node& node, TlsConfig& t) {
if (!node.IsMap()) return false;
if (!node["cert_path"] || !node["key_path"]) return false;
t.cert_path = node["cert_path"].as<std::string>();
t.key_path = node["key_path"].as<std::string>();
t.verify_client = node["verify_client"].as<bool>(false);
return true;
}
static Node encode(const TlsConfig& t) {
Node node;
node["cert_path"] = t.cert_path;
node["key_path"] = t.key_path;
node["verify_client"] = t.verify_client;
return node;
}
};
template <>
struct convert<ServerConfig> {
static bool decode(const Node& node, ServerConfig& s) {
if (!node.IsMap()) return false;
if (!node["host"] || !node["port"]) return false;
s.host = node["host"].as<std::string>();
s.port = node["port"].as<int>();
s.workers = node["workers"].as<int>(4);
// Champ optionnel : TLS
if (node["tls"] && !node["tls"].IsNull()) {
s.tls = node["tls"].as<TlsConfig>();
} else {
s.tls = std::nullopt;
}
// Séquence optionnelle
if (node["allowed_origins"]) {
s.allowed_origins =
node["allowed_origins"].as<std::vector<std::string>>();
}
return true;
}
static Node encode(const ServerConfig& s) {
Node node;
node["host"] = s.host;
node["port"] = s.port;
node["workers"] = s.workers;
if (s.tls.has_value()) {
node["tls"] = *s.tls;
}
node["allowed_origins"] = s.allowed_origins;
return node;
}
};
} // namespace YAMLAvec ces spécialisations en place, le chargement de la configuration tient en quelques lignes :
// config.yaml :
// server:
// host: 0.0.0.0
// port: 443
// workers: 8
// tls:
// cert_path: /etc/ssl/server.crt
// key_path: /etc/ssl/server.key
// verify_client: true
// allowed_origins:
// - https://app.example.com
// - https://admin.example.com
YAML::Node root = YAML::LoadFile("config.yaml");
auto server = root["server"].as<ServerConfig>();
std::print("{}:{} ({} workers)\n", server.host, server.port, server.workers);
if (server.tls) {
std::print("TLS actif : {}\n", server.tls->cert_path);
}L'itération sur un nœud de type map produit des paires de YAML::Node. La clé est elle-même un nœud, ce qui nécessite un appel .as<std::string>() pour obtenir une chaîne :
YAML::Node config = YAML::Load(R"(
environment:
APP_NAME: my-service
APP_PORT: "8080"
APP_DEBUG: "false"
DB_HOST: db.internal
)");
for (const auto& kv : config["environment"]) {
std::string key = kv.first.as<std::string>();
std::string value = kv.second.as<std::string>();
std::print("{} = {}\n", key, value);
}Un point important : contrairement à std::map et à nlohmann/json (qui utilise std::map en interne), yaml-cpp préserve l'ordre d'insertion des clés. Les clés sont itérées dans l'ordre dans lequel elles apparaissent dans le fichier YAML. C'est souvent le comportement souhaité pour les fichiers de configuration.
L'itération sur une séquence est directe :
YAML::Node config = YAML::Load(R"(
services:
- name: api
port: 8080
- name: worker
port: 9090
- name: scheduler
port: 7070
)");
for (const auto& service : config["services"]) {
std::string name = service["name"].as<std::string>();
int port = service["port"].as<int>();
std::print("Service {} sur le port {}\n", name, port);
}Pour parcourir l'intégralité d'un document YAML de profondeur arbitraire, une fonction récursive s'impose :
void dump_node(const YAML::Node& node, int depth = 0) {
std::string indent(depth * 2, ' ');
switch (node.Type()) {
case YAML::NodeType::Scalar:
std::print("{}(scalar) {}\n", indent, node.as<std::string>());
break;
case YAML::NodeType::Sequence:
std::print("{}(sequence, {} éléments)\n", indent, node.size());
for (const auto& item : node) {
dump_node(item, depth + 1);
}
break;
case YAML::NodeType::Map:
std::print("{}(map, {} clés)\n", indent, node.size());
for (const auto& kv : node) {
std::print("{} clé: {}\n", indent,
kv.first.as<std::string>());
dump_node(kv.second, depth + 1);
}
break;
case YAML::NodeType::Null:
std::print("{}(null)\n", indent);
break;
case YAML::NodeType::Undefined:
std::print("{}(undefined)\n", indent);
break;
}
}Ce type de fonction est utile pour le débogage, pour construire des outils d'inspection de fichiers YAML, ou pour implémenter des transformations génériques.
YAML permet de définir une ancre (&nom) sur un nœud et de le référencer ailleurs (*nom). yaml-cpp résout automatiquement ces références lors du chargement — le code de navigation n'a pas à s'en préoccuper :
# config.yaml
defaults: &defaults
timeout: 30
retries: 3
circuit_breaker:
threshold: 5
reset_after: 60
services:
api: *defaults # api hérite de tout le contenu de defaults
worker: *defaults # idem pour workerYAML::Node config = YAML::LoadFile("config.yaml");
// Les ancres sont résolues transparentement
int api_timeout = config["services"]["api"]["timeout"].as<int>();
// api_timeout == 30 (hérité du défaut)
int worker_retries = config["services"]["worker"]["retries"].as<int>();
// worker_retries == 3 (hérité du défaut)L'ancre *defaults assigne le nœud complet référencé par &defaults. C'est une copie : les deux nœuds partagent les mêmes valeurs.
La clé de merge << est une extension YAML 1.1 qui permet de fusionner les clés d'un nœud référencé dans le nœud courant, tout en autorisant les overrides locaux :
# ⚠️ Le merge key << n'est PAS résolu par yaml-cpp 0.8.0
services:
api:
<<: *defaults
host: api.internal
timeout: 60 # override du défautAvec yaml-cpp 0.8.0, la clé << est traitée comme une clé littérale ordinaire — les champs du nœud référencé ne sont pas fusionnés dans le nœud parent. config["services"]["api"]["timeout"] ne sera pas défini (le nœud sera Undefined). C'est un changement par rapport aux versions antérieures de yaml-cpp (0.6.x, 0.7.x) qui résolvaient le merge key.
Pour obtenir un comportement équivalent au merge key, il faut implémenter la fusion manuellement :
// Fusionner les clés d'un nœud source dans un nœud destination
void merge_nodes(YAML::Node& dest, const YAML::Node& source) {
if (!source.IsMap()) return;
for (const auto& kv : source) {
std::string key = kv.first.as<std::string>();
if (!dest[key].IsDefined()) {
dest[key] = kv.second;
}
}
}
⚠️ Les ancres simples (*nom) sont fiables et portables. Le merge key (<<) est une extension YAML 1.1 dont le support varie selon les parsers et les versions. Pour les nouveaux projets, préférer la duplication explicite ou les ancres simples au merge key.
Comme décrit en section 24.2, YAML interprète certains scalaires non quotés de manière surprenante. Dans le code C++, la meilleure défense est de toujours extraire avec le type attendu et de ne jamais supposer le type résolu par le parser :
YAML::Node config = YAML::Load(R"(
country: NO
version: 1.0
permissions: 0755
)");
// ❌ Dangereux : le type résolu par YAML peut surprendre
// En YAML 1.1, config["country"] peut être interprété comme booléen false
// .as<std::string>() retournerait alors "false" au lieu de "NO" !
std::string country = config["country"].as<std::string>();
// country == "false" ← bug silencieux !
// ✅ Défensif : documenter que les valeurs ambiguës doivent être quotées
// dans le fichier YAML : country: "NO"
// Ou vérifier la valeur après extractionLa solution définitive à ce problème se situe côté fichier YAML : quoter systématiquement les valeurs qui pourraient être ambiguës. C'est une bonne pratique à documenter pour les utilisateurs du fichier de configuration :
# Bonnes pratiques : quoter les valeurs potentiellement ambiguës
country: "NO" # chaîne, pas booléen
version: "1.0" # chaîne, pas float
permissions: "0755" # chaîne, pas octal
enabled: true # booléen intentionnel, pas de guillemets Un fichier YAML syntaxiquement valide peut avoir une structure incorrecte pour l'application. Une couche de validation entre le chargement et la conversion est recommandée pour les configurations critiques :
struct ConfigValidation {
bool valid = true;
std::vector<std::string> errors;
void require_key(const YAML::Node& parent, const std::string& key,
const std::string& context) {
if (!parent[key] || parent[key].IsNull()) {
valid = false;
errors.push_back(
std::format("Champ obligatoire '{}' manquant dans {}", key, context));
}
}
void require_type(const YAML::Node& node, const std::string& name,
YAML::NodeType::value expected) {
if (!node.IsDefined()) return; // absence traitée par require_key
bool type_ok = false;
std::string expected_name;
switch (expected) {
case YAML::NodeType::Scalar:
type_ok = node.IsScalar();
expected_name = "scalaire";
break;
case YAML::NodeType::Sequence:
type_ok = node.IsSequence();
expected_name = "séquence";
break;
case YAML::NodeType::Map:
type_ok = node.IsMap();
expected_name = "map";
break;
default: break;
}
if (!type_ok) {
valid = false;
errors.push_back(
std::format("'{}' doit être de type {} (reçu: {})",
name, expected_name, static_cast<int>(node.Type())));
}
}
};
ConfigValidation validate_config(const YAML::Node& root) {
ConfigValidation v;
v.require_key(root, "server", "racine");
if (root["server"]) {
const auto& server = root["server"];
v.require_type(server, "server", YAML::NodeType::Map);
v.require_key(server, "host", "server");
v.require_key(server, "port", "server");
// Validation de plage
if (server["port"] && server["port"].IsScalar()) {
try {
int port = server["port"].as<int>();
if (port < 1 || port > 65535) {
v.valid = false;
v.errors.push_back(
std::format("server.port doit être entre 1 et 65535 (reçu: {})",
port));
}
} catch (const YAML::BadConversion&) {
v.valid = false;
v.errors.push_back("server.port doit être un entier");
}
}
}
return v;
}Par défaut, les clés YAML qui ne correspondent à aucun champ de la structure C++ sont silencieusement ignorées. Cela peut masquer des fautes de frappe (tieout au lieu de timeout). Pour les détecter, on compare les clés du nœud YAML aux clés attendues :
void warn_unknown_keys(const YAML::Node& node,
const std::vector<std::string>& known_keys,
const std::string& context) {
if (!node.IsMap()) return;
for (const auto& kv : node) {
std::string key = kv.first.as<std::string>();
bool found = false;
for (const auto& known : known_keys) {
if (key == known) { found = true; break; }
}
if (!found) {
std::print(stderr, "⚠ Clé inconnue '{}' dans {} (ignorée)\n",
key, context);
}
}
}
// Utilisation
warn_unknown_keys(config["server"],
{"host", "port", "workers", "tls", "allowed_origins"},
"server");Ce contrôle est particulièrement utile en phase de développement et peut être activé conditionnellement en mode strict ou debug.
En combinant les techniques de cette section, voici le pattern recommandé pour charger une configuration YAML en production :
#include <yaml-cpp/yaml.h>
#include <filesystem>
#include <optional>
#include <print>
namespace fs = std::filesystem;
std::optional<ServerConfig> load_config(const fs::path& path) {
// 1. Vérification du fichier
if (!fs::exists(path)) {
std::print(stderr, "Configuration introuvable : {}\n", path.string());
return std::nullopt;
}
// 2. Chargement YAML
YAML::Node root;
try {
root = YAML::LoadFile(path.string());
} catch (const YAML::ParserException& e) {
std::print(stderr, "{}:{}:{}: erreur de syntaxe YAML\n {}\n",
path.string(), e.mark.line + 1, e.mark.column + 1, e.what());
return std::nullopt;
} catch (const YAML::BadFile& e) {
std::print(stderr, "Impossible de lire {} : {}\n",
path.string(), e.what());
return std::nullopt;
}
// 3. Validation structurelle
auto validation = validate_config(root);
if (!validation.valid) {
std::print(stderr, "Configuration invalide ({}) :\n", path.string());
for (const auto& err : validation.errors) {
std::print(stderr, " - {}\n", err);
}
return std::nullopt;
}
// 4. Conversion vers le type métier
try {
return root["server"].as<ServerConfig>();
} catch (const YAML::Exception& e) {
std::print(stderr, "Erreur de conversion : {}\n", e.what());
return std::nullopt;
}
}// Point d'entrée
int main(int argc, char* argv[]) {
fs::path config_path = (argc > 1) ? argv[1] : "config.yaml";
auto config = load_config(config_path);
if (!config) {
return 1;
}
std::print("Serveur configuré : {}:{}\n", config->host, config->port);
// ...
}Le pipeline suit la même logique que celui présenté en section 24.1.4 pour JSON : existence du fichier → parsing syntaxique → validation structurelle → conversion typée. Chaque étape produit un diagnostic adapté, et un échec à n'importe quelle étape retourne std::nullopt sans crash.
| Fonction | Source | Multi-doc | Exception en cas d'erreur |
|---|---|---|---|
YAML::LoadFile(path) |
Fichier | Non (1er doc) | BadFile, ParserException |
YAML::Load(string) |
Chaîne | Non (1er doc) | ParserException |
YAML::LoadAllFromFile(path) |
Fichier | Oui (tous) | BadFile, ParserException |
YAML::LoadAll(string) |
Chaîne | Oui (tous) | ParserException |
La section 24.2.2 abordera l'opération inverse : la génération de fichiers YAML depuis des structures C++ avec YAML::Emitter.