the anonymous
the anonymous

Reputation: 119

Manipulating nodes in yaml-cpp

I am currently working on a project that I would like to configure using the library yaml-cpp. Now I am trying to write a function which is given a vector of tags/strings (e.g., "A", "B", "C") and some parameter value (e.g., 31). The function should then insert a map node for each tag (if such a node does not already exist), map it to the subsequent node and finally create a scalar node with the parameter value. After the stated example call, the configuration should hence read

A:
  B:
    C: 31

Below you can see the attempt of a clean and simple implementation:

Config.hpp

class Config {

public:

    template<typename T>
    static void setConfigParam(const std::vector<std::string> &tags,
            T value) {
        assert(tags.size() > 0);
        assert(config);
        YAML::Node parentNode = config;
        for(int i=0; i < tags.size()-1; i++) {
            const std::string & tag = tags.at(i);
            if(parentNode[tag]) {
                YAML::Node childNode = parentNode[tag];
                parentNode = childNode;
            }
            else {
                YAML::Node childNode(YAML::NodeType::Map);
                parentNode[tag] = childNode;
                parentNode = childNode;
            }
        }
        parentNode[tags.back()] = value;
        std::cout << "Config:\n" << config << "\n\n";
    }

private:

    static YAML::Node config;

};

YAML::Node Config::config(YAML::NodeType::Map);

Test.cpp

#include "Config.hpp"

int main(int argc, char **argv) {
    int val1 = 3;
    vector<string> tags1 = {"A1","B1","C1","D1"};
    Config::setConfigParam(tags1, val1);
    string val2 = "test";
    vector<string> tags2 = {"A1","B1","C1","D2"};
    Config::setConfigParam(tags2, val2);
    bool val3 = true;
    vector<string> tags3 = {"A1","B1","C1"};
    Config::setConfigParam(tags3, val3);
    double val4 = 2.385;
    vector<string> tags4 = {"A1","B2","C1"};
    Config::setConfigParam(tags4, val4);
}

The problem of the function setConfigParam() seem to be the lines

YAML::Node parentNode = config;

and

parentNode = childNode

According to my understanding from the tutorial, the first line apparently creates the node parentNode as an alias for config. At each function call, the first execution of the second line then replaces the existing config object with the newly created childNode. However, in the remaining iterations the config is no longer being completely reset but adapted as expected.

If everything was running fine, the output should be:

Config:
A1:
  B1:
    C1:
      D1: 3

Config:
A1:
  B1:
    C1:
      D1: 3
      D2: test

Config:
A1:
  B1:
    C1: true

Config:
A1:
  B1:
    C1: true
  B2:
    C1: 2.385

The actual output of the code above is

Config:
B1:
  C1:
    D1: 3

Config:
B1:
  C1:
    D2: test

Config:
B1:
  C1: true

Config:
B2:
  C1: 2.385

I tried to prevent the mentioned issue by using pointers and finally managed to come up with the following implementation, which works correctly at least with the test code:

template<typename T>
static void setConfigParam(const std::vector<std::string> &tags,
        T value) {
    int numTags = tags.size();
    // Reads root node.
    assert(numTags > 0);
    assert(config);
    YAML::Node* parentNode = &config;
    // Determines index of first non-existing node.
    int i=0;
    for(; i < numTags-1; i++) {
        const std::string & tag = tags.at(i);
        if((*parentNode)[tag]) {
            auto childNode = (*parentNode)[tag];
            parentNode = &childNode;
        }
        else {
            break;
        }
    }
    // Note: necessary because *parentNode will later point
    // to a different node due to lib behavior .
    YAML::Node lastExistingNode = *parentNode;
    // Sets node value and creates missing nodes.
    if(i == numTags - 1) {
        lastExistingNode[tags.back()] = value;
    }
    else {
        YAML::Node newNode;
        newNode = value;
        for(int j = tags.size()-1; j>i; j--) {
            YAML::Node tmpNode;
            tmpNode[tags.at(j)] = newNode;
            newNode = tmpNode;
        }
        // Inserts missing nodes.
        lastExistingNode[tags.at(i)] = newNode;
    }
    std::cout << "Config:\n" << config << "\n\n";
}

However, I believe there must be a much simpler and cleaner solution. Hence, I am very thankful for any ideas on how to get the function to work correctly with a more simple implementation, in particular as there is not too much documentation on the library.

Upvotes: 2

Views: 4693

Answers (1)

Jesse Beder
Jesse Beder

Reputation: 34054

In your first example, assigning to an existing node has different behavior than you're intending. See this issue for more details.

Your example with pointers gets around this problem, but it has another problem: it dereferences a stack variable.

Instead, use a recursive solution:

template<typename T, typename Iter>
void setConfigParam(YAML::Node node, Iter begin, Iter end, T value) {
  if (begin == end) {
    return;
  }
  const auto& tag = *begin;
  if (std::next(begin) == end) {
    node[tag] = value;
    return;
  }
  if (!node[tag]) {
    node[tag] = YAML::Node(YAML::NodeType::Map);
  }
  setConfigParam(node[tag], std::next(begin), end, value);
}

Upvotes: 1

Related Questions