为现有的 C++ 代码提供 JSON 和 XML 序列化





5.00/5 (1投票)
此代码允许使用 JSON 和 XML,以非侵入性的方式,通过试探性模板,将单个变量或完整的对象树进行转储和恢复。
主要目标
- 用几行代码在程序执行之间保留持久化的变量和对象值,让模板完成所有工作。
- 几乎不影响现有代码,还可以存储和恢复整个变量对象树。
- 使用已知格式(实际上是 JSON 和 XML),但易于扩展。
- 允许不完整的(或部分丢失的)配置文件。
- 了解 C++ 标准的优点(尤其是试探性模板)和缺点,并弄清楚为什么我们有 Java,而不是解释型 C++。
一个小例子
这段小代码(01-counter.cc)展示了如何通过 C++ 中的模板和重载,仅用一个 `include` 和一行额外代码就能超越目标。
#include "keepconf.h" /// 1) Include the required templates
int counter;
//KEEPJSN( counter ); /// 2) This does the job ( JSON version )
KEEPXML( counter ); /// 2) This does the job ( XML version )
int main( int argc, char ** argv )
{ printf( "%s has been executed %d times"
, *argv, counter );
counter++; // Increase executions
return( counter );
}
首次执行时,我们得到文件 counter.jsn
{ "counter": 0
}
XML 版本是 counter.xml
<?xml version="1.0" ?>
<config style="rich">
<counter int="0"/>
选择的文件将反映执行次数。这很奇怪,因为它看起来更像是一个变量声明,而不是“本身”的代码,但它以“低熵”的方式完成了工作。正如你可能注意到的,XML 版本包含类型信息。这在后面的例子中很重要。
一堆变量
这是第二个例子(02-bunch.cc)。仅稍复杂一些,允许处理多个变量。
KEEPXMLLIST( bunch ) // Comment out that and comment in next line and for jsn version
// KEEPJSNLIST( bunch )
{ KEEPGLOB( counter );
KEEPGLOB( message );
KEEPGLOB( flag );
};
共享同一个配置文件
{ "bunch":
{ "counter": 1
, "message": "hello"
, "flag": 123
}
}
或者 XML 版本
<?xml version="1.0" ?>
<config style="rich">
<bunch class="TYPEbunch">
<counter int="1"/>
<message str="hello"/>
<flag byte="123"/>
</bunch>
你可以删除 JSON 或 XML 文件中的某个变量,下次运行程序时,删除的行会以默认值重新创建,所以我们具备部分默认值能力。
现在,一个对象
这组模板的真正实用性在于以非侵入性的方式为对象提供序列化。例如,对于一个尚未编码的名为 `PersintentExample` 的对象,此声明将为其实例(`objectExample`)提供持久性。我们不需要修改对象本身,只需声明序列化器。`KEEPITEM` 必须为对象中每个所需的持久化变量完成。
// Serializer declaration
//
KEEP_LOADER( PersintentExample )
{ KEEPITEM( aInteger );
KEEPITEM( aByte );
KEEPITEM( ourString );
KEEPITEM( anyType );
KEEPITEM( uninitialized );
}
KEEPXML( objectExample ); // This does the job ( xml version )
//KEEPJSN( objectExample ); // This does the job ( json version )
经验告诉你,从磁盘加载对象后,你可能需要初始化它。你可以通过在对象外部声明此项来完成:
/**
* A builder for the loaded objects can be added,
* but is optional
*/
void buildObject( PersintentExample & obj )
{ obj.doesNotMatter= 5;
fprintf( stderr
, "#\n"
"# %s has been built\n"
"#\n\n"
, typeId( obj ));
}
在这里,试探性模板的概念出现了。你只需不实现 `buildObject(PersintentExample & obj)`,就不会执行任何代码,也不会生成任何错误。对原始对象的唯一可能更改是声明此函数为 `friend`,以获取权限。运行程序 `03-object` 并查看 objectExample.xml(或 objectExample.jsn)以查看结果并进行尝试。
“即时”创建
`KEEPXML`(和 `KEEPJSN`)对对象进行“隐式”序列化,但我们也可以使用 `SAVEXLM` 和 `LOADXML`(以及它们的 JSON 版本)“手动”进行此持久化。04-new.cc 展示了如何做到这一点。这是此代码的另一个重要功能,它不仅填充已分配的变量,还会像“对象工厂”一样创建它们。此示例展示了如何处理这种情况,根据磁盘文件中对象是否存在采取不同操作。
任意对象列表
这是前一个例子的一个小升级。05-list.cc 能够“树状序列化”对象集合,从磁盘创建和填充其组件。接下来,我们将讨论如何到达下一个对象(在序列化输出时完成)以及如何将创建的对象添加到集合中。这是通过对象外部的函数 `nextObject` 完成的。它用于报告下一个(当 `toAdd` 为 NULL 时)和添加到集合的函数(当 `toAdd` 为 !NULL 时)。我使用了同一个函数来保持接口的简洁。
friend Aemet * nextObject( Aemet * hld
, Aemet * toAdd ) ;
list.xml(或 list.jsn)显示了序列化树的结果。你可以尝试修改它,并在主程序中查看结果。
任意对象的任意列表
这是迄今为止最高级的例子。它允许你在磁盘上存储任意对象的树。将此功能添加到现有对象的方法是,告知如何访问列表中的第一个对象和下一个对象,保存器使用这些信息来遍历树。这在遍历列表以保存它时使用。
friend type * type::nextObject( type * hld ) { return( hld->next ); }
friend type * type::firstObject( type * hld ) { return( hld ); }
以及如何将最近加载的对象添加到列表中(从磁盘加载时使用)
void type::linkObject( type * hld )
{ hld->next= next; next= hld;
}
尽管此代码仅依赖于模板且未使用任何虚拟化,但运行时必须知道已加载的对象。在任意列表的情况下,隐含的对象必须是虚拟的。声明 `linkObject virtual` 可能就足够了,但对于 `virtual` 对象来说,目前还不是必需的。
最初在 TUI 的模板版本中创建和测试了此代码,用于将窗口和小部件列表流式传输到磁盘。这是截图
屏幕上的多态性项目以这种方式存储在模板代码移植版中。这得益于删除了原始代码中数千行代码。
JSON 比 XML 简单得多。它知道变量名,但不知道类型名和数组索引。这让你在此示例中无计可施,至少在基本规范的形式上是这样。
此外,并非所有编译器都完全公开了完整的虚拟对象列表。这使得 MSC++ 无法管理此示例。GCC 仅部分公开此列表,因此,你想公开的每个对象都需要额外的工作,即在并行的虚拟对象列表中注册它。例如:
// Register the class ListRec2 to allow its use and tell how to save its members
REGISTERCLASS( ListRec2 )
{ KEEPROOT( Common );
KEEPITEM( str2 );
KEEPITEM( ownInteger );
}
最后,一个数组
07-array.cc 维护一个简单的对象数组。我包含了这个例子是为了指出 JSON 的另一个我认为的弱点。如果你删除 XML 版本中的第 3 个元素,系统将正确地重新创建并分配丢失的元素,但 JSON 没有索引信息,因此数组将被错误地创建。
如何测试
源包捆绑了 autotools(`./configure` & `make`)、codeblocks(`cblocks`)和 Visual Studio 2010(vc2010)的构建系统,用于示例。旧版本的 C++ 不支持 `typeof` 或 `decltype`。虽然可以不使用它们进行开发,但声明会稍微复杂一些。
接口与实现。抱歉?
当我开始编程时,我认为一个算法的良好实现是至关重要的,这是最重要的。让我们写一个比原来快 1000 倍的函数体,一个糟糕的实现以后可以修复。我完全错了。创建软件的主要问题之一是版本噩梦。你可以随时改进一个糟糕的实现,而不会产生副作用,但接口的更改意味着你的旧代码将无法构建。
这段代码与实现有关。它完美地知道该做什么,并以简单的方式告诉编译器。这意味着你的代码可以被广泛的用户使用。想想 stl,现在是 C++ 的一部分。作为微小的一个例子,`include` 文件中缺少“*.h”是实现更改的结果。实现就是你将信息告诉外部代码或编译器。试探性模板提供了很多智能性,因此你可以定义一个在常规情况下使用的模板,而在特殊情况下可以忽略它,从而实现具有“可适应复杂性”的接口。
Java 的爆发:为什么?
现在我想反思之前的几点,因为我认为这才是 Java 成功的原因,也是这个库运作的核心。编译器传统上将人类语言翻译成数字。这使得在编译时所有对变量名的引用及其操作都消失了。问题是,随着时间的推移,对象名称在运行时进行操作一直是必不可少的。事实上,我们不应该去看那些程序员乞求“`const char * nameof()`”或类似东西的博客。这意味着需要一个连接人类语言和程序的桥梁。这一切在 Java 中都很自然,但在 C++ 中,传统上一直存在构建这座桥梁的阻力。有一个名为 `typeid` 的晦涩函数;它充当了变量类型的桥梁,但在其原始规范中并非如此。这是宝贵时间的浪费。在这里,乌龟(Java)超越了兔子(C++)。
另一个重要的特点是缺乏 makefile 或构建系统。我认为构建一段代码所需的所有必要内容(依赖项、库、编译器开关等)都应该放在源文件中。构建系统已被滥用,以至于它们成为了熵的来源。
Using the Code
尽管我正在使用它,而且它非常小,但仍有可能改进。输入和输出流被硬编码到磁盘,这根本没有必要。JSON 解析器测试不足,等等,所以我将提供存储库地址。
svn checkout https://svn.code.sf.net/p/unicodecs/code/keepconf
模板和事件导向编程(在 xml.c 和 json.c 中使用)的可读性,ANSI C 解析器历来都很棘手。好消息是这些解析器非常简单,并且可以独立使用。此外,实现另一种格式支持(例如 YAML)也很简单。
未来
让 C++ 广泛用于 Web,并重用数百万高质量代码的一种方式,就是完成 C++ 接口,并融入从 Java 学到的东西。一旦“`#define`”从非平凡代码(以及 makefile)中消失(在我看来),我们就知道工作完成了。
历史
- 2019年5月26日:初始版本