如何使用不同类型的 std::initializer_list 构造函数来处理嵌套的支撑初始值设定项列表

How to use std::initializer_list constructor with different types to handle nested braced initializer lists

提问人:TheMemeMachine 提问时间:6/4/2023 更新时间:7/13/2023 访问量:138

问:

我正在查看 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_liststd::initializer_list

确定初始值设定项列表是否表示 OR 的条件应如下所示:JSONArrayJSONMap

如果列表中的每个嵌套元素本身都是一个长度为 2 的数组,其中第一个元素的类型可用于构造一个(我正在考虑使用类似的东西),而第二个元素是可以用来构造一个 ,那么我们可以推断出整个初始值设定项列表表示一个, 否则,我们将其视为JSONStringstd::is_constructible_v<JSONString, T>JSONObjectJSONMapJSONAarray

最后,我希望最终得到如下所示的代码:

#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
}

但即使这样,我也在处理嵌套支撑的初始值设定项列表时遇到了问题

json c++17 variadic-templates 初始值设定项列表

评论


答:

2赞 RandomBits 6/4/2023 #1

A 在类型方面是同质的 -- 所有元素都属于同一类型。诀窍是创建一个可以保存不同值的单一类型。这就是发挥作用的地方。std::initializer_liststd::variant

使用时的棘手之处在于它不是递归的,即它不能保存自己类型的值。有一些递归的变体实现,例如 rva::variantBooststd::variant

另外,如果你想了解细节,这里有一个很好的教程,介绍如何实现递归变体类型。

更新

以下代码是使用 for a json like 类型的粗略草图。该原型允许使用自然的初始化语法,就像 json 一样。它使用相同的技术来区分 obect 和 array。rva::variantnlohmann

示例代码

#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 } ]

评论

0赞 TheMemeMachine 6/4/2023
谢谢你的回答。但是,我仍然不明白使用这个新变体将如何解决我的问题。您能解释一下在这种情况下应该如何实现构造函数吗?initializer_list
0赞 TheMemeMachine 6/11/2023
非常感谢您的更新。但是,您展示的示例并不能完全回答我的问题。看看我用你的代码制作的这个工作示例(链接)。当我尝试使用嵌套的支撑初始值设定项列表进行初始化时,编译仍然失败
1赞 Weijun Zhou 7/13/2023
@TheMemeMachine 它失败,因为变体类型中没有。更改为链接将使代码按预期编译和工作。int22.0
1赞 Joel Croteau 7/13/2023 #2

支持初始化语法(如 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上的重载以及递归构造使嵌套语法能够很好地工作。

评论

0赞 TheMemeMachine 7/13/2023
你能提供一个工作的例子吗?我对您在第一个代码片段中编写构造函数的方式感到困惑。对于数组,您可以指定字符串,但数组可以保存任何其他 JSON 类型,因此它应该类似于 .std::initializer_list<JSON>