撤销重做(UndoRedo)
在撤销和重做的实现过程中,总会出现各种各样的特殊情况和特殊技巧来实现撤销和重做功能,由于本系列⽂档实现的是采⽤了⼀般化的
⽅法实现了撤销和重做框架!所以就要思考这样的问题:采⽤取巧的⽅法是否⼀定⽐这⾥通⽤的⽅法在时间和空间上⾯⾼效呢?本⽂就是
通过讨论⼀个基本的交换函数来作为类⽐,虽然不是很有说服⼒的解释,但是也说明了不少的问题!
下⾯是经典的交换函数的两种实现⽅式:
采⽤中间变量的交换过程:
inta=10;
intb=20;
//需要第三个变量进⾏中转的交换过程
intt;//中转变量
t=a;//执⾏之后a=10b=20t=10
a=b;//执⾏之后a=20b=20
b=t;//执⾏之后a=20b=10
这就类似于有⼀杯橙汁(a)和⼀杯⽜奶(b),要想把两个杯⼦中的饮料进⾏置换,就必须借助于第三⽀空杯⼦(t)。交换的过程如下:
1.把橙汁(a)导⼊空杯⼦(t)中
2.把⽜奶(b)导⼊盛橙汁的杯⼦(a)中
3.把空杯⼦(t)中的橙汁倒⼊盛⽜奶的杯⼦(a)中
在上⾯的讨论过程中,假设了三个杯⼦的⼤⼩⼀样,其实这种假设是不必要的!特别需要注意的是,这⾥的这个过程可以很容易的实现逆
操作!
下⾯再来看看采⽤另外的⼀种假设的交换过程:
inta=10;
intb=20;
//不需要第三个变量进⾏中转的交换过程
a=a+b;//右边a=10b=20执⾏之后a=30b=20
b=a-b;//右边a=30b=20执⾏之后a=30b=10
a=a-b;//右边a=30b=10执⾏之后a=20b=10
从上⾯的交换过程可以看出,并没有采⽤第三个变量,⽽是直接采⽤了两个变量的⾃⾝!这其实就是假设了:装橙汁和装⽜奶的两个杯⼦
都最多只装了⼀半!交换过程如下:
1.将⽜奶倒⼊盛橙汁的杯⼦中
2.从1中的混合溶液中萃取出橙汁,放⼊盛⽜奶的杯⼦中
上⾯的这个萃取过程是可不是那么容易实现的,这是由物理特性和化学特性决定的!但是把上⾯的⽜奶和橙汁换成⽔和汽油,那么实现起
来就容易多了!当然是⽜奶换成⽔,橙汁换成汽油啦:)不过还要注意的是,上⾯的那个数学过程是可以实现可逆操作的,但是后⾯的那
个物理过程却很难实现逆操作!
从上⾯的两种交换过程可以看出:虽然可以采⽤⼀些特定的技巧(例如第⼆种交换⽅法)实现不采⽤第三个量实现交换,但是这种交换过
程的限制却太多了,不容易推⼴使⽤!此外这种⾮常有技巧的交换过程的逆操作的实现也是⾮常困难的,不可能有⽐较通⽤的⽅法实现逆
操作的!
鉴于此,第⼀种⽅法虽然采⽤了第三个变量实现交换,但是这种⽅法有着下⾯的种种优势:
1.交换过程可以⾮常容易的实现
2.交换过程的逆操作也可以很容易的实现
3.采⽤第三个变量实现交换的过程并不⽐其他的特定技巧实现的交换过程的⽅法占⽤的资源多!
关于第3点,可以这样解释:第⼀种交换⽅法所⽤的三个杯⼦可以是⼀样⼤,⽽第⼆种交换⽅法的两个杯⼦,先倒出⽜奶的杯⼦可以只有装
橙汁的杯⼦的容积的⼀半⼤⼩!如果装⽜奶的杯⼦和第⼀种⽅法中的杯⼦⼤⼩⼀致,那么两种⽅法的杯⼦的容积是⼀样的!由此可见,采
⽤特定的⽅法并不⼀定省资源:(
有了上⾯的讨论,可以看出,第⼀种交换⽅法是⼀种⾮常好的通⽤的交换⽅法:)撤销和重做的本质就是⼀种交换过程,下⾯的内容都是
围绕这种⽅法来讨论的:)
⾸先讨论⼀下针对整型变量v的撤销和重做功能的原理实现:
intv=5;//v的当前值
v=8;//(1)现在需要将v的值修改成为8
v=5;//(2)撤销将v的值修改成为8的操作
v=8;//(3)重做将v的值修改成为8的操作
在上⾯的代码中可以看出:这是⼀个典型的修改操作的撤销和重做的原理实现,其中:
1.步骤(1)中的8是修改参数
2.步骤(2)中的5是v在执⾏修改操作之前的原始数据
3.步骤(3)中的8是执⾏重做操作需要的修改参数备份,必须在执⾏修改操作之前备份之
从⽽可以总结出实现撤销和重做所必须遵守的规范如下:
1.为了实现撤销操作必须在修改对象之前备份原始数据
2.为了实现重做操作必须在修改对象的时候备份修改参数
有了这两条规范就可以保证任何操作(到⽬前为⽌我还没有发现不能⽤这种⽅式实现撤销和重做的操作)都可以通过这两条规则实现撤销
和重做的能⼒了。
好了,到⽬前为⽌,有了实现撤销和重做⽅案的通⽤规则,并且这种规则的空间和时间效率都⾮常好,我们该讨论⼀下三个基本操作和⼀
个复合操作了。⾄于为什么只有三种基本操作和⼀个复合操作,这是我在编写这个撤销和重做框架的过程中慢慢积累起来的,⾄于严格的
证明,恐怕不是我现在可以处理的了:(
这个三个基本操作是:
a.创建操作
b.修改操作
c.删除操作
⼀个复合操作是:
d.复合操作
⼀共是四个操作,其中复合操作可以是三个基本操作的组合,也可以是三个基本操作和复合操作的任意组合,也就是说:复合操作⾥⾯还
有⼦复合操作,这种嵌套可以达到任意的层次。这⾥⾯的组合就是千变万化的了!
好了,说了这么多,是该看看具体的代码是如何实现的了。值得说明的是:为了代码的正确性和可读性,在实现的过程中尽量避免⾮常复
杂的C++指针操作问题,当然对于避免不了的指针问题,也要尽可能的使其简单;取⽽代之的是尽量使⽤STL中的容器和算法来实现需要
的功能。
在上⾯的修改操作的原理实现中,我们⾸先就已经拥有了⼀个整型变量v,所以执⾏修改操作的必要条件就是保证该变量已经存在(没有变
量,如何修改啊),但是这⾥的变量v从分配到释放的过程没有考虑,因此下⾯来讨论分配和释放两个过程对应的创建操作和删除操作:
int*ptr=newint(5);//创建操作
deleteptr;ptr=;//删除操作
可以看出,对象的创建过程就是创建操作,⽽对象的释放过程就是删除操作。在此其实还有另外⼀种考虑:如何表⽰存在和消失的概念?
⽬前我只能想到⽤另外⼀个对象来记录这种变化,也就是说:对象的存在和消失的状态,对象⾃⾝是不可以保存的,必须通过另外的对象
(这⾥采⽤的是整型指针)来保存!⽽这另外⼀个对象来保存其他对象的存在和消失的状态,实际上也是⼀种修改过程,只不过这种修改
过程不需要实现撤销和重做功能,也就是说实现撤销和重做能⼒需要借助于不需要撤销和重做的基础才能实现!这是不是很⽭盾,其实这
是普遍的规律,量⼦信息的通信还是需要借助于传统通讯基础才能实现:)
事实上,除了⽤指针对象来表⽰对象的存在和消失状态外,也可以⽤STL容器来表⽰对象的存在和消失状态,⽽后者就是在本⽂中采⽤的⽅
案!当容器中存在对象的时候,可以从STL容器中索引出相应的对象;当容器中没有指定索引的对象的时候,就表⽰该对象已经被释放了!
Tip
·事实上,只要能够表⽰存在和消失的概念的C++量都可以在这⾥作为
·另外⼀个对象。
这⾥强调了索引,其实这⾥的索引包含了两层意思:
名字
·STL容器中可以存放多个同类型对象,为了识别不同的对象,就需要为每个对象取⼀个
·独⼀⽆⼆的名字,当然这⾥的独⼀⽆⼆仅仅只限于该STL容器范围
动作
·为STL容器中的多个对象取独⼀⽆⼆名字的⽬的就是为了能够在修改的时候有提取指定
·对象的能⼒
在编写GUI程序的过程中,经常⽤对话框来实现对⼀批对象的操作,⽽且这种批量操作的撤销操作和重做操作也希望能够⼀步执⾏所有⼦操
作的撤销和重做操作,否则⾏为就会极其怪异。这种批量操作不同于上⾯的修改操作、创建操作以及删除操作,⽽是独⽴于这三种基本操
作之外的操作。简单点说就是⼀种能够组合基本操作和批量操作的操作。这种操作就是前⾯所说的复合操作。
现在对前⾯的讨论进⾏总结如下:
修改操作
1.为了实现撤销操作必须在对对象修改之前保存原始信息备份
2.为了实现重做操作必须在修改对象的时候保存修改信息备份
创建操作删除操作
1.必须通过对象(v)之外的其他对象(ptr)来反映该对象的存在和消失状态
2.创建操作和删除操作互为逆操作,可以⽤来互相实现对⽅的撤销操作
3.本质上讲,创建操作和删除操作的撤销和重做操作实际上就是针对该对象之外的对象(ptr)执⾏的修改操作,因此也必须要满⾜修改操
作的信息备份要求
复合操作
1.必须能够组合前⾯讨论的修改操作、创建操作和删除操作三种基本操作
2.当然复合操作也要能够组合其他的⼦复合操作
为了使得讨论更加形象,在这⾥特别提供⼀个对象类:
//对象类
classObject
{
public:
Object():_member(0){}
Object(intm):_member(m){}
private:
int_member;
};
有了这些讨论,我们就可以实现撤销和重做基本结构的原理实现了:
创建操作的原理实现:
//⾸先创建两个Object对象,为了避免指针的出现,采⽤了标识号的⽅法
{//创建操作的撤销和重做
typedefstd::map
ContainerC;//必须⽤这个容器来表⽰对象的存在状态,并且可以根据标识符得到对象
intid1=1,id2=2;//两个对象的标识号创建参数保存于此
Objectobj(10);//对象创建参数保存于此
(std::make_pair(id1,obj));//创建标识号为id1的对象
(std::make_pair(id2,obj));//创建标识号为id2的对象
//创建操作的撤销⾮常容易实现
(id1);//创建标识号为id1的对象的撤销操作
(id2);//创建标识号为id2的对象的撤销操作
//创建操作的重做操作也⾮常容易实现,不过需要备份的创建参数
(std::make_pair(id1,obj));//重做创建标识号为id1的对象
(std::make_pair(id2,obj));//重做创建标识号为id2的对象
}
修改操作的原理实现:
{//修改操作的撤销和重做
typedefstd::map
ContainerC;//必须⽤这个容器来表⽰对象的存在状态,并且可以根据标识符得到对象
//当然如果需要修改操作的话,对象就必须⼀定已经存在了
/
intid1=1,id2=2;//两个对象的标识号创建参数保存于此
Objectobj(10);//对象创建参数保存于此
(std::make_pair(id1,obj));//创建标识号为id1的对象
(std::make_pair(id2,obj));//创建标识号为id2的对象
/
//下⾯才能够开始实现修改操作
//为了能够实现撤销操作⽽必须保存的信息
ObjectOBK1=C[id1];//备份标识号为id1的对象的原始信息
ObjectOBK2=C[id2];//备份标识号为id2的对象的原始信息
//为了能够实现重做操作⽽必须保存的信息
ObjectOM1(20);//备份标识号为id1的对象的修改参数信息
ObjectOM2(50);//备份标识号为id2的对象的修改参数信息
//开始对对象实现修改操作
C[id1]=OM1;//修改标识号为id1的对象
C[id2]=OM2;//修改标识号为id2的对象
//开始对对象实现撤销操作
C[id1]=OBK1;//撤销修改标识号为id1的对象
C[id2]=OBK2;//撤销修改标识号为id2的对象
//开始对对象实现重做操作
C[id1]=OM1;//重做修改标识号为id1的对象
C[id2]=OM2;//重做修改标识号为id2的对象
}
删除操作的原理实现:
{//删除操作的撤销和重做
typedefstd::map
ContainerC;//必须⽤这个容器来表⽰对象的存在状态,并且可以根据标识符得到对象
//当然如果需要删除操作的话,对象就必须⼀定已经存在了
/
intid1=1,id2=2;//两个对象的标识号创建参数保存于此
Objectobj(10);//对象创建参数保存于此
(std::make_pair(id1,obj));//创建标识号为id1的对象
(std::make_pair(id2,obj));//创建标识号为id2的对象
/
//下⾯才能够开始实现删除操作
//为了能够实现撤销操作⽽必须保存的信息
ObjectOBK1=C[id1];//备份标识号为id1的对象的原始信息
ObjectOBK2=C[id2];//备份标识号为id2的对象的原始信息
//开始对对象实现删除操作
(id1);//删除标识号为id1的对象
(id2);//删除标识号为id2的对象
//开始对对象实现撤销操作
(std::make_pair(id1,OBK1));//创建标识号为id1的对象
(std::make_pair(id2,OBK2));//创建标识号为id2的对象
//开始对对象实现重做操作
(id1);//重做删除标识号为id1的对象
(id2);//重做删除标识号为id2的对象
}
复合操作的原理实现:
{//复合操作
//由于复合操作需要对三个基本操作进⾏保存,也需要对⼦复合操作
//进⾏保存,在这⾥不⽅便给出,所以在后续的章节对这三个基本操
//作进⾏了封装之后才能够讨论。
}
从上⾯的三种操作的原理实现可以看出,为了实现三个基本操作的撤销和重做,都必须在执⾏操作之前进⾏必要的数据备份,为撤销和重
做提供必要的数据。
此外,表达对象存在和消失的状态是通过容器类(Container)来实现的!容器类的状态变化是不可以撤销和重做的,这也正是体现了前⾯
所提到的新功能必须借助于现有的基础才能实现的思想。此外⽤整数作为区别同类型对象的标识符!采⽤整数作为标识符是本框架在最初
的时候采⽤的⽅案。但是后来发现:采⽤指针作为标识符⽐整数作标识符更加直接、⽅便且⾼效!这在后⾯的代码中将会直接反映出来:-)
复合操作没能实现的原因已经在代码⾥⾯说明了:-)
在上⼀节中,已经对三种基本操作的撤销和重做的原理实现进⾏了细致的分析,并给出了⽰例代码,但是并没能够给出复合操作的⽰例代
码。这⾥⾯有着⽐较深刻的原因,具体来说就是:复合操作⾥⾯保存的是三个基本操作以及⼦复合操作的任意组合,这⾥⾯本⾝就存在⼀
个将三个基本操作和复合操作对象化的概念,在三个基本操作实现对象化之前是不能够讨论复合操作的!既然需要对三个基本操作进⾏对
象化,同时还应该看到:复合操作可以保存其它的复合操作,这说明复合操作也需要对象化!既然三个基本操作和⼀个复合操作都需要对
象化,并且这些对象都可以保存到复合操作⾥⾯,那么在对三个基本操作的对象化的同时就应该考虑复合操作的对象化问题!
现在就来讨论⼀下三个基本操作和⼀个复合操作的对象化问题,不过在讨论这个问题之前必须先回答下⾯的⼏个问题:
1.三个基本操作和⼀个复合操作的组合过程需要撤销和重做功能吗?
2.三个基本操作和⼀个复合操作怎样保存在复合操作⾥⾯?
3.撤销和重做功能在应⽤程序⾥⾯是怎么使⽤的?
对于问题1可以肯定的说是不需要撤销和重做功能的,否则就会成为“蛋鸡问题”!究竟是先有蛋还是先有鸡呢?我想这不应该是我回答的
问题吧:-)
对于问题2,C++⾥⾯有⽐较完美的⽅案,那就是C++天⽣具备的多态能⼒,所以问题2的答案就是通过保存操作类的基类指针指向不同类
型的对象实例的⽅法。
对于问题3就⽐较⿇烦了,不过还是有⽐较简单的⽅案的:应⽤程序中保存⼀个可撤销操作的命令队列,同时也要保存⼀个可重做操作的命
令队列;每当执⾏⼀个操作的时候,在执⾏完毕之后将这个命令保存到可撤销操作命令队列尾部,并且清空可重做操作命令队列;每当执
⾏撤销操作的时候,执⾏这个可撤销操作的反执⾏操作,执⾏完毕之后将这个命令从可撤销操作队列尾部移到可重做操作队列尾部;对于
重做操作则是执⾏可重做操作队列中的操作的执⾏操作,执⾏完毕之后将这个操作的命令从可重做操作队列尾部移到可撤销操作队列尾
部。
为了以后讨论的⽅便,在此引进“命令”概念:
命令
·将操作编码为C++类之后就成了命令类,命令类创建的对象就是命令
有了以上问题的讨论便可以得出如下的概念:
1.命令的操作对象都是另外存在的⼀个容器(对象存在和消失的状态由其保存),这个容器必须满⾜:
a.⽤标识号区别容器中的同类型的不同对象,⽽且这种绑定是在程序运⾏期间是终⽣绑定的!
b.能够根据标识号得到对象
2.必须有⼀个命令基类,这样才可以将所有的命令保存到复合命令中;同时命令基类必须提供接⼝,以提供执⾏操作和反执⾏操作。
3.复合命令实际上是⼀个命令队列,执⾏操作就是顺序调⽤⼦命令的执⾏操作,⽽反执⾏操作就是逆序调⽤⼦命令的反执⾏操作。
关于标识符与对象的绑定是终⽣绑定的问题根源在于:当标识符绑定的对象在执⾏删除操作之后,该标识符⼜和别的对象进⾏了绑定,那
么当这个删除操作撤销的时候就会出问题!因为⼀个标识符不可以⽤来标识两个对象,否则如何能够根据这个标识符来得到绑定的对象
呢?
有了这些讨论,现在可以得出标识符应该具备的功能列表了:
1.在程序的运⾏期间的任何时刻,⼀个标识符都要能够且只能够索引出⼀个对象
2.只有当标识符被完全废⽌的时候才能够重新使⽤这个标识符
3.所谓的标识符完全废⽌,是指在程序运⾏到废⽌该标识号之后的任何时候都不存在调⽤该标识号索引相应对象的代码
在前⾯的原理实现中,给出的是采⽤int类型的标识符。关于标识符的产⽣,采⽤的是预先分配的⽅式(直接给出了id1和id2)。这对于固
定的对象代码当然没有问题,但是要想使得标识号能够⽤到动态⽣成的对象上⾯,就必须提供⼀个标识号⾃动⽣成的函数了!
经过深⼊的考虑(⼤约两年多时间)之后,发现:采⽤指针作为标识符⽐整数作标识符更加直接、⽅便且⾼效,具体原因如下:
1.在程序的运⾏周期内,程序分配的指针值(整数值)是绝对不会重复的!当然这也是要满⾜⼀定条件的,在讨论后⾯的容器类的时候
会讨论到这⼀点。这就解决了标识符⾃动⽣成的问题:-)
2.标识符要求能够根据标识符索引得到绑定的对象!关于这⼀点C++指针天⽣就具备这种能⼒,根本就不需要额外的编码实现!从⽽采
⽤指针作为标识符使得C++实现更加⾃然
3.要想在⼀个对象内部引⽤其它对象,采⽤指针是⼀个更加直接的⽅式!试想⼀下,如果⼀个对象内部放置了⼀个整数⽤来索引其它的
对象,这会多么的别扭啊!此外⽤整数标识其它类型的对象把对象的类型弄丢了!这在C++中应该尽⼒避免,因为C++的⾼效很⼤⼀部分
在于类型系统的编译期优化
有了上⾯关于标识符的详细讨论之后,在后⾯的章节中将会采⽤指针作为标识符来实现这个撤销和重做框架。
好了现在终于可以给出三个基本命令和⼀个复合命令的⽰例代码了:
命令基类command(提供两个公共的接⼝:redo和undo)的实现如下:
structcommand
{
virtual~command(){}//避免内存泄漏
virtualvoidredo()=0;//重做操作,兼任执⾏操作
virtualvoidundo()=0;//撤销操作
};
Note
·将操作概念映射到具体的C++类,我将这种映射得到的C++类命名为操作
·类,⽽操作类定义的对象就是命令,⽽这种将操作定义为命令
·的过程就是对象化。
组合操作类的复合操作类:
classbatch//复合命令
:publiccommand//是⼀种命令
,publicstd::list
{
public:
virtual~batch()//当复合命令被释放的时候,所有的⼦命令也要释放
{
this->free();
}
voidrecord(command*pCmd)//记录⼦命令,⼦命令必须通过new创建
{
this->push_back(pCmd);
}
public:
virtualvoidredo()//执⾏操作和重做操作
{
std::for_each(begin(),end(),std::mem_fun(&command::redo));
}
virtualvoidundo()//撤销操作
{
std::for_each(rbegin(),rend(),std::mem_fun(&command::undo));
}
voidfree()//⽤复合命令实现命令中⼼的时候,需要这个函数
{
std::for_each(this->begin(),this->end(),kill());//释放⼦命令对象
this->clear();//释放⼦命令对象指针
}
};
其中的仿函数kill的实现如下:
structkill//给STL使⽤的仿函数
{
template
{
deleteptr;//delete⾃动判断ptr是否为NULL
}
};
这⾥的kill纯粹是为了使得编码更加简洁。
虽然有了上⾯的命令基类、复合操作类就可以实现任意的撤销和重做功能的程序了,但是很明显,还需要编写⼤量的派⽣⾃命令基类的各
种各样的操作类。那么有没有办法减轻或者消除这种负担呢?
在前⾯讨论过程中,我们已经假定任何操作都可以采⽤三种基本操作和⼀个复合操作来实现,那么撤销和重做的架构的⾃动化就要以这个
假定为基础了。
在前⾯讨论三个基本操作的时候,我们知道对象的存在状态必须通过另外的对象来表现,因此需要提供⼀个始终存在的对象来表⽰。在这
⾥就采⽤和STL类似的容器类来表⽰这种状态:
template
{
public:
typedefTobject_type;
typedefstd::t
typedefstd::list
typedefstd::map
protected:
objects_type_objects;//所有的对象都保存在这⾥
unud_type_unud;//第⼆个参数是对应的变量被命令引⽤的数量
public:
//下⾯两个函数是在命令的创建和删除的时候调⽤的
//配合_unud就是⼀个受管理的引⽤计数智能指针
//只是引⽤该对象的“智能指针”只能是命令
voidincrea(T*id){_unud[id]++;}
voiddecrea(T*id){_unud[id]--;}
//获得⼀个可以使⽤的对象空间,当没有对象可以回收的时候就创建⼀个
virtualT*generate(T*ud=)
{
T*ptr=ud;
typenameunud_type::iteratorit;
//如果_unud中有引⽤计数为零的对象,直接返回该指针
for(it=_();it!=_();++it)
{
if(0==it->cond&&ud!=it->first)
{
ptr=it->first;
break;
}
}
//如果_unud中没有引⽤计数为零的对象,在_vector中新建⼀个对象
//并返回该新建对象的指针
if(ud==ptr)
{
__back(T());//调⽤默认构造函数的情况
ptr=&_();
}
returnptr;
}
//得到某个指针被命令引⽤的次数,不存在的指针引⽤计数为零
typenameunud_type::size_typeud(T**pID)
{
return(_(*pID)==_())?0:_unud[*pID];
}
public:
virtualvoidcreate(T*id)
{
this->inrt(id);
}
virtualvoidmodify(T*id,constT&v)
{
*id=v;
}
virtualvoidremove(T*id)
{
this->era(id);
}
};
此容器类已经采⽤了指针作为标识符!把这⾥的代码和前⾯的原理实现中的容器类(STL的map容器)进⾏⼀下对⽐便可以发现⾮常类
似,但也有很多不同。这是因为这个容器类考虑了前⾯谈到的标识符所必须具备的功能!下⾯逐⼀的进⾏解释:
标识符⾃动创建
·在程序的运⾏周期内,程序分配的指针值(整数值)是绝对不会重复的!
1.在上⾯的容器类⾥⾯,container向外提供的是t概念,也就是说容器⾥⾯的标识符(指针)是不允许重复的!
2.既然标识符是指针,那么必然有管理指针指向的对象的容器,在这⾥是_objects。最初使⽤的是STL的vector类,但是经过使⽤发
现,采⽤vector不能满⾜需求,相反采⽤list却⽐较能够满⾜需求!因为这个容器仅仅只是保存对象,对这些对象只执⾏⼀次添加操作,添
加的同时把新添加的对象地址值取出,作为标识符。除此之外,不需要任何其他的操作了。
3.对象的分配是通过_objects进⾏管理的。这样可以降低复杂度!基本分配原理就是:⾸先看看_unud⾥⾯有没有引⽤计数为零的还
没有被释放的闲置对象。有就直接利⽤之,反之则在_objects中追加⼀个,同时将追加的对象地址取出返回!
4.既然有对象分配,⾃然也要有资源的回收。上⾯的容器类的实现中,_unud容器是⼀个map容器,作⽤是和increa()和
decrea()函数配合使⽤,构成⼀个引⽤计数的“智能指针”概念!特别需要注意的是,这⾥的“智能指针”就是我们讨论的三个基本命
令对象!
索引对象
·C++指针天⽣就可以索引到对象:-)
嵌⼊到其他对象
·在其他的对象⾥⾯可以直接编写对象指针代码,这样就可以嵌⼊到其他的对象⾥⾯了
·!这⼀点在处理复杂对象的时候特别有⽤(当然要配合⾃动化编程的⾼级技术才可以
·)。
create()modify()remove()
·分别被三个基本命令调⽤,这样就可以⽐较对称的处理三个基本操作了:)撤销和重
·做的能⼒全部来⾃于三个基本命令类对必要数据的备份:)当然也可以采⽤其他专⽤
·的⽅法可以避免⼀些数据的备份,或许可以节省⼀些时间和空间上的开销,但是通常
·来说,这样是得不偿失的!即使采取各种各样的技巧,也未必真的可以减少时间或空
·间上的开销,见前⾯的swap函数的讨论。
ud()
·因为对象的指针是作为标识符的,同时也可以直接操作对象!既然可以直接操作对象
·,那么就可以在撤销和重做库的外部保存该指针变量,该指针变量有可能是动态分配
·的就会出现释放的问题,所以在此提供⼀个查询指针的命令引⽤计数的函数。当该函
·数返回0的时候,就表⽰没有命令引⽤该指针,也就表⽰可以安全删除该指针变量:)
·特别注意不是删除指针指向的对象,⽽是指针变量⽽已,指针所指向的对象
·并没有被删除,该对象由容器类管理。
有了上⾯的容器类之后,就可以来编写属于该容器类的三个基本操作类了:
创建操作类:
template
{
voidinit()
{
//下⾯的generate函数主要是在第⼀次执⾏创建命令的时候起作⽤
_ID=_te(_ID);//如果_ID已经被使⽤了,则创建新的对象空间
_(_ID);//容器类的引⽤计数增⼀
}
public:
typedeftypenameContainer::object_typeT;
create(Container&C,T*&ID):_C(C),_ID(ID)
{
init();
}
create(Container&C,T*&ID,constT&O):_C(C),_ID(ID)
{
init();*_ID=O;
}
virtual~create()
{
_(_ID);//容器类的引⽤计数减⼀
}
voidredo()
{
_(_ID);
}
voidundo()
{
_(_ID);
}
private:
create(){}
Container&_C;
T*&_ID;
};
修改操作类:
template
{
public:
typedeftypenameContainer::object_typeT;
public:
modify(Container&C,T*ID,constT&O):_C(C),_ID(ID),_O(O)
{
_OB=*_ID;//备份信息
_(_ID);
}
virtual~modify()
{
_(_ID);
}
voidredo()
{
_(_ID,_O);
}
voidundo()
{
_(_ID,_OB);
}
private:
bool_backuped;//是否已经进⾏过备份啦,避免重复备份的问题
modify(){}
Container&_C;
T*_ID;
T_O;//修改参数
T_OB;//修改之前的对象
};
删除操作类(因为delete是C++操作符,所以选择了remove):
template
{
typedeftypenameContainer::object_typeT;
public:
remove(Container&C,T*&ID):_C(C),_ID(ID),_IDB(ID)
{
_(_ID);
}
virtual~remove()
{
_(_ID);
}
voidredo()
{
_(_ID);
_ID=;//标识号为NULL表⽰已经被删除了
}
voidundo()
{
_ID=_IDB;//撤销了删除操作,标识号应该复原
_(_ID);
}
private:
remove(){}
Container&_C;
T*&_ID;
T*_IDB;
};
有了前⾯的这些讨论就可以⽤最少的编码付出得到⾮常强⼤的撤销和重做功能。下⾯是⼀个使⽤中的实际例⼦:
在测试代码中使⽤的对象:
//对象类
classObject
{
public:
Object():_member(0){}
Object(intm):_member(m){}
//这个函数是前⾯的命令中所必须的操作符
Object&operator=(constObject&o)
{
if(&o!=this)
{
_member=o._member;
}
return*this;
}
private:
int_member;
//下⾯的这个函数仅仅是为了⽅便显⽰对象信息⽽重载的操作符
template
friendStream&operator<<(Stream&s,Object&o)
{
s<
returns;
}
};
//为了能够判断操作的正确性需要输出容器的信息
template
voiddisplay(constchar*str,Container&c)
{
std::cout<
typenameContainer::iteratorit=();
for(;it!=();++it)
{
std::cout<<<<*it<<<<**it<<;
}
std::cout<
}
测试代码:
创建命令的撤销和重做⽰例:
{//模拟创建操作的撤销和重做
typedefundo::raw::container
typedefundo::raw::create
CONTAINER::object_type*id1=;
CONTAINER::object_type*id2=;
CONTAINERC;
CREATE*pCmd1=newCREATE(C,id1);
CREATE*pCmd2=newCREATE(C,id2);
display(,C);
pCmd1->redo();//模拟创建标识号为id1的Object对象
pCmd2->redo();//模拟创建标识号为id2的Object对象
display(,C);
pCmd1->undo();//模拟撤销创建标识号为id1的Object对象
pCmd2->undo();//模拟撤销创建标识号为id2的Object对象
display(,C);
pCmd1->redo();//模拟重做创建标识号为id1的Object对象
pCmd2->redo();//模拟重做创建标识号为id2的Object对象
display(,C);
deletepCmd1;deletepCmd2;
}
测试结果:
创建CREATE命令之后:[0]
执⾏CREATE命令之后:[2](00375600,0)(003756A8,0)
撤销CREATE命令之后:[0]
重做CREATE命令之后:[2](00375600,0)(003756A8,0)
修改命令的撤销和重做⽰例:
{//模拟修改操作的撤销和重做
//在执⾏修改操作的时候,被修改的对象当然应该已经存在
typedefundo::raw::container
CONTAINERC;
CONTAINER::object_type*id1=;
CONTAINER::object_type*id2=;
{
typedefundo::raw::create
CREATE*pCmd1=newCREATE(C,id1);
CREATE*pCmd2=newCREATE(C,id2);
pCmd1->redo();//模拟创建标识号为id1的Object对象
pCmd2->redo();//模拟创建标识号为id2的Object对象
deletepCmd1;deletepCmd2;
}
//下⾯才可以进⾏修改操作的撤销和重做模拟了
typedefundo::raw::modify
MODIFY*pCmd1=newMODIFY(C,id1,Object(20));
MODIFY*pCmd2=newMODIFY(C,id2,Object(50));
display(,C);
pCmd1->redo();//模拟修改标识号为id1的Object对象
pCmd2->redo();//模拟修改标识号为id2的Object对象
display(,C);
pCmd1->undo();//模拟撤销修改标识号为id1的Object对象
pCmd2->undo();//模拟撤销修改标识号为id2的Object对象
display(,C);
pCmd1->redo();//模拟重做修改标识号为id1的Object对象
pCmd2->redo();//模拟重做修改标识号为id2的Object对象
display(,C);
deletepCmd1;deletepCmd2;
}
测试结果:
创建MODIFY命令之后:[2](003755D0,0)(00375690,0)
执⾏MODIFY命令之后:[2](003755D0,50)(00375690,20)
撤销MODIFY命令之后:[2](003755D0,0)(00375690,0)
重做MODIFY命令之后:[2](003755D0,50)(00375690,20)
删除命令的撤销和重做⽰例:
{//模拟删除操作的撤销和重做
//在执⾏删除操作的时候,被删除的对象当然应该已经存在
typedefundo::raw::container
CONTAINERC;
CONTAINER::object_type*id1=;
CONTAINER::object_type*id2=;
{
typedefundo::raw::create
CREATE*pCmd1=newCREATE(C,id1);
CREATE*pCmd2=newCREATE(C,id2);
pCmd1->redo();//模拟创建标识号为id1的Object对象
pCmd2->redo();//模拟创建标识号为id2的Object对象
deletepCmd1;deletepCmd2;
}
//下⾯才可以进⾏删除操作的撤销和重做模拟了
typedefundo::raw::remove
REMOVE*pCmd1=newREMOVE(C,id1);
REMOVE*pCmd2=newREMOVE(C,id2);
display(,C);
pCmd1->redo();//模拟删除标识号为id1的Object对象
pCmd2->redo();//模拟删除标识号为id2的Object对象
display(,C);
pCmd1->undo();//模拟撤销删除标识号为id1的Object对象
pCmd2->undo();//模拟撤销删除标识号为id2的Object对象
display(,C);
pCmd1->redo();//模拟重做删除标识号为id1的Object对象
pCmd2->redo();//模拟重做删除标识号为id2的Object对象
display(,C);
deletepCmd1;deletepCmd2;
}
测试结果:
创建REMOVE命令之后:[2](003755E8,0)(003756A8,0)
执⾏REMOVE命令之后:[0]
撤销REMOVE命令之后:[2](003755E8,0)(003756A8,0)
重做REMOVE命令之后:[0]
复合命令的撤销和重做⽰例:
{//模拟复合操作的撤销和重做
typedefundo::raw::container
typedefundo::raw::create
typedefundo::raw::batchBATCH;
CONTAINER::object_type*id1=;
CONTAINER::object_type*id2=;
CONTAINERC;
BATCH*pMCmd=newBATCH();
pMCmd->record(newCREATE(C,id1));
pMCmd->record(newCREATE(C,id2));
display(,C);
pMCmd->redo();//模拟执⾏复合命令
display(,C);
pMCmd->undo();//模拟撤销复合命令
display(,C);
pMCmd->redo();//模拟重做复合命令
display(,C);
deletepMCmd;
}
测试结果:
创建BATCH命令之后:[0]
执⾏BATCH命令之后:[2](003755D0,0)(003756D8,0)
撤销BATCH命令之后:[0]
重做BATCH命令之后:[2](003755D0,0)(003756D8,0)
从上⾯的代码中,可以看出三个基本操作对象化之后成为三个基本命令,⽽⼀个复合操作则对象化为⼀个复合命令。从上⾯的⽰例代码中
已经可以看出使⽤这种⽅式的撤销和重做机制需要遵守:
1.必须⽤标识号来区分同种类型的不同对象
2.对象必须是可拷贝的(在三个基本命令中都需要)
3.所有命令对象的创建都必须使⽤new操作符的⽅式创建(这是为了⽅便管理⽽提出的)
4.标识号绝对不允许重复;关于这⼀点其实可以很容易弄错的,所以在这⾥特别解释⼀下。所谓的标识号绝对不允许重复是指:任意的
标识号在应⽤程序中最多只能使⽤⼀次,记住了,是在应⽤程序运⾏的整个⽣命周期中只能够使⽤⼀次(暂时⽤这么强的语⽓,在⼀定的
条件下,这种要求可以降低),就像GUID不允许重复⼀样。有这种要求的原因,⼀⽅⾯是为了是代码更加趋于简单、可靠;另⼀⽅⾯的原
因是为了程序的运⾏效率。关于这⼀点会在后续的⽂章中详细解释。表⾯上看GUID就可以满⾜这⾥的要求,但是我并不采⽤它,因为
GUID相关的函数是Windows平台所特有的,这样就限制了本⽂所介绍的撤销和重做机制的平台⽆关性;另⼀⽅⾯是因为GUID占⽤16个
字节的空间,对于上⾯的⽰例代码来说,标识号类型的尺⼨⽐对象的尺⼨还⼤,显然很不划算,关于这⼀点和字符编码的⽅案有些类
似,Unicode编码⽅式就可以节省⼤量的ASCII编码所表⽰的信息,同时也可以表⽰⽆穷多的字符编码,在后续的章节中就会给出⼀个特
别定制的标识符类,以达到我们的要求
⽬前为⽌,已经成功的将三个基本操作封装为三个基本命令,同时也将⼀个复合操作封装成了⼀个复合命令,另外也给出了简单的使⽤代
码;从代码中可以看出,撤销和重做的过程还是⽐较晦涩,⽤户为了表达撤销和重做的过程还需要编写很多的额外代码,⽽且这种代码也
没有很直接的表达撤销和重做的思想。
最为重要的⼀点是:在上⾯的使⽤例⼦中,所有的命令是分散的,缺乏⼀个集中管理的地⽅,从⽽导致了编码的繁琐和不规范!
为了更好的表达撤销和重做机制,并且更加简化客户端代码的书写,在此将会对前⾯给出的代码进⾏更深⼊的封装,让客户端编写的代码
尽可能的少,同时也能够更加直接的表达撤销和重做的意思。
总的说来,在此要达到下列⽬标:
1.处理更多的对象类型,前⾯只是处理了⼀种类型(Object)。从本质上来说,处理多个数据类型和处理⼀个数据类型是⼀样的,但是
对于⽅便使⽤该撤销和重做框架来说,给出⼀个⽰例将会是读者更加直接的了解到如何使⽤该框架的⽅便的实现⾃⼰的撤销和重做能⼒。
另外从⽅便使⽤的⾓度来说,本章中必须给出⼀个惯例,并且在这个惯例的基础上,实现撤销和重做功能。实际上在本⽂中介绍的撤销和
重做框架还天⽣具备了序列化能⼒,这可是⼀个⾮常不错的副产品哦:)关于序列化的原理,可以参见本⼈的其它相关⽂档。
2.撤销和重做过程在代码中表达得更加直观。从前⾯的⽰例代码中可以看出,撤销和重做的功能虽然可以实现了,但是也要注意撤销和
重做的表现并不直观,直接看到代码,阅读代码的⼈并不能够很容易的了解代码的意义;所以,在此还要会改进这种代码,使得阅读代码
就像阅读⽂档⼀样,使代码更直接的表达撤销和重做的意义。
3.实现任意次的撤销和重做。前⾯的撤销和重做次数默认是⽆限的,虽然默认的情况下是⽆限次的撤销和重做,但是客户端有时候可能
因为种种原因⽽不想使撤销和重做的步骤太多,因⽽在此还要给出如何实现有限次的撤销和重做⽅案。
下⾯依次解决所提出的三个问题。对于问题(1)⽂中将会给出两个⽐较直接的数据类型:矩形类(Rectangle)和圆形类(Circle)。对于问题
(2)⽂中将会更进⼀步的封装前⾯的使⽤⽅式,最后⼀个问题,⽂中将会给出⼀个使⽤惯例,是可以封装成为头⽂件的,这在后⾯讨论。
有了上⾯的命令基类和复合操作类,要想使⽤撤销和重做功能,还需要⼀个命令中⼼类,来集中管理各种命令,⽽这⾥的复合操作类已经
具备了⼀定的命令管理功能了,因此,命令中⼼类采⽤了复合操作类来实现:
classundo_bat:publicbatch{};//撤销命令队列
classredo_bat:publicbatch{};//重做命令队列
classcenter:publicundo_bat,publicredo_bat//命令中⼼,管理两种命令队列
{
private:
size_t_limit;
public:
typedefredo_batredo_type;
typedefundo_batundo_type;
typedefundo_typehistory_type;
center():_limit(100)//默认最⼤撤销操作步骤总数为100步,⼀般的应⽤已经⾜够
{
}
virtual~center()
{
undo_type::free();
redo_type::free();
}
public:
size_tlimit()//获取撤销命令队列的最⼤命令数量
{
return_limit;
}
voidlimit(size_tLimit)//设置撤销命令队列的最⼤命令数量
{
_limit=Limit;
relimit();
}
voidrelimit()//检查撤销命令队列是否已满,剔除过期命令
{
typedefstd::list
if(_limit>=undo_type::size())return;
while(_limit
{
command*pCmd=static_cast
undo_type::pop_front();
asrt(!=pCmd);
deletepCmd;//命令被释放了
}
}
public:
boolundoable()
{
return!undo_type::empty();
}
}
boolredoable()
{
return!redo_type::empty();
}
voidundo()//撤销
{
if(!undo_type::empty()){
undo_type::back()->undo();
redo_type::push_back(undo_type::back());
undo_type::pop_back();
}
}
voidredo()//执⾏或者重做
{
if(!redo_type::empty()){
redo_type::back()->redo();
undo_type::push_back(redo_type::back());
redo_type::pop_back();
}
}
voidundo(intnumber)//⼀次撤销很多步命令
{
for(inti=0;i
if(undo_type::empty())break;
elundo();
}
}
voidredo(intnumber)//⼀次重做很多命令
{
for(inti=0;i
if(redo_type::empty())break;
elredo();
}
}
//下⾯的record、exec和stop三个函数实现复合命令的⾃动创建,只要保证如下
//的固定格式即可:
//record();//复合命令开始
//exec(命令1);//⼦命令
//exec(命令2);
//record();//⼦复合命令开始
//......
//stop();//⼦复合命令结束
//......
//exec(命令n);
//stop();//复合命令结束
//这种组合⽅式可以任意的嵌套,从⽽实现⽆级撤销
voidrecord(command*pCmd=)
{
history_type::record(pCmd);
redo_type::free();
}
//下⾯的execute也可以单独使⽤,不需要record和stop的配合
voidexecute(command*pCmd)
{
asrt(pCmd!=);
pCmd->redo();
record(pCmd);
relimit();
}
voidstop()
{
history_type&H=static_cast
history_type::iteratorresult=();
history_type::rever_iteratorrit;
rit=std::find((),(),static_cast
rit=std::find((),(),static_cast
std::advance(result,std::distance(rit,())-1);
if(result!=())
{
history_type::difference_typediff;
diff=std::distance(result,());
if(diff==1||diff==2){
(result);
}el{
batch*pBat=newbatch();
pBat->splice(pBat->begin(),H,result,());
pBat->remove(static_cast
record(pBat);
}
}
}
};
可以看出,通过三个基本操作类、⼀个复合操作类、容器类以及命令中⼼类就可以实现任意的撤销和重做功能了!⽽且再也没有必要编写
派⽣⾃命令基类的类,更不需要再实现redo和undo虚函数了,从⽽极⼤的简化的命令编写!所有的其他命令都可以由这⾥的三个基本命令
和⼀个复合命令组合出来:)
来看⼀个使⽤中的实际例⼦:
//矩形类
classRectangle
{
public:
Rectangle():_x(0),_y(0),_width(20),_height(10){}
Rectangle(intx,inty,intw,inth):_x(x),_y(y),_width(w),_height(h){}
private:
int_x,_y,_width,_height;
//下⾯的函数仅仅是为了输出信息⽽准备的
friendstd::ostream&operator<<(std::ostream&s,Rectangle&o)
{
returns<<<
}
};
//圆形类
classCircle
{
public:
Circle():_x(0),_y(0),_radius(20){}
Circle(intx,inty,intr):_x(x),_y(y),_radius(r){}
private:
int_x,_y,_radius;
//下⾯的函数仅仅是为了输出信息⽽准备的
friendstd::ostream&operator<<(std::ostream&s,Circle&o)
{
returns<<<
}
};
//需要⼀个控制⾯板类
classControl
:publicundo::raw::container
,publicundo::raw::container
,publicundo::raw::center//center必须放置在最后
{
public:
template
{
typedefundo::raw::container
undo::raw::center::execute(newundo::raw::create
undo::raw::center::execute(newundo::raw::create
}
template
{
typedefundo::raw::container
undo::raw::center::execute(newundo::raw::modify
}
template
{
typedefundo::raw::container
undo::raw::center::execute(newundo::raw::remove
}
private:
//为了能够判断操作的正确性需要输出容器的信息
template
{
std::cout<
typenameContainer::iteratorit=();
for(;it!=();++it)
{
std::cout<<<<*it<<<<**it<<;
}
std::cout<
}
public:
//下⾯的函数是输出该控制⾯板对象的相关信息的
voiddisplay(constchar*str)
{
typedefundo::raw::container
typedefundo::raw::container
std::cout<<<
std::cout<<<
std::cout<<<
std::cout<
display(,static_cast
display(,static_cast
std::cout<<<
}
};
从上⾯的Control代码可以看出,这⾥的Control就好⽐数据库,⽽container
center就是DBMS了:-)
从⽽处理多种不同的数据类型就相当于增加多种不同的数据库表(container
式如下:
//⾸先应⽤程序中必须有⼀个全局的控制⾯板类对象
ControlC;
Rectangle*idr1=,*idr2=;//两个测试使⽤的矩形类对象标识号
Circle*idc1=,*idc2=;//两个测试使⽤的圆形类对象标识号
y();
//模拟创建矩形和圆形过程
(idr1);
(idr2);
(idc1);
(idc2);
y();
//模拟修改操作
//模拟修改操作
(idr1,Rectangle(1,1,5,10));
(idr2,Rectangle(15,10,50,30));
(idc1,Circle(15,10,50));
(idc2,Circle(5,10,70));
y();
//开始模拟撤销操作
();//撤销1步
y(1);
(4);//再撤销4步
y(4);
();//重做⼀步
y(1);
(4);//重做4步
y(4);
//下⾯的代码演⽰了如何将多个命令合并成为⼀个复合命令的⽅法,
//通过撤销和重做⼀步的⽅式演⽰了复合命令是否真的成为了⼀个
//命令。将多个命令合并成为⼀个命令的⽅法如下:
();//记录开始
(idr1);//删除标识号为idr1的矩形对象
(idr2);//删除标识号为idr2的矩形对象
(idc1);//删除标识号为idc1的圆形对象
(idc2);//删除标识号为idc2的圆形对象
();//记录结束
y();
();//撤销1步
y(1);
();//重做⼀步
y(1);
();//再撤销1步
y(1);
//现在模拟⼀下如何使⽤限制次数的撤销和重做⽅法
(5);//命令中最多只允许撤销和重做5次
y(5);
();//撤销1步
y(1);
(4);//再撤销4步
y(4);
();//重做⼀步
y(1);
(4);//重做4步
y(4);
//在这⾥正好模拟⼀下当重做队列中还有命令的时候再执⾏新的命令的情况
//到这⾥为⽌,重做队列中还有⼀个命令可以重做,但是没有重做。这样将
//会直接添加新的命令到历史纪录的末尾,清除掉这个可以重做的命令,同
//时清理过期的命令,保证命令历史记录不会超过指定的最⼤可撤销步骤数
//量,在这⾥是5次。
(idr2,Rectangle(150,100,500,300));
y();
测试结果:
----------控制⾯板中的初始状态----------
Undo:[0]Redo:[0]
Undo:[0]Redo:[0]
矩形容器:[0]
圆形容器:[0]
==========控制⾯板中的初始状态==========
----------创建了两个矩形对象和两个圆形对象----------
Undo:[4]Redo:[0]
矩形容器:[2](00375758,(0,0,20,10))(003757B8,(0,0,20,10))
圆形容器:[2](00375818,(0,0,20))(003758A8,(0,0,20))
==========创建了两个矩形对象和两个圆形对象==========
----------修改了两个矩形对象和两个圆形对象----------
Undo:[8]Redo:[0]
矩形容器:[2](00375758,(1,1,5,10))(003757B8,(15,10,50,30))
圆形容器:[2](00375818,(15,10,50))(003758A8,(5,10,70))
==========修改了两个矩形对象和两个圆形对象==========
----------撤销1步之后----------
Undo:[7]Redo:[1]
矩形容器:[2](00375758,(1,1,5,10))(003757B8,(15,10,50,30))
圆形容器:[2](00375818,(15,10,50))(003758A8,(0,0,20))
==========撤销1步之后==========
----------撤销4步之后----------
Undo:[3]Redo:[5]
矩形容器:[2](00375758,(0,0,20,10))(003757B8,(0,0,20,10))
圆形容器:[1](00375818,(0,0,20))
==========撤销4步之后==========
----------重做1步之后----------
Undo:[4]Redo:[4]
矩形容器:[2](00375758,(0,0,20,10))(003757B8,(0,0,20,10))
圆形容器:[2](00375818,(0,0,20))(003758A8,(0,0,20))
==========重做1步之后==========
----------重做4步之后----------
Undo:[8]Redo:[0]
矩形容器:[2](00375758,(1,1,5,10))(003757B8,(15,10,50,30))
圆形容器:[2](00375818,(15,10,50))(003758A8,(5,10,70))
==========重做4步之后==========
----------执⾏复合命令----------
Undo:[9]Redo:[0]
矩形容器:[0]
圆形容器:[0]
==========执⾏复合命令==========
----------撤销1步----------
Undo:[8]Redo:[1]
矩形容器:[2](00375758,(1,1,5,10))(003757B8,(15,10,50,30))
圆形容器:[2](00375818,(15,10,50))(003758A8,(5,10,70))
==========撤销1步==========
----------重做1步----------
Undo:[9]Redo:[0]
矩形容器:[0]
圆形容器:[0]
==========重做1步==========
----------再撤销1步恢复原来的状态----------
Undo:[8]Redo:[1]
矩形容器:[2](00375758,(1,1,5,10))(003757B8,(15,10,50,30))
圆形容器:[2](00375818,(15,10,50))(003758A8,(5,10,70))
==========再撤销1步恢复原来的状态==========
----------限制最⼤可撤销步骤为5次----------
Undo:[5]Redo:[1]
矩形容器:[2](00375758,(1,1,5,10))(003757B8,(15,10,50,30))
圆形容器:[2](00375818,(15,10,50))(003758A8,(5,10,70))
==========限制最⼤可撤销步骤为5次==========
----------撤销1步之后----------
Undo:[4]Redo:[2]
矩形容器:[2](00375758,(1,1,5,10))(003757B8,(15,10,50,30))
圆形容器:[2](00375818,(15,10,50))(003758A8,(0,0,20))
==========撤销1步之后==========
----------撤销4步之后----------
Undo:[0]Redo:[6]
Undo:[0]Redo:[6]
矩形容器:[2](00375758,(0,0,20,10))(003757B8,(0,0,20,10))
圆形容器:[1](00375818,(0,0,20))
==========撤销4步之后==========
----------重做1步之后----------
Undo:[1]Redo:[5]
矩形容器:[2](00375758,(0,0,20,10))(003757B8,(0,0,20,10))
圆形容器:[2](00375818,(0,0,20))(003758A8,(0,0,20))
==========重做1步之后==========
----------重做4步之后----------
Undo:[5]Redo:[1]
矩形容器:[2](00375758,(1,1,5,10))(003757B8,(15,10,50,30))
圆形容器:[2](00375818,(15,10,50))(003758A8,(5,10,70))
==========重做4步之后==========
----------重做队列中还有命令的时候再执⾏新的命令的情况----------
Undo:[5]Redo:[0]
矩形容器:[2](00375758,(1,1,5,10))(003757B8,(150,100,500,300))
圆形容器:[2](00375818,(15,10,50))(003758A8,(5,10,70))
==========重做队列中还有命令的时候再执⾏新的命令的情况==========
从上⾯的⽰例代码中可以看出,表达撤销和重做的⽅式⽐上⼀节直接多了,如果将本节的center类也放到头⽂件中,那么使⽤这个撤销和重
做框架将会容易得多。从运⾏的结果看,已经达到了本节开始提出的前两个问题,⾄于第三个问题所提出的任意次实际上也实现了,虽然
表⾯上看只是实现了有限次的撤销和重做⽅案,但是实际上这已经⾜够我们⽬前的所有的应⽤了,不是我不想实现真正的任意次数的撤销
和重做⽅案,⽽是STL的list容器限制了,我们知道list容器⾥⾯的max_size()函数可以得到⼀个list容器允许容纳的最⼤元素数量。由此可
见,真正意义上的⽆限次数并不存在,只要这个数⾜够⼤就可以了。另外计算机的存储容量再⼤,也还是有限的。所以可以这么说:在本
⽂中已经成功的实现了⽆限次的撤销和重做⽅案。
有了前⾯的这些基础性的代码之后,我们就可以处理⼤量的撤销和重做操作了:)但是通常的情况下,我们还是希望能够往command⾥⾯
添加⼀些额外的信息。
例如:为了在图形⽤户界⾯应⽤程序⾥⾯显⽰还可以撤销的命令队列列表以及可以重做的命令列表,这两种情况下都需要给每⼀个命令添
加⼀个额外的名称信息,所以我们将前⾯的command代码修改为下⾯的basic_command代码:
classDefaultExtraData{};//默认的额外信息
template
structbasic_command:publicExtraData//扩展了command类
{
virtual~basic_command(){}//避免内存泄漏
virtualvoidredo()=0;//重做操作,兼任执⾏操作
virtualvoidundo()=0;//撤销操作
};
template<>structbasic_command
{
virtual~basic_command(){}//避免内存泄漏
virtualvoidredo()=0;//重做操作,兼任执⾏操作
virtualvoidundo()=0;//撤销操作
};
特别注意上⾯的代码,为了避免空基类,采⽤了模板特化的静态选择机制,这样就可以根据模板参数选择合适的command类。
从上⾯的代码还可以看出,提供⼀个ExtraData给basic_command就可以完全的改变前⾯提供的command的结构!可以增加属性,也可
以增加⽅法。总之所有可以添加的信息都可以,现在给出⼀个⽰例:
structMyExtraData
{
std::stringname;//命令的名字
};
有了这个额外的数据之后,就可以采⽤这⼀系列的basic_nx模板类来使得命令类可以添加额外的属性信息了;)当然仅仅限于
basic_command模板类及其派⽣类才有这个功能,command仍然没有这个功能!
除了上⾯的给命令添加额外的信息需求之外,还有⼀个⽐较常见的问题会在应⽤中出现:容器类中盛放的对象类型的默认构造函数不能调
⽤的时候,前⾯的container类就⽆能为⼒了!所以在此也要给出⼀种处理这种情况的能⼒!除此之外,前⾯还假定:该容器类⾥⾯的所有
对象的修改都是通过对象类的赋值运算符实现的,这在⼤多数情况下是可以接受的,但是这样毕竟还是⼀个限制,⽽且这是不必要的!
可以处理⾮默认构造函数创建对象的容器类:
template
{
public:
typedefTobject_type;
typedefstd::list
typedefstd::map
typedeftypenameConfig::create_typecreate_type;
typedeftypenameConfig::modify_typemodify_type;
private:
objects_type_objects;//所有的对象都保存在这⾥
unud_type_unud;//第⼆个参数是对应的变量被命令引⽤的数量
public:
//得到标识号的命令引⽤计数
typenameunud_type::mapped_typeud(T**pID)
{
return_(*pID)==_()?0:_unud[*pID];
}
//下⾯两个函数是在命令的创建和删除的时候调⽤的
//配合_unud就是⼀个受管理的引⽤计数智能指针
//只是引⽤该对象的“智能指针”只能是命令
voidincrea(T*ID){_unud[ID]++;}
voiddecrea(T*ID){_unud[ID]--;}
//获得⼀个可以使⽤的对象空间,当没有对象可以回收的时候就创建⼀个
T*generate(T*ud=)
{
T*ptr=ud;
typenameunud_type::iteratorit;
//如果_unud中有引⽤计数为零的对象,直接返回该指针
for(it=_();it!=_();++it)
{
if(0==it->cond&&ud!=it->first)
{
ptr=it->first;
break;
}
}
//如果_unud中没有引⽤计数为零的对象,在_objects中新建⼀个对象
//并返回该新建对象的指针
if(ud==ptr)
{
//Create避免了某些对象不可以通过默认构造函数创建的问题
//Create避免了某些对象不可以通过默认构造函数创建的问题
__back(create_type()());//调⽤Create仿函数创建对象
ptr=&_();
}
returnptr;
}
public:
voidcreate(T*ID)
{
this->inrt(ID);
}
voidmodify(T*ID,constT&v)
{
modify_type()(*ID,v);//通过指定的修改仿函数实现修改过程
}
voidremove(T*ID)
{
this->era(ID);
}
};
容器类增加了⼀个配置(basic_config)参数,配置参数中主要是创建仿函数和修改仿函数!分别解决下⾯的问题:
1.创建仿函数(create)。主要⽤来处理那些默认构造函数不是public属性的类,创建这些对象就只能通过其他的构造函数了!
2.修改仿函数(modify)。主要⽤来处理那些不可以通过赋值运算符修改对象属性的类,修改这些对象就只能通过其他的⽅法了!
有了上⾯的两个模板参数之后就可以使得basic_container类具有极⼤的可扩展性和处理能⼒。下⾯是容器类需要的配置类:
template
{
protected:
structCreate
{
constT&operator()()
{
staticconstTO;//默认构造函数
returnO;
}
};
structModify
{
voidoperator()(T&lhs,constT&rhs)
{
lhs=rhs;//默认采⽤赋值运算符实现修改
}
};
public:
typedefCreatecreate_type;
typedefModifymodify_type;
};
有了上⾯的这种容器类之后就可以处理创建命令特别常⽤的⼀种功能:创建对象的时候能够提供创建参数,在创建对象的时候就对对象进
⾏修改,只是这种修改是不可以撤销的!可以添加额外信息并且可以提供创建参数的创建命令:
template
{
voidinitialize()
{
//下⾯的generate函数主要是在第⼀次执⾏创建命令的时候起作⽤
_ID=_te(_ID);//如果_ID已经被使⽤了,则创建新的对象空间
_(_ID);//容器类的引⽤计数增⼀
}
typedeftypenameContainer::object_typeT;
typedeftypenameContainer::modify_typeM;
public:
basic_create(Container&C,T*&ID):_C(C),_ID(ID)
{
initialize();
}
//经常的时候还需要在创建的时候就赋予初始值
basic_create(Container&C,T*&ID,constT&O):_C(C),_ID(ID)//,_O(O)
{
initialize();M()(*_ID,O);//创建的时候直接赋初始值
}
virtual~basic_create()
{
_(_ID);//容器类的引⽤计数减⼀
}
voidredo()
{
_(_ID);
}
voidundo()
{
_(_ID);
}
private:
basic_create(){}
Container&_C;
T*&_ID;
};
可以添加额外信息的修改命令:
template
{
typedeftypenameContainer::object_typeT;
typedeftypenameContainer::modify_typeM;
public:
basic_modify(Container&C,T*ID,constT&O):_C(C),_ID(ID),_O(O)
{
M()(_OB,*_ID);//备份信息
_(_ID);
}
virtual~basic_modify()
{
_(_ID);
}
voidredo()
{
_(_ID,_O);
}
voidundo()
{
_(_ID,_OB);
}
private:
basic_modify(){}
Container&_C;
T*_ID;
T_O;//修改参数
T_OB;//修改之前的对象备份
};
可以添加额外信息的删除命令:
template
{
typedeftypenameContainer::object_typeT;
public:
basic_remove(Container&C,T*&ID):_C(C),_ID(ID),_IDB(ID)
{
_(_ID);
}
virtual~basic_remove()
{
_(_ID);
}
voidredo()
{
_(_ID);
_ID=;//标识号为NULL表⽰已经被删除了
}
voidundo()
{
_ID=_IDB;//撤销了删除操作,标识号应该复原
_(_ID);
}
private:
basic_remove(){}
Container&_C;
T*&_ID;
T*_IDB;
};
下⾯是三个基本命令的扩展代码的测试⽤例:
typedefundo::basic_container
typedefundo::basic_command
typedefundo::basic_create
typedefundo::basic_modify
typedefundo::basic_remove
CTc;
CT::object_type*PANDAXCL=;
AUTOCXX_ASSERT_EQUAL((&PANDAXCL),0);
CreatecreatePandaxcl(c,PANDAXCL,std::string());
AUTOCXX_ASSERT_EQUAL((),0);
AUTOCXX_ASSERT_EQUAL((&PANDAXCL),1);
//在执⾏其它的命令之前必须保证这个对象已经存在
AUTOCXX_ASSERT_NOT_EQUAL(PANDAXCL,);
();
AUTOCXX_ASSERT_NOT_EQUAL(PANDAXCL,);
//如果上⾯的命令不执⾏的话,后⾯的对这个对象的修改和移除操作都会失败
//调⽤了上⾯的create命令后可以进⾏检查了
AUTOCXX_ASSERT_EQUAL((),1);
AUTOCXX_ASSERT_EQUAL((&PANDAXCL),1);
//尝试create命令的撤销
();//调⽤create命令的撤销操作
AUTOCXX_ASSERT_NOT_EQUAL(PANDAXCL,);
//调⽤了上⾯的create命令的撤销命令之后可以进⾏检查了
AUTOCXX_ASSERT_EQUAL((),0);
AUTOCXX_ASSERT_EQUAL((&PANDAXCL),1);
//尝试create命令的重做操作
();//调⽤create命令的重做操作
//调⽤了上⾯的create命令的撤销命令之后可以进⾏检查了
AUTOCXX_ASSERT_EQUAL((),1);
AUTOCXX_ASSERT_EQUAL((&PANDAXCL),1);
//现在来尝试⼀下modify命令,特别注意:使⽤modify和remove命令的时候
//必须保证相应的对象已经存在了,否则结果就是未知的:)
//下⾯的这个修改命令将ID为PANDAXCL的对象修改为⼤写形式
AUTOCXX_ASSERT_EQUAL((&PANDAXCL),1);
ModifymodifyPandaxcl(c,PANDAXCL,std::string());
AUTOCXX_ASSERT_EQUAL((&PANDAXCL),2);
//调⽤modify的执⾏操作(⼜称重做操作)
();//相当于执⾏操作
//调⽤了上⾯的modify命令的执⾏命令之后可以进⾏检查了
AUTOCXX_ASSERT_EQUAL((),1);
AUTOCXX_ASSERT_EQUAL((&PANDAXCL),2);
AUTOCXX_ASSERT_NOT_EQUAL(*PANDAXCL,std::string());
AUTOCXX_ASSERT_EQUAL(*PANDAXCL,std::string());
//调⽤modify的撤销操作
();
//调⽤了上⾯的modify命令的执⾏命令之后可以进⾏检查了
AUTOCXX_ASSERT_EQUAL((),1);
AUTOCXX_ASSERT_EQUAL((&PANDAXCL),2);
AUTOCXX_ASSERT_EQUAL(*PANDAXCL,std::string());
AUTOCXX_ASSERT_NOT_EQUAL(*PANDAXCL,std::string());
//调⽤modify的重做操作(⼜称执⾏操作)
();
//调⽤了上⾯的modify命令的重做命令之后可以进⾏检查了
AUTOCXX_ASSERT_EQUAL((),1);
AUTOCXX_ASSERT_EQUAL((&PANDAXCL),2);
AUTOCXX_ASSERT_NOT_EQUAL(*PANDAXCL,std::string());
AUTOCXX_ASSERT_EQUAL(*PANDAXCL,std::string());
//再次调⽤modify的撤销操作
();
//调⽤了上⾯的modify命令的执⾏命令之后可以进⾏检查了
AUTOCXX_ASSERT_EQUAL((),1);
AUTOCXX_ASSERT_EQUAL((&PANDAXCL),2);
AUTOCXX_ASSERT_EQUAL(*PANDAXCL,std::string());
AUTOCXX_ASSERT_NOT_EQUAL(*PANDAXCL,std::string());
//下⾯来尝试⼀下remove操作,同modify操作⼀样,执⾏remove操作的时候必须
//保证指定ID的对象已经存在了!
//下⾯的这个remove删除ID为PANDAXCL的对象
RemoveremovePandaxcl(c,PANDAXCL);
AUTOCXX_ASSERT_EQUAL((&PANDAXCL),3);
//调⽤remove命令的执⾏操作
();
//调⽤remove命令的执⾏操作之后来核实⼀下我们想要的结果
AUTOCXX_ASSERT_EQUAL((),0);
AUTOCXX_ASSERT_EQUAL((&PANDAXCL),0);
//调⽤remove命令的撤销操作
();
//调⽤remove命令的撤销操作之后来核实⼀下我们想要的结果
AUTOCXX_ASSERT_EQUAL((),1);
AUTOCXX_ASSERT_EQUAL((&PANDAXCL),3);
AUTOCXX_ASSERT_EQUAL(*PANDAXCL,std::string());
AUTOCXX_ASSERT_NOT_EQUAL(*PANDAXCL,std::string());
//同样你也可以对⼀个命令执⾏很多次,但是需要特别注意上⾯的⼏个顺序问题
//命令可以在对象不存在的时候创建,但是不能创建相同ID的对象两
//次及其以上
//命令必须在指定ID存在的时候进⾏操作,否则程序运⾏意义未知
//命令必须在指定ID存在的时候进⾏操作,否则程序运⾏意义未知
//为了对上⾯的三种情况进⾏保证,才出现了undo⾥⾯的其它的⼏个模版类
可以添加额外信息的复合命令:
template
:publicCommand//是⼀种命令
,publicstd::list
{
typedefCommandcommand_type;
public:
virtual~basic_batch()//当复合命令被释放的时候,所有的⼦命令也要释放
{
this->free();
}
voidrecord(command_type*pCmd)//记录⼦命令,⼦命令必须通过new创建
{
this->push_back(pCmd);
}
public:
virtualvoidredo()//执⾏操作和重做操作
{
std::for_each(this->begin(),this->end(),std::mem_fun(&command_type::redo));
}
virtualvoidundo()//撤销操作
{
std::for_each(this->rbegin(),this->rend(),std::mem_fun(&command_type::undo));
}
voidfree()//⽤复合命令实现命令中⼼的时候,需要这个函数
{
std::for_each(this->begin(),this->end(),kill());//释放⼦命令对象
this->clear();//释放⼦命令对象指针
}
};
下⾯是复合命令的扩展代码的测试⽤例:
可以添加额外信息的命令中⼼:
template
template
template
:publicbasic_undo_bat
,publicbasic_redo_bat
{
private:
size_t_limit;
public:
typedefCommandcommand_type;
typedefbasic_batch
typedefbasic_redo_bat
typedefbasic_undo_bat
typedefundo_typehistory_type;
basic_center():_limit(100)//默认最⼤撤销操作步骤总数为100步,⼀般的应⽤已经⾜够
{
}
virtual~basic_center()
{
}
public:
size_tlimit()//获取撤销命令队列的最⼤命令数量
{
return_limit;
}
voidlimit(size_tLimit)//设置撤销命令队列的最⼤命令数量
{
_limit=Limit;
relimit();
}
voidrelimit()//检查撤销命令队列是否已满,剔除过期命令
{
typedefstd::list
if(_limit>=undo_type::size())return;
while(_limit
{
command_type*pCmd=undo_type::front();
undo_type::pop_front();
asrt(!=pCmd);
deletepCmd;//命令被释放了
}
}
public:
boolundoable()
{
return!undo_type::empty();
}
boolredoable()
{
return!redo_type::empty();
}
voidundo()//撤销
{
if(!undo_type::empty()){
undo_type::back()->undo();
redo_type::push_back(undo_type::back());
undo_type::pop_back();
}
}
voidredo()//执⾏或者重做
{
if(!redo_type::empty()){
redo_type::back()->redo();
undo_type::push_back(redo_type::back());
redo_type::pop_back();
}
}
voidundo(intnumber)//⼀次撤销很多步命令
{
for(inti=0;i
if(undo_type::empty())break;
elundo();
}
}
voidredo(intnumber)//⼀次重做很多命令
{
for(inti=0;i
if(redo_type::empty())break;
elredo();
}
}
//下⾯的record、execute和stop三个函数实现复合命令的⾃动创建,只要保证如下
//的固定格式即可:
//record();//复合命令开始
//execute(命令1);//⼦命令
//execute(命令2);
//record();//⼦复合命令开始
//......
//stop();//⼦复合命令结束
//......
//execute(命令n);
//stop();//复合命令结束
//这种组合⽅式可以任意的嵌套,从⽽实现⽆级撤销
voidrecord(command_type*pCmd=)
{
history_type::record(pCmd);
redo_type::free();
}
//下⾯的execute也可以单独使⽤,不需要record和stop的配合
voidexecute(command_type*pCmd)
{
asrt(pCmd!=);
pCmd->redo();
record(pCmd);
relimit();
}
voidstop()
{
history_type&H=static_cast
typenamehistory_type::iteratorresult=();
typenamehistory_type::rever_iteratorrit;
rit=std::find((),(),static_cast
std::advance(result,std::distance(rit,())-1);
if(result!=())
{
typenamehistory_type::difference_typediff;
diff=std::distance(result,());
if(diff==1||diff==2){
(result);
}el{
batch_type*pBat=newbatch_type();
pBat->splice(pBat->begin(),H,result,());
pBat->remove(static_cast
record(pBat);
}
}
}
};
下⾯是命令中⼼的扩展代码的基本状态的测试⽤例:
typedefundo::basic_command
typedefundo::basic_container
typedefundo::basic_create
typedefundo::basic_modify
typedefundo::basic_remove
CTc;//注意这⾥的容器类仍然是和撤销和重做概念是分离的,实际应⽤的过程可能是整合
//在⼀起的,命令中⼼对象和容器对象是同⼀个对象,通过多重派⽣得到
//在上⾯的代码中没有涉及到和⼀般的应⽤程序相关的类似概念,那么center模版类的出现这是
//给出了这个⾮常近似的概念
undo::basic_center
CT::object_type*PANDAXCL=;
CT::object_type*QQ=;
CT::object_type*BLOG=;
CT::object_type*EMAIL=;
CT::object_type*NETWORK=;
//下⾯⽤center命令中⼼的观点来重新实现上⾯的功能
//注意center对象的execute已经不可以直接使⽤了,为的就是下⾯的这个概念:
//每⼀个操作都是⽴即执⾏的,这和⼀般的应⽤程序⾥⾯的操作概念⾮常类似
AUTOCXX_ASSERT_EQUAL((),0);
e(newCreate(c,PANDAXCL));
AUTOCXX_ASSERT_EQUAL((),1);
e(newCreate(c,QQ));
AUTOCXX_ASSERT_EQUAL((),2);
e(newCreate(c,BLOG));
AUTOCXX_ASSERT_EQUAL((),3);
e(newCreate(c,EMAIL));
AUTOCXX_ASSERT_EQUAL((),4);
//撤销⼀步操作
();
AUTOCXX_ASSERT_EQUAL((),3);
AUTOCXX_ASSERT_EQUAL((),3);
//重做⼀步操作
();
AUTOCXX_ASSERT_EQUAL((),4);
//撤销多步操作
(3);
AUTOCXX_ASSERT_EQUAL((),1);
//重做多步操作
(3);
AUTOCXX_ASSERT_EQUAL((),4);
//撤销多步操作(如果撤销的步骤数量⽐实际的可撤销数量多,多余的部分没有任何效果)
(100);//注意这⾥的实际可撤销步骤只有4步
AUTOCXX_ASSERT_EQUAL((),0);
//重做多步操作
(200);
AUTOCXX_ASSERT_EQUAL((),4);
//撤销多步操作
(3);
AUTOCXX_ASSERT_EQUAL((),1);
//重做操作的步骤数量和先前撤销操作的数量不等
(1);
AUTOCXX_ASSERT_EQUAL((),2);
//再次全部撤销
(100);
AUTOCXX_ASSERT_EQUAL((),0);
//再次全部重做
(100);
AUTOCXX_ASSERT_EQUAL((),4);
//当出现连续进⾏撤销操作的时候
(1);
AUTOCXX_ASSERT_EQUAL((),3);
(1);
AUTOCXX_ASSERT_EQUAL((),2);
(1);
AUTOCXX_ASSERT_EQUAL((),1);
(3);
AUTOCXX_ASSERT_EQUAL((),4);
//当出现进⾏撤销操作的时候⼜开始了新命令的时候,就会把先前已经存在的已
//经撤销过的可重做操作清除也就是说这⾥存在⼀个不可撤销过程,实际上相当
//于我们做事的时候,刚开始做错了,恢复原始状态之后马上进⾏重新选择的过
//程,当然不需要记录之前我们做错过的事情:(
(2);
AUTOCXX_ASSERT_EQUAL((),2);
//这个时候⼜开始了⼀个新的命令
e(newCreate(c,NETWORK));
AUTOCXX_ASSERT_EQUAL((),3);
//在这种情况下考虑重做的过程
(100);//实际上什么也没有做,因为之前缓存的可重做队列被清空了:(
AUTOCXX_ASSERT_EQUAL((),3);
下⾯是命令中⼼的扩展代码的模拟复合命令的测试⽤例:
typedefundo::basic_command
typedefundo::basic_container
typedefundo::basic_create
typedefundo::basic_modify
typedefundo::basic_remove
CTc;//容器对象,每个对象必须放在⼀个同类型的容器中
undo::basic_center
CT::object_type*PANDAXCL=;
CT::object_type*QQ=;
CT::object_type*BLOG=;
CT::object_type*EMAIL=;
CT::object_type*NETWORK=;
//现在来考虑⼀下之前考虑过的batch命令在center中是如何表⽰的
AUTOCXX_ASSERT_EQUAL((),0);
();
e(newCreate(c,PANDAXCL));
e(newCreate(c,EMAIL));
e(newCreate(c,QQ));
e(newCreate(c,BLOG));
e(newCreate(c,NETWORK));
();
AUTOCXX_ASSERT_EQUAL((),5);
//撤销⼀步,实际上就是把上⾯的三个⼦命令组成的⼀个batch命令撤销了
();
AUTOCXX_ASSERT_EQUAL((),0);
//重做⼀步,实际上就是把上⾯的三个⼦命令组成的⼀个batch命令重做了
();
AUTOCXX_ASSERT_EQUAL((),5);
//上⾯的记录过程其实还可以嵌套⼦记录过程,相当于前⾯的例⼦中的⼦batch命令⼀样
();
//没有任何命令也是可以接受的
();
下⾯是命令中⼼的扩展代码的模拟⼦复合命令的测试⽤例:
typedefundo::basic_command
typedefundo::basic_container
typedefundo::basic_create
typedefundo::basic_modify
typedefundo::basic_remove
CTc;//容器对象,每个对象必须放在⼀个同类型的容器中
undo::basic_center
CT::object_type*PANDAXCL=;
CT::object_type*QQ=;
CT::object_type*BLOG=;
CT::object_type*EMAIL=;
CT::object_type*NETWORK=;
//现在来考虑⼀下之前考虑过的batch命令在center中是如何表⽰的
AUTOCXX_ASSERT_EQUAL((),0);
();
{
e(newCreate(c,PANDAXCL));
//上⾯的记录过程其实还可以嵌套⼦记录过程,相当于前⾯的例⼦中的⼦batch命令⼀样
();
{
e(newCreate(c,EMAIL));
();
{
e(newCreate(c,QQ));
e(newCreate(c,BLOG));
e(newCreate(c,NETWORK));
}
();
}
();
}
();
AUTOCXX_ASSERT_EQUAL((),5);
//撤销⼀步,实际上就是把上⾯的三个⼦命令组成的⼀个batch命令撤销了
();
AUTOCXX_ASSERT_EQUAL((),0);
//重做⼀步,实际上就是把上⾯的三个⼦命令组成的⼀个batch命令重做了
();
AUTOCXX_ASSERT_EQUAL((),5);
//由上述测试可以看出,只要center的record函数和stop函数是配对的那么就可以⽆限制的
//进⾏嵌套!当然,如果⼀个batch命令只有⼀个⼦命令的话就没有必要啦:)
//注意加深嵌套层级就意味着性能损失,虽然它数量少的时候微不⾜道,但是⼀旦数量庞⼤
//之后就很明显啦:)
好了,到此为⽌已经成功的完成了命令的扩展,使⽤⽅法并没有多⼤不同:)可以放⼼使⽤了:)
本文发布于:2022-11-22 16:47:57,感谢您对本站的认可!
本文链接:http://www.wtabcd.cn/fanwen/fan/90/354.html
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系,我们将在24小时内删除。
留言与评论(共有 0 条评论) |