65.9K
CodeProject 正在变化。 阅读更多。
Home

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

starIconstarIconstarIconstarIconstarIcon

5.00/5 (1投票)

2019年5月26日

CPOL

8分钟阅读

viewsIcon

10135

downloadIcon

200

此代码允许使用 JSON 和 XML,以非侵入性的方式,通过试探性模板,将单个变量或完整的对象树进行转储和恢复。

主要目标

  1. 用几行代码在程序执行之间保留持久化的变量和对象值,让模板完成所有工作。
  2. 几乎不影响现有代码,还可以存储和恢复整个变量对象树。
  3. 使用已知格式(实际上是 JSON 和 XML),但易于扩展。
  4. 允许不完整的(或部分丢失的)配置文件。
  5. 了解 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.cjson.c 中使用)的可读性,ANSI C 解析器历来都很棘手。好消息是这些解析器非常简单,并且可以独立使用。此外,实现另一种格式支持(例如 YAML)也很简单。

未来

让 C++ 广泛用于 Web,并重用数百万高质量代码的一种方式,就是完成 C++ 接口,并融入从 Java 学到的东西。一旦“`#define`”从非平凡代码(以及 makefile)中消失(在我看来),我们就知道工作完成了。

历史

  • 2019年5月26日:初始版本
© . All rights reserved.