为什么 const YAML::Node 对象在 yaml-cpp 中表现得不像类值对象?
Why do const YAML::Node objects not behave like value-like objects with yaml-cpp?
我正在尝试使用 yaml-cpp 创建一个 YamlConfig class。它的一个主要特点是,在 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;
}
此代码在编译时会产生以下输出,但没有我的评论:
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
快速浏览 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[]
return 是一个值,不是引用,所以你不能使用指针;并且没有 find
函数可以 return 一个可以使用的迭代器。
一个公认的可怕的解决方法是:
auto tmp = current[keySegment]; // get next node
current.~Node(); // destruct node reference (not the referenced node)
new (¤t) Node(tmp); // call copy constructor with placement new to assign
// tmp to current. necessary since current is invalid at this point.
或者,您可以递归实现 seek
以避免重新分配 current
。
我正在尝试使用 yaml-cpp 创建一个 YamlConfig class。它的一个主要特点是,在 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;
}
此代码在编译时会产生以下输出,但没有我的评论:
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
快速浏览 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[]
return 是一个值,不是引用,所以你不能使用指针;并且没有 find
函数可以 return 一个可以使用的迭代器。
一个公认的可怕的解决方法是:
auto tmp = current[keySegment]; // get next node
current.~Node(); // destruct node reference (not the referenced node)
new (¤t) Node(tmp); // call copy constructor with placement new to assign
// tmp to current. necessary since current is invalid at this point.
或者,您可以递归实现 seek
以避免重新分配 current
。