【问题标题】:Why do const YAML::Node objects not behave like value-like objects with yaml-cpp?为什么 const YAML::Node 对象的行为不像带有 yaml-cpp 的类值对象?
【发布时间】:2019-11-28 20:24:40
【问题描述】:

我正在尝试使用 yaml-cpp 创建一个 YamlConfig 类。它的一个主要特点是,在 Bukkit 的风格中,一个 Minecaft API,它的用户可以通过一个字符串轻松引用地图树中的不同节点(例如,包含地图的地图,其中包含地图,但深度不同) “map1.map2.map3.keyoffinalvalue”。我在下面的最小示例中编写了 seek 函数来执行此操作,但即使它被标记为 const,每次调用它时打印出来的字符串也是不同的,并且似乎只是包含上一个最终值的映射调用。这表明 m_rootNode 似乎正在发生变化的问题。怎么回事?

最初,这个函数不是 const (调试后我需要将其设为非 const),我认为由于可怕的 API 设计,YAML::Node 的行为就像某种参考而不是行为良好的参考C++ 中标准的类值类型(令 API 的用户惊讶的是,API 设计通常是可怕的)。但是,这与标记为 const 的函数不一致。因此,我现在不知道发生了什么。我也尝试通过我的搜索引擎找到类似的问题,但除了是同一个 YAML 库的一部分之外,没有什么是远程相关的。

#include <yaml-cpp/yaml.h>
#include <string>
#include <string_view>
#include <vector>
#include <iostream>

class YamlConfig{
public:
    YamlConfig(const std::string &yaml);
    YAML::Node seek(std::string_view key, bool create) const;
private:
    YAML::Node m_rootNode;

    static std::vector<std::string> split(std::string_view input, char delimeter);
};

YamlConfig::YamlConfig(const std::string &yaml){
    m_rootNode = YAML::Load(yaml);
}

YAML::Node YamlConfig::seek(std::string_view key, bool create) const {
    auto splitKey = split(key, '.');
    YAML::Node current = m_rootNode;

    YAML::Emitter emitter;
    emitter << current;
    std::cout << emitter.c_str() << std::endl;

    for(const auto &keySegment : splitKey){
        if(current.IsMap()){
            current = current[keySegment];
            if( (!current) && (!create) ){
                throw std::runtime_error("Invalid YAML key due to attempting to descend in to non-existent node: " + keySegment);
            }
        }else{
            throw std::runtime_error("Invalid YAML key due to attempting to descend in to non-map node: " + std::string(key));
        }
    }

    return current;
}

std::vector<std::string> YamlConfig::split(std::string_view input, char delimeter) {
    std::vector<std::string> output;
    auto baseit = input.begin();
    for(auto it=input.begin();it!=input.end();++it){
        if(*it == delimeter){
            output.emplace_back(baseit, it);
            baseit = it+1;
            if(*baseit == delimeter){
                throw std::invalid_argument("Double delimiter found in string \"" + std::string(input) + "\"");
            }
        }
    }
    output.emplace_back(baseit, input.end());

    return output;
}

int main(){
    const std::string yaml = "engine:\n    view-distance: 16\n    fullscreen: false\n";

    std::cout << yaml << std::endl;

    YamlConfig yamlConfig(yaml);
    std::cout << yamlConfig.seek("engine.view-distance", false).as<std::string>() << std::endl;
    std::cout << yamlConfig.seek("engine.view-distance", false).as<std::string>() << std::endl;

    return 0;
}

此代码在编译时会在没有我的 cmets 的情况下产生以下输出:

engine: //this is the printout of the string in main
    view-distance: 16
    fullscreen: false

engine: //this is the first printout of the root node, good
  view-distance: 16
  fullscreen: false
16 //this is the printout of the value that was retrieved from the yaml data
view-distance: 16 //This is the second printout of the "root" node. It looks like the root node is now the engine node, changed via a const function What is going on? 
fullscreen: false
terminate called after throwing an instance of 'std::runtime_error' //this is an artifact of the root node seemingly changing, and is consistent with it changing to be the engine node
  what():  Invalid YAML key due to attempting to descend in to non-existent node: engine
Aborted (core dumped)

编译命令: clang++ --std=c++17 -lyaml-cpp yaml.cpp -o yaml

【问题讨论】:

    标签: c++ yaml c++17 yaml-cpp


    【解决方案1】:

    快速浏览 API 会发现 these lines

    mutable detail::shared_memory_holder m_pMemory;
    mutable detail::node* m_pNode;
    

    mutable 修饰符告诉我们,即使是此节点上的 const 函数也可能会更改这些值。这是令人担忧的,但实际上不是问题。正如我们所见,YAML::Node 只是对实际节点的引用。进一步挖掘,我们发现了assignment operator的实现:

    inline Node& Node::operator=(const Node& rhs) {
     if (is(rhs))
        return *this;
      AssignNode(rhs);
      return *this;
    }
    
    /* snip */
    
    inline void Node::AssignNode(const Node& rhs) {
      if (!m_isValid)
        throw InvalidNode(m_invalidKey);
      rhs.EnsureNodeExists();
    
      if (!m_pNode) {
        m_pNode = rhs.m_pNode;
        m_pMemory = rhs.m_pMemory;
        return;
      }
    
      m_pNode->set_ref(*rhs.m_pNode);
      m_pMemory->merge(*rhs.m_pMemory);
      m_pNode = rhs.m_pNode;
    }
    

    如我们所见,分配YAML::Node修改引用的节点,这是您的问题。即使您的函数是 const 这也有效,因为您仍然可以从 const 指针修改引用的数据。

    问题是,API 应该如何使用?我真的不知道。 operator[] 返回一个值,而不是一个引用,所以你不能使用指针;并且没有 find 函数会返回一个可以使用的迭代器。

    一个,公认的可怕的解决方法是:

    auto tmp = current[keySegment]; // get next node
    current.~Node(); // destruct node reference (not the referenced node)
    new (&current) Node(tmp); // call copy constructor with placement new to assign 
        // tmp to current. necessary since current is invalid at this point.
    

    或者,您可以递归实现seek 以避免重新分配current

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2013-11-28
      • 1970-01-01
      • 1970-01-01
      • 2020-09-01
      • 2016-09-12
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多