yaml-cpp/docs/Tutorial.md
Benjamin Navarro 3be2e1b28f Allowing partial specialization of the convert struct
This is done by adding a second, defaulted, template parameter that can
be used in conjunction of std::enable_if

An example of usage is added to the tutorial
2021-10-21 12:48:50 +02:00

6.9 KiB

Introduction

A typical example, loading a configuration file, might look like this:

YAML::Node config = YAML::LoadFile("config.yaml");

if (config["lastLogin"]) {
  std::cout << "Last logged in: " << config["lastLogin"].as<DateTime>() << "\n";
}

const std::string username = config["username"].as<std::string>();
const std::string password = config["password"].as<std::string>();
login(username, password);
config["lastLogin"] = getCurrentDateTime();

std::ofstream fout("config.yaml");
fout << config;

Basic Parsing and Node Editing

All nodes in a YAML document (including the root) are represented by YAML::Node. You can check what kind it is:

YAML::Node node = YAML::Load("[1, 2, 3]");
assert(node.Type() == YAML::NodeType::Sequence);
assert(node.IsSequence());  // a shortcut!

Collection nodes (sequences and maps) act somewhat like STL vectors and maps:

YAML::Node primes = YAML::Load("[2, 3, 5, 7, 11]");
for (std::size_t i=0;i<primes.size();i++) {
  std::cout << primes[i].as<int>() << "\n";
}
// or:
for (YAML::const_iterator it=primes.begin();it!=primes.end();++it) {
  std::cout << it->as<int>() << "\n";
}

primes.push_back(13);
assert(primes.size() == 6);

and

YAML::Node lineup = YAML::Load("{1B: Prince Fielder, 2B: Rickie Weeks, LF: Ryan Braun}");
for(YAML::const_iterator it=lineup.begin();it!=lineup.end();++it) {
  std::cout << "Playing at " << it->first.as<std::string>() << " is " << it->second.as<std::string>() << "\n";
}

lineup["RF"] = "Corey Hart";
lineup["C"] = "Jonathan Lucroy";
assert(lineup.size() == 5);

Querying for keys does not create them automatically (this makes handling optional map entries very easy)

YAML::Node node = YAML::Load("{name: Brewers, city: Milwaukee}");
if (node["name"]) {
  std::cout << node["name"].as<std::string>() << "\n";
}
if (node["mascot"]) {
  std::cout << node["mascot"].as<std::string>() << "\n";
}
assert(node.size() == 2); // the previous call didn't create a node

If you're not sure what kind of data you're getting, you can query the type of a node:

switch (node.Type()) {
  case Null: // ...
  case Scalar: // ...
  case Sequence: // ...
  case Map: // ...
  case Undefined: // ...
}

or ask directly whether it's a particular type, e.g.:

if (node.IsSequence()) {
  // ...
}

Building Nodes

You can build YAML::Node from scratch:

YAML::Node node;  // starts out as null
node["key"] = "value";  // it now is a map node
node["seq"].push_back("first element");  // node["seq"] automatically becomes a sequence
node["seq"].push_back("second element");

node["mirror"] = node["seq"][0];  // this creates an alias
node["seq"][0] = "1st element";  // this also changes node["mirror"]
node["mirror"] = "element #1";  // and this changes node["seq"][0] - they're really the "same" node

node["self"] = node;  // you can even create self-aliases
node[node["mirror"]] = node["seq"];  // and strange loops :)

The above node is now:

&1
key: value
&2 seq: [&3 "element #1", second element]
mirror: *3
self: *1
*3 : *2

How Sequences Turn Into Maps

Sequences can be turned into maps by asking for non-integer keys. For example,

YAML::Node node  = YAML::Load("[1, 2, 3]");
node[1] = 5;  // still a sequence, [1, 5, 3]
node.push_back(-3) // still a sequence, [1, 5, 3, -3]
node["key"] = "value"; // now it's a map! {0: 1, 1: 5, 2: 3, 3: -3, key: value}

Indexing a sequence node by an index that's not in its range will usually turn it into a map, but if the index is one past the end of the sequence, then the sequence will grow by one to accommodate it. (That's the only exception to this rule.) For example,

YAML::Node node = YAML::Load("[1, 2, 3]");
node[3] = 4; // still a sequence, [1, 2, 3, 4]
node[10] = 10;  // now it's a map! {0: 1, 1: 2, 2: 3, 3: 4, 10: 10}

Converting To/From Native Data Types

Yaml-cpp has built-in conversion to and from most built-in data types, as well as std::vector, std::list, and std::map. The following examples demonstrate when those conversions are used:

YAML::Node node = YAML::Load("{pi: 3.14159, [0, 1]: integers}");

// this needs the conversion from Node to double
double pi = node["pi"].as<double>();

// this needs the conversion from double to Node
node["e"] = 2.71828;

// this needs the conversion from Node to std::vector<int> (*not* the other way around!)
std::vector<int> v;
v.push_back(0);
v.push_back(1);
std::string str = node[v].as<std::string>();

To use yaml-cpp with your own data types, you need to specialize the YAML::convert<> template class. For example, suppose you had a simple Vec3 class:

struct Vec3 { double x, y, z; /* etc - make sure you have overloaded operator== */ };

You could write

namespace YAML {
template<>
struct convert<Vec3> {
  static Node encode(const Vec3& rhs) {
    Node node;
    node.push_back(rhs.x);
    node.push_back(rhs.y);
    node.push_back(rhs.z);
    return node;
  }

  static bool decode(const Node& node, Vec3& rhs) {
    if(!node.IsSequence() || node.size() != 3) {
      return false;
    }

    rhs.x = node[0].as<double>();
    rhs.y = node[1].as<double>();
    rhs.z = node[2].as<double>();
    return true;
  }
};
}

Then you could use Vec3 wherever you could use any other type:

YAML::Node node = YAML::Load("start: [1, 3, 0]");
Vec3 v = node["start"].as<Vec3>();
node["end"] = Vec3(2, -1, 0);

Partial specialization

If you need to specialize the convert struct for a set of types instead of just one you can use partial specialization with the help of std::enable_if (SFINAE).

Here is a small example showing how to partially specialize the convert struct for all types deriving from a base class:

// Base class
class A
{
public:
  A() = default;
  A(int a) : a{a} {}

  // virtual load/emit methods
  virtual void load(const YAML::Node &node) {
    a = node["a"].as<int>();
  }

  virtual YAML::Node emit() const  {
    YAML::Node node;
    node["a"] = a;
    return node;
  }
  
  int a;
};

// Derived class
class B : public A
{
public:
  B() = default;
  B(int a, int b) : A{a}, b{b} {}

  // override virtual load/emit methods
  virtual void load(const YAML::Node &node) override {
    A::load(node);
    b = node["b"].as<int>();
  }

  virtual YAML::Node emit() const override {
    YAML::Node node = A::emit();
    node["b"] = b;
    return node;
  }

  int b;
};

// Implementation of convert::{encode,decode} for all classes derived from or being A
namespace YAML {
  template<typename T> 
  struct convert<T, typename std::enable_if<std::is_base_of<A, T>::value>::type> {
    static Node encode(const T &rhs) {
      Node node = rhs.emit();
      return node;
    }

    static bool decode(const Node &node, T &rhs) {
      rhs.load(node);
      return true;
    }
  };
}

Which can then be use like this:

YAML::Node node = YAML::Load("{a: 1, b: 2}");
B b = node.as<B>();
b.a = 12;
b.b = 42;
node = b;