提问人:TheMemeMachine 提问时间:6/4/2023 更新时间:7/13/2023 访问量:138
如何使用不同类型的 std::initializer_list 构造函数来处理嵌套的支撑初始值设定项列表
How to use std::initializer_list constructor with different types to handle nested braced initializer lists
问:
我正在查看 nlohmann json 库,我看到作者可以像这样构造 json 对象:
json j2 = {
{"pi", 3.141},
{"happy", true},
{"name", "Niels"},
{"nothing", nullptr},
{"answer", {
{"everything", 42}
}},
{"list", {1, 0, 2}},
{"object", {
{"currency", "USD"},
{"value", 42.99}
}}
};
在示例下方,他陈述了以下内容:
请注意,在所有这些情况下,您永远不需要“告诉”编译器 要使用的 JSON 值类型。如果您想明确或 表示一些边缘情况,函数 json::array() 和 json::object() 将有所帮助:
我对此很感兴趣,并尝试实现我自己对这种行为的简化版本,但我没有成功。我无法让初始值设定项列表同时接受不同的类型。我还尝试分析 nlohmann 库的实际源代码,我看到他的 json 对象也有一个构造函数,该构造函数接受包含一些(据我所知)固定类型的构造函数,但我不明白这如何允许理解嵌套的支撑初始值设定项列表,如示例中所示。std::initializer_list
std::initializer_list
确定初始值设定项列表是否表示 OR 的条件应如下所示:JSONArray
JSONMap
如果列表中的每个嵌套元素本身都是一个长度为 2 的数组,其中第一个元素的类型可用于构造一个(我正在考虑使用类似的东西),而第二个元素是可以用来构造一个 ,那么我们可以推断出整个初始值设定项列表表示一个, 否则,我们将其视为JSONString
std::is_constructible_v<JSONString, T>
JSONObject
JSONMap
JSONAarray
最后,我希望最终得到如下所示的代码:
#include <iostream>
#include <vector>
#include <map>
#include <variant>
class JSONObject;
using JSONString = std::string;
using JSONNumber = double;
using JSONBool = bool;
using JSONNull = nullptr_t;
using JSONArray = std::vector<JSONObject>;
using JSONMap = std::map<std::string, JSONObject>;
class JSONObject {
public:
JSONObject() : var{JSONMap{}} {}
template <typename T>
JSONObject(std::initializer_list<T> list) {
// I do not understand how to implement this
}
private:
std::variant<JSONString, JSONNumber, JSONBool, JSONNull, JSONArray, JSONMap> var;
};
int main() {
JSONObject jsonObj = {
{"pi", 3.141},
{"happy", true},
{"name", "Niels"},
{"nothing", nullptr},
{"answer", {
{"everything", 42}
}},
{"list", {1, 0, 2}},
{"object", {
{"currency", "USD"},
{"value", 42.99}
}}
};
return 0;
}
在做一些研究时,我还想到了一个想法,可以像这样制作一个可变参数模板构造函数:JSONObject
template <typename... Args>
JSONObject(Args&&... args) {
// some fold expression to deduce how to construct the variant
}
但即使这样,我也在处理嵌套支撑的初始值设定项列表时遇到了问题
答:
A 在类型方面是同质的 -- 所有元素都属于同一类型。诀窍是创建一个可以保存不同值的单一类型。这就是发挥作用的地方。std::initializer_list
std::variant
使用时的棘手之处在于它不是递归的,即它不能保存自己类型的值。有一些递归的变体实现,例如 rva::variant 和 Boost。std::variant
另外,如果你想了解细节,这里有一个很好的教程,介绍如何实现递归变体类型。
更新
以下代码是使用 for a json like 类型的粗略草图。该原型允许使用自然的初始化语法,就像 json 一样。它使用相同的技术来区分 obect 和 array。rva::variant
nlohmann
示例代码
#include <iostream>
#include <map>
#include <string>
#include <vector>
#include "variant.hpp"
using std::cin, std::cout, std::endl;
using JsonBase = rva::variant<
std::nullptr_t,
std::string,
double,
bool,
std::map<std::string, rva::self_t>,
std::vector<rva::self_t>
>;
class JsonValue : public JsonBase {
public:
using JsonBase::JsonBase;
using InitializerList = std::initializer_list<JsonValue>;
JsonValue(InitializerList init) {
bool is_object = std::all_of(init.begin(), init.end(), [](const auto& value) {
if (std::holds_alternative<std::vector<JsonBase>>(value)) {
const auto& arr = std::get<std::vector<JsonBase>>(value);
return arr.size() == 2 and std::holds_alternative<std::string>(arr[0]);
}
return false;
});
if (is_object) {
std::map<std::string, JsonBase> m;
for (const auto& value : init) {
const auto& arr = std::get<std::vector<JsonBase>>(value);
const auto& key = std::get<std::string>(arr[0]);
m.emplace(key, arr[1]);
}
*this = m;
} else {
std::vector<JsonBase> vec;
for (auto&& value : init)
vec.emplace_back(value);
*this = vec;
}
}
};
std::ostream& operator<<(std::ostream& os, const JsonBase& value) {
if (std::holds_alternative<std::nullptr_t>(value))
os << "{ }";
else if (std::holds_alternative<std::string>(value))
os << "\"" << std::get<std::string>(value) << "\"";
else if (std::holds_alternative<double>(value))
os << std::get<double>(value);
else if (std::holds_alternative<bool>(value))
os << std::boolalpha << std::get<bool>(value);
else if (std::holds_alternative<std::map<std::string, JsonBase>>(value)) {
os << "{ ";
for (const auto& [key, value] : std::get<std::map<std::string, JsonBase>>(value))
os << "{ " << key << " : " << value << " } ";
os << "]";
}
else if (std::holds_alternative<std::vector<JsonBase>>(value)) {
os << "[ ";
for (const auto& elem : std::get<std::vector<JsonBase>>(value))
os << elem << " ";
os << "]";
}
return os;
}
int main(int argc, const char *argv[]) {
JsonValue str = "abc";
cout << str << endl;
JsonValue dbl = 1.0;
cout << dbl << endl;
JsonValue bol = true;
cout << bol << endl;
JsonValue vec = {
"abc", "def", 1.0, true
};
cout << vec << endl;
JsonValue m = {
{ "key0", 2.0 },
{ "key1", true }
};
cout << m << endl;
}
输出
"abc"
1
true
[ "abc" "def" 1 true ]
{ { key0 : 2 } { key1 : true } ]
评论
initializer_list
int
2
2.0
支持初始化语法(如 nlohmann 的 JSON 库)的关键在于重载构造函数以接受不同类型的初始值设定项列表。
对于最外层的初始值设定项列表,我们可以重载:std::initializer_list<T>
class JSON {
public:
JSON(std::initializer_list<std::string> init); // array
JSON(std::initializer_list<std::pair<std::string, JSON>> init); // object
};
这使我们能够区分值列表(数组)和键值对列表(对象)。
对于嵌套对象,我们可以递归调用 JSON 构造函数:
JSON(std::initializer_list<std::pair<std::string, JSON>> init) {
// construct object
}
关键是嵌套的 JSON 对象本身将从初始值设定项列表构造,最终在达到字符串或数字等基本值时终止。
可变参数模板方法也可以工作,但需要显式指定类型来指导重载解决:
template<typename... Args>
JSON(Args... args) {
construct(args...);
}
void construct(std::pair<std::string, JSON>& kv) {
// handle kv pair
}
void construct(std::string& s) {
// handle string
}
// etc
initializer_list上的重载以及递归构造使嵌套语法能够很好地工作。
评论
std::initializer_list<JSON>
评论