JSON
应该算是目前网络数据交换格式的事实标准,似乎没有那一种语言不存在支持这种数
据格式的库,C++也不例外。不过JSON
虽然和语言无关,但是它毕竟源于动态语言,所以
在C++中,很多JSON
库接口都不太自然。C++11
引入的universal initialization
和initializer list
让一个拥有自然的接口的JSON
库成为可能。这篇文章介绍的就是这样
一个库——JSON for Modern C++。我第一次见到这个库的时候有了使用动态语言的
感觉。
1 | json array = {"hello", 1, 2.5, false, true, {1, 2}}; |
你没有看错,上面写的真的是C++
的代码不是Python
。这篇文章会介绍关于这个神奇的
JSON
库的一些使用和实现上的细节,一起看看吧。
JSON 的基础知识
下面这段话来自JSON
的官方文档:
JSON建构于两种结构:
- “名称/值”对的集合(A collection of name/value pairs)。不同的语言中,它被理 解为对象(object),纪录(record),结构(struct),字典(dictionary),哈希 表(hash table),有键列表(keyed list),或者关联数组 (associative array) 。
- 值的有序列表(An ordered list of values)。在大部分语言中,它被理解为数组( array)。
另外一个比较重要的点是,JSON
中的值可以是下面几种:
1 | string, number, true, false, object, array, null |
结合上面的定义你会发现,这是一种递归的定义,比如数组是值的有序列表,而值又可以是 数组,所以这是一种无穷的结构。
JSON 值在静态语言中的表示 —— nlohmann::json
前面提到一个JSON
值可以是string, number, true, false, object, array, null
中的
任意一种,但是C++是一种静态类型语言,任何值都有它固定的类型,一个值不能既是int又
是double。
解决这个问题的关键点在于抽象,也就是《C++沉思录》中反复提到的一个概念——用类抽象
一个概念。nlohmann::json
这个类抽象的概念就是JSON
的值。也就是说从静态语言的角
度考虑问题,所有这些值都有同一个类型:nlohmann::json
:
1 | json string_value = "The quick brown fox jumps over the lazy dog."; |
当然因为json
本身也是一个类型,所以我们可以声明一个存放json
的array和object。
1 | json json_array = { |
这样一来就变成了值的递归了,可以扩展成为一个无穷的结构。
需要注意的是,上面例子中显式的构造 json 对象是没有必要的,因为这些构造函数都不 是 explicit 构造函数,支持隐式转化,上面这样写只是为了方便问题的说明。
JSON 值的实际类型
当然无论我们如何抽象,它终究无法逃离C++作为一种静态类型语言的核心,我们可以让
json
类即表示number
又表示array
但是在底层的实现上我们终究无法让一个变量的类
型动态的改变,即便现在 C++11 支持 auto
也是如此。
1 | auto json_value = 0; |
上面这种语法在目前的 C++ 中是不合法的。在JSON for Modern C++的底层实现上
,它为JSON
中的每一种value
类型都设定了对应的C++类型,其中默认值如下:
1 | - object: std::map |
注意上面的 number
使用了三个不同的值是因为在JSON for Modern C++内部
number
其实被细分成了number_integer
、number_unsigned
、number_float
三种。
我们看到的nlohmann::json
这个类型实际上是一个别名:
1 | using json = basic_json<>; |
也就是说它实际上是模板类basic_json<>
使用默认的模板参数时实例化出来的类型。而
basic_json
这个模板类的声明如下:
1 | template < |
这个模板类有9个模板参数,其中前面7个就是用于表示JSON value
的实际类型。
ObjectType
和ArrayType
这两个模板参数的语法叫做模板的模板参数
也就是说,
ObjectType
这个模板参数匹配的实际是另一个模板,这个模板至少有U, V
两个模板参数
和Args
表示的其他可选模板参数,需要注意的是,U, V, Args
对于 basic_json
来说
是没有意义的,因为它 ObjectType
的模板参数,不是 basic_json
的模板参数,语法
上可以省略它们(这和函数的声明可以省略名字相通)。
实际上如果你愿意,你可以替换掉其中的一些类型,比如你想要用std::deque
表示
array
那么你可以定义你自己的类型JSON
类型如下:
1 | using MyJson = nlohmann::basic_json<std::map, std::deque>; |
不过目前来看,我们似乎没有办法使用std::unordered_map
作为ObjectType
,这好像是
一个库本身的问题std::unorderd_map cannot be used as ObjectType #164,
在 basic_json
中值的实际类型根据模板参数被定义成了下面这几种:
1 | using object_t = ObjectType<StringType, |
类型判断
nlohmann::json
提供了下列这些借口来判断内部实际的值是什么类型。
1 | is_primitive , is_structured , is_null , is_boolean , is_number , |
其中 is_structured
是指 is_object() or is_array()
,is_primitive
则是指
is_null() or is_string() or is_boolean() or is_number();
在 basic_json
内部有一个成员变量:m_type
,它的类型是一个枚举类:
1 | enum class value_t : uint8_t |
其中 discarded
这种类型只存在与解析的过程中,实际的 json
值不会是这种类型。
值的存储
在 basic_json
定义了一个成员变量m_value
,用于存储实际的值,为了能够让
m_value
可以表示多种类型的数据,它的类型被定义成了一个union
。
1 | union json_value |
我们都知道 union
的size
是由size
最大的那个成员变量决定的,为了能够节省空间
,object
, array
, string
这三种类型的之实际上是使用指针来存储的。
类型的自动转换
如果单说JSON
值的抽象,一个中等水平的程序员都可以做到,JSON for Modern
C++真正厉害的地方在于它的自动转换,正是这些自动转换让我们觉得它的API自然
而顺手。
C++ 中的自动类型转换
先说一下自动类型转换,在C++中,类型之间的自动转换分为两种:
非
explicit
的单参数构造函数(逻辑上单参即可,多参数但是后面都有默认参数也 可以),它可以用于把其他类型自动转换成本类型,比如我们最常见的const char*
到std::string
转换。1
std::string hello = "hello";
operator Type()
操作符,这种函数可以用于把本类型自动转换成Type
类型。比如下 面这个例子:1
2
3
4
5
6
7
8
9
10
11
12
13
14class Widget {
public:
Widget(const std::string& name) : name_(name) {}
operator std::string() {
return name_;
}
private:
std::string name_;
};
Widget widget = "btn";
std::string btn = widget;
数据自动转换为 nlohmann::json
如前所述,这是通过非 explicit
单参构造函数实现的,basic_json
定义了很多这一
类的构造函数,其中最强大的一个莫过于下面这个:
1 | template<typename CompatibleType, typename U = detail::uncvref_t<CompatibleType>, |
这个构造函数体现了作者极强的模板功底,让人叹服,如果去掉各种细节实际上这个函数调
用了JSONSerializer<U>::to_json
来完成构造。JSONSerializer
是basic_json
的一个
模板参数,默认情况下是adl_serializer
,ADL
是一个非常重要的C++术语,指的是通过
参数的命名空间查看函数的。
对于 nlohmann::json
指定的值类型(或者可以自动转换为指定的类型),都会有一个
to_json
函数的重载,比如对于bool
有如下定义:
1 | template<typename BasicJsonType, typename T, enable_if_t< |
更厉害的是,对于那些不是指定的类型(std::map, std::vector, std::string 等),它内
部定义了另一个匿名名字空间中的全局变量to_json
1 | constexpr const auto& to_json = detail::static_const<detail::to_json_fn>::value; |
去掉各种细节,最终他实际上是to_json_fn
这个类的一个变量,这个类重载了函数调用操
作符。
1 | struct to_json_fn |
这个类的设计也是令人拍案叫絕的,首先call
这个函数的优先级分派上priority_tag
的
设计非常精妙:
1 | template<unsigned N> struct priority_tag : priority_tag < N - 1 > {}; |
因为子类可以自动转换为父类,所以匹配上priority_tag<1>
优先级高,但是如果不成功
会有自动调用priority_tag<0>
,这里再通过 static_assert 在编译期间给出可读的错误
信息,实在让人叹为观止。
当然这些都不是重点,第一个 call
函数会通过 ADL
查找到位于 T 类型同一
namespace
下面的 to_json
函数。也就是说用户定义的任何类型,只要在同一
namespace
下面实现to_json
就可以自动转化为 basic_json
。每次读到这段代码都会
有一种膜拜之情油然而生,太牛逼了!
1 | namespace ns { |
有了上面这些构造函数才使得下面这些语句合法合法。
1 | json number = 1; |
上面这个构造函数基本上是通吃所有值类型的,但是有两个除外,null
和 object
,
basic_json
有一个专门处理null
的构造函数如下:
1 | basic_json(std::nullptr_t = nullptr) noexcept; |
对于object
则主要是修正问题,正常情况下,在下面这种情况:
1 | json value = { |
会默认转换成一个array
,因为 value 的初始化值其实是一个
std::initializer_list<basic_json>
,其中每一个element
都是一个array
,但是对于
上面这种情况实际上应该解析成一个object
才对。为了修正这个问题,basic_json
还提
供了另一个构造函数:
1 | basic_json(std::initializer_list<basic_json> init, |
可以看出默认情况下,之前的代码会从 array
修正到 object
。当然如果你的本意确实
是创建一个array
的话可以使用静态成员函数:json::array
1 | static basic_json array(std::initializer_list<basic_json> init = |
此外,为了接口的对称性,还存在一个静态方法json::object
用于创建一个对象。
nlohmann::json
到实际数据类型的自动转换
反方向的转换在JSON for Modern C++中同样存在,这个转换主要是通过下面这个 函数来实现的:
1 | template < typename ValueType, typename std::enable_if < ...... , int >::type = 0 > |
这个函数存在让下面这样的语法变得合法:
1 | nlohmann::json json = { |
它实际上上的实现和 to_json
类似,不过使用的是from_json
。
从另一个角度看待 nlohmann::json
—— 容器
其实在设计上来说,nlohmann::json
被设计成了一个容器,你可以想象std::map
,
std::vector
拥有的那些API,nlohmann::json
都存在。但是 nlohmann::json
除了表
示 object
和 vector
之外还表示 number
,true
,false
这些值。对于这些类
型的值来说,容器相关的那些 API 实际上没有真正的意义。从下面这张图可以看出这些相
关的 API 的具体实现情况:
需要注意的地方
容器的 API 有 STL 基础的人基本上都非常熟悉,这里不再赘述,有几个和普通容器不对等 的地方需要大家注意:
array
的 index 如果超过大小 operator[],不会出错,中间缺失的那些index会默认 创建成空的json
对象。比如:1
2
3
4
5
6json arr = json::array();
arr[3] = "hello world";
arr[5] = 42;
std::cout << arr << std::endl;最终得到的输出是
[null,null,null,"hello world",null,42]
array
的operator[]
只能用sizet_t
调用(包括那些可以默认转换的),而object
的operator[]
只能用std::string
调用(也包括那些可以默认转换的 )。否则会出现运行时异常。这种错误目前似乎无法在编译期间检查出来。
basic_json::value
basic_json
除了提供容器常用的接口 operator[]
和 at
之外,还提供了 value
成员函数用于取对象中的值,当值不存在的时候提供默认值。这个方法和 Python
中的
or 很像。
1 | retun x or "default" |
1 | json j = { |
这个函数对于处理可选参数非常有用。
吐嘈
这个库非常强大,实现上也非常的精巧,但是有时候我会觉得,如果把 JSON
的值和
JSON
这两个概念分开表示可能结构上会清晰一些,让 json
单纯是个容器。当然这都
是感觉上的东西,实际上作者为什么没有这样做可能有他自己其他方面考虑。