现在的位置: 首页 > 综合 > 正文

撤销和重做(Undo和Redo)的C++完美实现(9)

2013年10月28日 ⁄ 综合 ⁄ 共 14777字 ⁄ 字号 评论关闭
#if 0

在这一章里面将会讨论到该撤销和重做库的序列化问题。序列化几乎是每一个应用程
序中都会考虑到的问题,在本系列文档的开始就已经提到了该撤销和重做库天生具备了序
列化能力,那么在本文中将会详细的讨论序列化的问题。并且尽可能的给出非常具有弹性
的序列化框架。

为了保证该序列化框架的弹性,我将该序列化框架分为三大部分:

(1)档案类,专门负责档案格式的

(2)序列化类,专门负责序列化框架,将一些固有的流程采用自动化方法实现

为了保证该序列化框架的易用性,必须声明一声:该序列化框架是专门针对该撤销和
重做库的。所以对于该撤销和重做库进行了大量的自动化实现,包括简单类型的序列化的
自动化实现,复合类型的序列化的自动化实现和控制类的序列化的自动化实现。是否需要
序列化功能,仅仅添加一个类还是删除一个类的问题。所以使用起来非常的方便。

#endif
#ifdef ARCHIVE_H//档案类的实现
struct text_tag;//文本档案标签
struct binary_tag;//二进制档案标签
//支持文本格式的档案
template <class StreamType,class TAG>struct archive;
template <class StreamType>struct archive<StreamType,text_tag>
{
archive(StreamType&S):_S(S){}
template <class T> archive&write(const T&O)
{
_S << " " << O << " ";
return *this;
}
template <class T> archive&read(T&O)
{
_S >> O;
return *this;
}
//流不允许出现const类型,也就是说所有的流都是可以改动的
operator StreamType&(){return _S;}
private:
StreamType&_S;
};
//支持二进制格式的档案
template <class StreamType>struct archive<StreamType,binary_tag>
{
archive(StreamType&S):_S(S){}
template <class T> archive&write(const T&o)
{
_S.write(reinterpret_cast<const char*>(&o),sizeof(T));
return *this;
}
template <class T> archive&read(T&o)
{
_S.read(reinterpret_cast<char*>(&o),sizeof(T));
return *this;
}
//流不允许出现const类型,也就是说所有的流都是可以改动的
operator StreamType&(){return _S;}
private:
StreamType&_S;
};
//任何其它的格式都必须有上面的write函数和read函数,下面给出一个可扩展的档案格式
//类代码模板
//template <class StreamType>struct archive<StreamType,other_tag>
//{
// archive(StreamType&S):_S(S){}
// template <class T> archive&write(const T&o)
// {
// //在这里实现你自己的序列化写出功能
// return *this;
// }
// template <class T> archive&read(T&o)
// {
// //在这里实现你自己的序列化读取功能
// return *this;
// }
// //流不允许出现const类型,也就是说所有的流都是可以改动的
// operator StreamType&(){return _S;}
//private:
// StreamType&_S;
//};
#endif//ARCHIVE_H
#ifdef SERIALIZE_H//序列化类的实现
#include "language.h"//LOOP
#include "traits.h"//traits
#include "tuple.h"//tuple
#include "control.h"
namespace xcl=pandaxcl;
//有时候并不需要将控制类中的所有类型都序列化出来,这里给出一个类型串来选择在控
//制类中的需要序列化的类型
template <class T>struct serialize;
//针对于tuple类型的特化,tuple类型可以作为简单类型使用,也就是针对于简单类型的
//特化
template <class ConsType>struct serialize<xcl::tuple<ConsType> >
{
typedef ConsType cons_type;
typedef xcl::tuple<ConsType> target_type;
//下面的两个函数是任何类型都必须遵守的规范,必须提供这两个函数
template <class ArchiveType,class ControlType>
static ArchiveType&write(ArchiveType&AR,const ControlType&C,const target_type&O)
{
const int Length = xcl::length<cons_type>::value;
WRITE_ENVIRONMENT<ArchiveType,ControlType> e(AR,C,O);
xcl::LOOP<WRITE,0,Length>::execute(e);
}
template <class ArchiveType,class ControlType>
static ArchiveType&read(ArchiveType&AR,ControlType&C,target_type&O)
{
const int Length = xcl::length<cons_type>::value;
READ_ENVIRONMENT<ArchiveType,ControlType> e(AR,C,O);
xcl::LOOP<READ,0,Length>::execute(e);
}
private:
template <class ArchiveType,class ControlType>
struct WRITE_ENVIRONMENT
{
WRITE_ENVIRONMENT(ArchiveType&AR,const ControlType&C,const target_type&O)
:_AR(AR),_C(C),_O(O){}
ArchiveType &_AR;
const ControlType &_C;//常量
const target_type &_O;//常量
};
template <class ArchiveType,class ControlType>
struct READ_ENVIRONMENT
{
READ_ENVIRONMENT(ArchiveType&AR,ControlType&C,target_type&O)
:_AR(AR),_C(C),_O(O){}
ArchiveType &_AR;
ControlType &_C;//变量
target_type &_O;//变量
};
template<int i> struct WRITE
{
template <class EnvironmentType>
static void execute(EnvironmentType&e)
{
//你的代码在这里编写
e._AR.write(xcl::field<i>(e._O));
}
};
template<int i> struct READ
{
template <class EnvironmentType>
static void execute(EnvironmentType&e)
{
//你的代码在这里编写
e._AR.read(xcl::field<i>(e._O));
}
};
};
//针对于复合类型的特化
template <class ConsType>struct serialize<xcl::compound<ConsType> >
{
typedef ConsType cons_type;
typedef xcl::compound<ConsType> target_type;
//下面的两个函数是任何类型都必须遵守的规范,必须提供这两个函数
//向档案中写数据的操作
template <class ArchiveType,class ControlType>
static ArchiveType&write(ArchiveType&AR,const ControlType&C,const target_type&O)
{
const int Length = xcl::length<cons_type>::value;
WRITE_ENVIRONMENT<ArchiveType,ControlType> e(AR,C,O);
xcl::LOOP<WRITE,0,Length>::execute(e);
}
//从档案中读数据的操作
template <class ArchiveType,class ControlType>
static ArchiveType&read(ArchiveType&AR,ControlType&C,target_type&O)
{
const int Length = xcl::length<cons_type>::value;
READ_ENVIRONMENT<ArchiveType,ControlType> e(AR,C,O);
xcl::LOOP<READ,0,Length>::execute(e);
}
private:
//针对于写操作和读操作将各自需要的参数都用下面的方式传递到静态循环中
template <class ArchiveType,class ControlType>
struct WRITE_ENVIRONMENT
{
WRITE_ENVIRONMENT(ArchiveType&AR,const ControlType&C,const target_type&O)
:_AR(AR),_C(C),_O(O){}
ArchiveType &_AR;
const ControlType &_C;//常量
const target_type &_O;//常量
};
template <class ArchiveType,class ControlType>
struct READ_ENVIRONMENT
{
READ_ENVIRONMENT(ArchiveType&AR,ControlType&C,target_type&O)
:_AR(AR),_C(C),_O(O){}
ArchiveType &_AR;
ControlType &_C;//非常量
target_type &_O;//非常量
};
template<int i> struct WRITE
{
//复合类型对象的序列化仅仅只是序列化属性对象标识号对应的对象在
//相应的简单类型容器列表中的索引号
template <class EnvironmentType>
static void execute(EnvironmentType&e)
{
//你的代码在这里编写
typedef typename xcl::type<cons_type,i>::result CT;
typedef xcl::identifier<CT> IDT;
typedef xcl::container<IDT,CT> CONT;
const CONT&c=xcl::field<CT>(e._C);//这里要求控制类里面的类型不可以重复
typename CONT::const_iterator it=c.find(xcl::field<i>(e._O).ID);
typedef typename CONT::size_type size_type;
//输出的时候输出复合类型的标识号属性在相应的简单类型容器中的相对位置
//这样方便于管理标识号,主要是保证程序运行过程中程序中的标识号绝对不
//重复,如果将标识号保存到了文件中,再次序列化的时候就不好办了,因为
//那就要求在程序的整个生命周期中都不要出现重复的标识号,这就没有必要
//了,实际上微软的GUID标识号正是出于这个目的才出现的。在这里不论是出
//于空间还是时间的考虑,都没有必要采用GUID。所以最后特别强调一下,序
//列化输出的时候仅仅只是输出了一个相对位置,而不是输出了标识号。
//复合类型的标识号属性一定对应着相应的简单类型容器中的简单类型变量:
if(it!=c.end()){
//得到相对于简单类型容器的开始处的位置
size_type i=std::distance(c.begin(),it);
//将这个相对位置输出到档案中
e._AR.write(i);
}else{//虽然不会出现这个问题,但是为了本文档的说明,需要这个输出,在库
//的实现中将会删除这里的代码。
std::clog<< "在序列化输出复合类型对象的时候出现了错误" << std::endl;
}
}
};
template<int i> struct READ
{
template <class EnvironmentType>
static void execute(EnvironmentType&e)
{
//你的代码在这里编写
typedef typename xcl::type<cons_type,i>::result CT;
typedef xcl::identifier<CT> IDT;
typedef xcl::container<IDT,CT> CONT;
CONT&c=xcl::field<CT>(e._C);//这里要求控制类里面的类型不可以重复
typedef typename CONT::size_type size_type;
size_type tmp;//用来临时保存读取的索引号
e._AR.read(tmp);
typename CONT::iterator it = c.begin();
std::advance(it,tmp);//将游标移动到指定的索引位置
xcl::field<i>(e._O).ID = it->first;//将指定位置处的对象的标识号保存到复合对象中来
}
};
};
//针对于控制类的特化
template <class ConsType>struct serialize<xcl::control<ConsType> >
{
typedef ConsType cons_type;//类型必须唯一
typedef xcl::control<cons_type> control_type;
template<class ArchiveType>
static ArchiveType&write(ArchiveType&AR,const control_type&C)
{
typedef SERIALIZE<ArchiveType> IO;
typename IO::WRITE_ENVIRONMENT e(AR,C);
const int Length = xcl::length<cons_type>::value;
xcl::LOOP<IO::template WRITE,0,Length>::execute(e);
return AR;
}
template <class ArchiveType>
static ArchiveType&read(ArchiveType&AR,control_type&C)
{
typedef SERIALIZE<ArchiveType> IO;
typename IO::READ_ENVIRONMENT e(AR,C);
const int Length = xcl::length<cons_type>::value;
xcl::LOOP<IO::template READ,0,Length>::execute(e);
return AR;
}
//利用上面的两个函数来实现标准的输出和输入流操作符完成上面的函数调用过程
//输出
template <class ArchiveType>
friend ArchiveType&operator<<(ArchiveType&AR,const control_type&C)
{
return write(AR,C);
}
//输入
template <class ArchiveType>
friend ArchiveType&operator>>(ArchiveType&AR,control_type&C)
{
return read(AR,C);
}
private:
//实现序列化所有的类型的静态函数
template<class ArchiveType> struct SERIALIZE
{
//分成写环境和读环境,主要是为了类型的安全性。写的时候不允许修
//改控制类对象,所以将控制类对象设为常量;但是读的时候必须修改
//控制类对象,所以将控制类对象不设为常量。
struct WRITE_ENVIRONMENT
{
WRITE_ENVIRONMENT(ArchiveType&AR,const control_type&C)
:_AR(AR),_C(C){}
ArchiveType &_AR;//档案类必须可以修改
const control_type &_C;//常量控制类
};
struct READ_ENVIRONMENT
{
READ_ENVIRONMENT(ArchiveType&AR,control_type&C) :_AR(AR),_C(C){}
ArchiveType &_AR;//档案类必须可以修改
control_type &_C;//非常量控制类
};
//实现写某个类型的容器中的所有的元素到档案的静态函数,LOOP循环
//调用实现序列化写入所有类型容器的所有的元素的功能
template<int i> struct WRITE
{
typedef typename xcl::type<cons_type,i>::result current_type;
typedef xcl::identifier<current_type> identifier_type;
typedef xcl::container<identifier_type,current_type> container_type;
template <class EnvironmentType>
static void execute(EnvironmentType&e)
{
//你的代码在这里编写
const container_type&c=xcl::field<i>(e._C);
//为了方便读取,必须要添加一个表示容器中元素数量的数据
typedef typename container_type::size_type size_type;
//得到容器中的元素数量
size_type tmp = c.size();
//将这个元素数量写道档案中,主要是为了方便从档案中提取数据
e._AR.write(tmp);
//遍历输出所有的元素,注意:仅仅只是输出对象,不输出标识号
//因为标识号仅仅只是程序内部用来识别不同类型不同对象的方法
typename container_type::const_iterator it;
for(it=c.begin();it!=c.end();++it)
{//只序列化对象部分,标识号部分并不参与序列化过程
current_type::write(e._AR,e._C,it->second);
}
}
};
//实现读某个类型的容器中的所有的元素到档案的静态函数,LOOP循环
//调用实现序列化读取所有类型容器的所有的元素的功能
template<int i> struct READ
{
typedef typename xcl::type<cons_type,i>::result current_type;
typedef xcl::identifier<current_type> identifier_type;
typedef xcl::container<identifier_type,current_type> container_type;
template <class EnvironmentType>
static void execute(EnvironmentType&e)
{
//你的代码在这里编写
container_type&c=xcl::field<i>(e._C);
//首先读取该容器中原来的数据数量
typedef typename container_type::size_type size_type;
size_type tmp=0;
e._AR.read(tmp);
for(size_type i=0;i<tmp;++i)
{//只序列化对象部分,标识号部分并不参与序列化过程
//重新分配新的标识号,并且直接通过容器类的创建函数创建对象到
//容器中。这就是说序列化读取过程是不允许撤销和重做的:)
current_type O;//这里必须允许调用默认的构造函数
//从档案中提取数据到临时对象O
current_type::read(e._AR,e._C,O);
//将这个临时对象创建到容器中,这一步不允许撤销。也就是只要没有
//使用命令来操作了容器,那么该操作就是不可撤销的:)。这一点可以
//用来实现那些不需要撤销和重做功能的操作。
c.create(identifier_type(),O);
}
}
};
};
};
#endif//SERIALIZE_H
#ifdef CODE1//g++ -DARCHIVE_H -DSERIALIZE_H -DCODE1 thisfile.cpp
#include "control.h"
#include <fstream>
namespace xcl=pandaxcl;
//完全自定义的简单类型,撤销和重做库没有帮助做任何事情
class Simple
{
public:
//任何简单类型都必须提供没有参数的构造函数,这是因为在从档案中提取数据
//的时候,需要创建一个临时变量
Simple():_member(0){}
Simple(int m):_member(m){}
template <class ArchiveType,class ControlType>
static ArchiveType&write(ArchiveType&AR,const ControlType&C,const Simple&O)
{//必须采用这种形式的序列化,方便扩展档案格式
return AR.write(O._member);
}
template <class ArchiveType,class ControlType>
static ArchiveType&read(ArchiveType&AR,ControlType&C,Simple&O)
{//必须采用这种形式的序列化,方便扩展档案格式
return AR.read(O._member);
}
private:
int _member;
};
//从上面完全自定义简单类型可以看出,一个简单的类型需要写很多额外的代码,在这里
//可以由撤销和重做库辅助自动化定义简单类型,最重要的就是自动实现了序列化功能的
//代码,仅仅只要按照下面的格式来使用就可以了:)
typedef xcl::cons<int,
xcl::cons<char,
xcl::null_type> > ANOTHERSIMPLE;
class AnotherSimple
//采用tuple模板之后就可以自动化实现简单类型了
:public xcl::tuple<ANOTHERSIMPLE>
//添加了下面的serialize模板之后就自动实现了自动化简单类型的序列化功能
,public serialize<xcl::tuple<ANOTHERSIMPLE> >
{
public:
//任何简单类型都必须提供没有参数的构造函数,这是因为在从档案中提取数据
//的时候,需要创建一个临时变量
AnotherSimple(){}
//自动化定义简单类型的构造函数写法示例
AnotherSimple(int m1,char m2)
{
xcl::field<0>(*this) = m1;
xcl::field<1>(*this) = m2;
}
};
//下面的cons定义可以采用宏的方式来线性化处理的,但是为了代码简单直接起见,笨示
//例中不采用宏的方式。当然在实际的库中会提供一定数量的宏来方便处理常见的问题:)
typedef xcl::cons<Simple,
xcl::cons<Simple,
xcl::cons<Simple,
xcl::cons<Simple,
xcl::null_type> > > > COMPOUND;
class Compound
//采用compound模板之后就可以自动化实现复合类型了
:public xcl::compound<COMPOUND>
//添加了下面的serialize模板之后就自动实现了复合类型的序列化功能
,public serialize<xcl::compound<COMPOUND> >
{//在这里添加额外的操作
};
typedef xcl::cons<Simple,
xcl::cons<AnotherSimple,
xcl::cons<Compound,
xcl::null_type> > > CONTROL;
class Control
//采用control模板之后就可以自动化实现控制类
:public xcl::control<CONTROL>
//添加了下面的serialize模板之后就自动实现了控制类的序列化功能
,public serialize<xcl::control<CONTROL> >
{//在这里添加额外的操作
};
//测试代码主程序
int main()
{
//将档案和标准输出流捆绑,送往档案的数据全部重定向到了标准输出中
archive<std::ostream,text_tag> debug(std::cout);//text_ar
{//测试二进制格式和文本格式的档案的输出功能
Control C;
C.create(Simple(10));//完全自定义的简单类型
C.create(Simple(20));//完全自定义的简单类型
C.create(Simple(30));//完全自定义的简单类型
C.create(Simple(40));//完全自定义的简单类型
C.create(AnotherSimple(90,'A'));//自动化定义的简单类型
C.create(AnotherSimple(70,'B'));//自动化定义的简单类型
C.create(Compound());//自动化定义的复合类型
//将档案和文件流捆绑,送往档案的数据全部重定向到了文件中
std::ofstream tout("test.txt");
archive<std::ostream,text_tag> tar(tout);//text_ar
//将档案和文件流捆绑,送往档案的数据全部重定向到文件中
std::ofstream bout("test.bin");
archive<std::ofstream,binary_tag> bar(bout);//binary_ar
debug << C ;//输出控制类的信息到标注输出,进行观察
//将控制中心中的所有的数据送往文本格式的档案中(文件)
tar << C ;
//将控制中心中的所有的数据送往二进制格式的档案中(文件)
bar << C ;
}
std::cout << std::endl;
{//测试二进制格式的档案的读取功能
Control C;//空的控制类
//将档案和文件流捆绑,送往档案的数据全部来自于文件
std::ifstream in("test.bin");
archive<std::ifstream,binary_tag> bar(in);
bar >> C;//读入数据
debug << C;//输出控制类的信息到标注输出,进行观察
}
std::cout << std::endl;
{//测试文本格式的档案的读取功能
Control C;//空的控制类
//将档案和文件流捆绑,送往档案的数据全部来自于文件
std::ifstream in("test.txt");
archive<std::ifstream,text_tag> tar(in);
tar >> C;//读入数据
debug << C;//输出控制类的信息到标注输出,进行观察
}
return 0;
}
#endif//CODE1
////////////////////////////////////////////////////////////////////////////////
//该程序运行结果如下:
/*******************************************************************************
8 10 20 30 40 0 0 0 0 2 90 A 70 B 1 4 5 6 7
8 10 20 30 40 0 0 0 0 2 90 A 70 B 1 4 5 6 7
8 10 20 30 40 0 0 0 0 2 90 A 70 B 1 4 5 6 7
*******************************************************************************/
//产生的test.txt的内容如下:
/*******************************************************************************
8 10 20 30 40 0 0 0 0 2 90 A 70 B 1 4 5 6 7
*******************************************************************************/
//产生的test.bin的内容如下:(由于是二进制格式,在这里转换成为十六进制文本)
/*******************************************************************************
0000000: 0800 0000 0d0a 0000 0014 0000 001e 0000 ................
0000010: 0028 0000 0000 0000 0000 0000 0000 0000 .(..............
0000020: 0000 0000 0002 0000 005a 0000 0041 4600 .........Z...AF.
0000030: 0000 4201 0000 0004 0000 0005 0000 0006 ..B.............
0000040: 0000 0007 0000 000d 0a .........
*******************************************************************************/
////////////////////////////////////////////////////////////////////////////////
#if 0

从上面的程序的运行结果可以看出:程序的输出结果表示,不论是写入文本格式的档
案还是写入二进制格式的档案都是正确的;另外不论是读取文本格式的档案还是读取二进
制格式的档案也都是正确的。从而可以说:“序列化已经成功的在撤销和重做库中实现了
”。同时我们还可以看到,由于简单类型都比较有规律,所以本文中也给出了使用tuple的
方法实现自动化简单类型的方法。关于tuple的实现会在本文的附录中给出,这也是“C++
自动化(模板元)编程”系列里面讨论的内容,可以参看本人的相关文档。

另外还可以看出,本文介绍的档案类是可以无限扩充的,我只给出了两种非常常见的
格式的实现:文本格式和二进制格式。至于其它的格式您可以自由发挥,并且可以保证:
只要是按照本文介绍的规矩写的序列化代码,档案格式变动的时候,仅仅只需要改变档案
类就可以了,其它的根本不需要任何的改动。例如完全自定义的简单类型的序列化代码,
它和档案类完全是隔离了的,不会出现混乱现象;同样复合类,控制类都采用这种方法进
行了隔离。所以该撤销和重做库的序列化方案的弹性是非常大的。

另外值得说明的是:本文中讨论序列化类代码和档案类代码将会分别保存在“
serialize.h”和“archive.h”两个文件中,方便以后使用。

好了,到目前为止,撤销和重做库的主体框架已经搭好,但是并不表示该系列的文档
就已经不必写了,实际上在使用的过程中还会产生千奇百怪的情况,这就是检验该撤销和
重做库的实用性和强健性过程。欢迎读者朋友反馈,我也好进行改进:)。这里特别强调一
下上面的文本格式的档案类中使用字符串(特指std::string或者和std::string类似的类
如:MFC的CString类,而不能是C字符串)作为简单类型的时候,字符串里面不允许有任何
的空白。而二进制格式档案则还不允许使用任何的字符串。这些问题的解决在后续的文档
中将会解决。

现在列举一些我的设计过程中考虑到了的问题:

(1)如何在对话框中使用撤销和重做能力

(2)如何处理大型对象,例如位图对象

(3)如何产生其它格式的档案类,例如XML格式的档案

(4)如何对采用该库开发的程序生成的档案进行版本控制

(5)撤销和重做的进一步的智能化和优化,例如:在一个复合命令中创建了一个对象,
而在这个复合命令的后面又出现了删除该对象的命令,这样就可以删除这两个命令,减少
撤销和重做过程中的时间和空间开销。这里面有着许许多多可以优化的地方,都会在后续
的文档中进行讨论。

上面就是我现在能够列举的问题,将会在后续的文档中进行解答。敬请关注:)

#endif
#ifdef TUPLE_H//附录:tuple.h的实现
#pragma once
#include "scatter.h"
namespace pandaxcl{
template <class T>struct tuple_unit
{
T _value;
};
template <class Cons,int start=0>
struct tuple:public scatter<Cons,tuple_unit,start>
{
typedef Cons cons_type;
};
//下面的函数用来根据类型来得到相应的基元类型,注意这个函数仅仅只能够
//在用scatter产生代码的时候使用的类型串里面不含有重复类型的时候有效
template <class T,int start,class Cons> T&field(tuple<Cons,start>&obj)
{
return static_cast<tuple_unit<T>&>(obj)._value;
}
template <class T,int start,class Cons> const T&field(const tuple<Cons,start>&obj)
{
return static_cast<const tuple_unit<T>&>(obj)._value;
}
//下面的函数用来根据类型索引号来得到相应的基元类型
template <int i,int start,class Cons>
typename pandaxcl::type<Cons,i-start>::result&
field(tuple<Cons,start>&obj)
{
typedef typename pandaxcl::type<Cons,i-start>::result CT;
typedef scatter<CT,tuple_unit,i> RT;
return static_cast<RT&>(obj)._value;
};
template <int i,int start,class Cons>
const typename pandaxcl::type<Cons,i-start>::result&
field(const tuple<Cons,start>&obj)
{
typedef typename pandaxcl::type<Cons,i-start>::result CT;
typedef scatter<CT,tuple_unit,i> RT;
return static_cast<const RT&>(obj)._value;
};
}//namespace pandaxcl{
#endif//TUPLE_H

抱歉!评论已关闭.