Posted on 2009-11-15 18:09
cexer 阅读(12245)
评论(176) 编辑 收藏 引用 所属分类:
GUI 、
pattern
1 开篇废话
我喜欢用C++写 GUI 框架,因为那种成就感是实实在在地能看到的。从毕业到现在写了好多个了,都是实验性质的。什么拳脚飞刀毒暗器,激光核能反物质,不论是旁门左道的阴暗伎俩,还是名门正派的高明手段,只要是 C++ 里有的技术都试过了。这当中接触过很多底层或是高级的技术,像编译时类型检测,运行时代码修改等等,按实现的不同 GUI 涉及的东西是没有边际的。从最开始模仿 MFC,ATL 那样的实现学到很多东西,然后开始看一些开源的著名的 GUI 框架,像 MFC,WTL,SmartWin++,win32gui,jlib2 ,VCF 获得很多启发,到现在似乎有一种已看尽天下 GUI 的感觉。在学习别人的框架和自己的实现过程中,真真实实地感觉自己成长了不少,也有很多感悟。
写到这,我作为轮子制造爱好者,在这里向那些喊着"不要重复制造轮子的"批评家们承认错误。在有那么多好的轮子的情况下,我不值得浪费地球资源,浪费时间精力来自己手工重复打造。但是不值得归不值得,在值得和喜欢之间我还是选择后者。并且人生在世,什么才是值得?我觉得不是拯救人类,为世界和平做贡献,也不是努力奋斗,为地球人民谋福利,而是简单地做自己喜欢的事。
写过的那些代码很多都消失在硬盘的海洋里了,但那些挑灯苦想来的感悟还在。在它们也消失之前,我想利用空闲时间把这些觉得有点用处的经验写出来,正好这个博客也已经快一年没更新了。另外也算是对那些发我邮件的朋友的回应。
我的想法是用一系列日志,按照实现一个 GUI 框架的具体思维递进过程来阐述实现一个 GUI 框架的具体思维递进过程。这样说好像有点递归,简单地解释就是这一系列日志不是想用《记忆碎片》那样错乱的叙述方式来说明一个多有意思的故事,而是尽量简单自然地记录一下写 GUI 框架过程中我的思考。这个递进过程也就是实现一个 GUI 框架的过程,一系列日志之后,我们将会看到一个长得漂亮眼,极富弹性,能干又节约的 GUI 框架。
虽然写的内容都是在 Windows 的 GUI 系统之上,但其原理是触类旁通的,其它基于消息的 GUI 系统也都大同小异。所用的代码也都是阐述原理的,自知绝对达不到商业巨作的水准,所以请不要一上来就批判,要知道我只是想分享而已。之所以先这样说一下,是很害怕那种一上来就"怎么不跨平台啊?","怎么都还看得到HWND啊?","怎么不能用成员函数处理消息啊?"的同志。不喜欢站在高处指着别人的天灵盖说话的人。要知道车轮也是一步步造出来的,不要一开始就想载着MM在高速路上飙豪车像少年啦飞驰。
我认为写技术博客有三种境界,一种是一直在那绘声绘色地描述自己的鱼有多可口多美味,让读者只能垂涎兴叹,一种是授人以鱼的人,闷头就摆出来各种生猛海鲜,让读者难以消化,还有一种境界是授人以渔,怎么钓鱼怎么煮鱼都细细地教给读者。读博客的人有两个境界,一种是只吃鱼的,一上来就只要代码,一种是学打鱼的,想知其然更想知其所以然。读博客时我努力做学打鱼的类型,自己写博客时我会努力做到授人以渔的境界。
另外要说明的是,同样作为尘世中的一个渺小个体,我大多数时候也是在为生存而奔波劳累着的。除此之外剩余的大多时候,更是要玩游戏,K歌,看电影,陪MM,吃喝玩乐。再剩余用来写这个的时候不是很多,有可能这一系列日志一夜写就,也有可能增删五年披阅十载,孩子都叫爸了还没完成。所以请大家不要对这个博客抱很大的期待,就当我是路边街头的表演,你打酱油经过时偶尔瞟过来一眼就好了。
要说的废话终于说完了,下面开始正题。
2 基本概念
基于消息的 GUI 框架的封装,一切都围绕消息展开。复杂的框架设计,明确了需求之后,第一步首先是划分模块。所以,要阐述一个设计过程,第一步也应该是先说清最基本的概念和模块划分,而不是一上来就用广义相对论把读者全部放倒。GUI 框架是干什么的当然是地球人都知道的,但 GUI 框架没有什么已经划分的标准概念,我是按照设计的需要来划分的。如果把 GUI 框架看作一个单位,那么这个单位里最重要的角色有这几个:
- 消息发送者(message sender)
- 消息监听者(message listener)
- 消息检查者(message checker)
- 消息处理者(message handler)
- 消息分解者(message cracker)
- 消息映射者(message mapper)
下面分别说明。
2.1 消息发送者和消息(message sender,message)
消息发送者其实只是在这里友情客串一下,它不在框架设计之内,由操作系统扮演这个劳苦功高的角色,它的工作是将消息发送到消息监听者。在这里面隐含了一下最重要的角色,消息。其实剩余的所有角色说到底也只是死跑龙套的,真正领衔的是消息本身,比如窗口大小改变了的消息,按钮被点击了的消息等等,所有人都高举旗帜紧密团结在它周围进行工作。但消息本身只是一个很简单的数据结构,因为再复杂的 GUI 系统,它的消息也不过是几个参数,所以框架的实现重点在其它的角色。在此之前简单地封装一下消息,一个最简单的封装可能是这样:
1: // 消息封装类
2: class Message
3: {
4: public:
5: Message( UINT id_=0,WPARAM wparam_=0,LPARAM lparam_=0 )
6: :id( id_ )
7: ,wparam ( wparam_ )
8: ,lparam ( lparam_ )
9: ,result ( 0 )
10: {}
11:
12: UINT id;
13: WPARAM wparam;
14: LPARAM lparam;
15: LRESULT result;
16: };
就这样的我们的公司已经有了核心角色了。从概念上讲,我们的这个基于消息的 GUI 框架已经完成了 99% 。然后我们可以以它为中心,按功能划分进行详细讨论,一步步完成那剩余的 1% 的极富创意和挑战的工作。在此之前,先得简单解释一下这几个角色都各是什么概念。消息传送者如上所述,将不在讨论范围内。
2.2 消息监听者(message listener)
消息监听者完成的工作是从操作系统接收到消息,消息是从这里真正到达了框架之内。最简单的消息监听者是一个提供给操作系统的回调函数,比如在 Windows 平台上这个函数的样子是这样:
1: //我是最质朴的消息接收者
2: LRESULT CALLBACK windowProc( HWND window,UINT id,WPARAM wparam,LPARAM lparam );
一个好 GUI 框架当然不能赤祼祼地使用这个东西,我们要在此之上进行面向对象的封装。消息监听者能想到的最自然的封装模式是观察者模式(Observer),这样的模式下的监听者实现看起来像这个样子:
1: //我是一个漂亮的观察者模式的消息监听者
2: class MessageListener
3: {
4: public:
5: virtual LRESULT onMessage( Message* message ) = 0;
6: };
7:
8: //监听者这样工作
9: MessageListener* listener;
10: window->addListener( listener );
11:
jlib2 和 VCF 的实现就是这种模式。但现实当中大多数框架没有使用这种模式,比如 SmartWin++ 和 win32gui ,甚至没有使用任何模式比如 MFC 和 WTL 。我想它们所以不采用观察者模式,有些是因为框架整体实现的牵制,有的则可能是因为没能解决某些技术问题。我们的 GUI 框架将实现观察者模式的消息监听者,所以这些问题我们后面也会遇到,到时候再详述。
2.3 消息检查者(message checker)
消息检查者完成的工作很简单。当收到消息的时候,框架调用消息检查者检查这个消息是否符合某种条件,如果符合,则框架再调用消息处理者来处理这个消息,所以有点类似一个转换者,输入(消息),输出一个(是/否)的值。最简单的检查者可能就是一个消息值的比较,比如:
1:
2: /最简单的消息检查者
3: essage.id == /*消息值*/
4:
5: /比如
6: essage.id == WM_CREATE
展开MFC 和 ATL 的消息映射宏,可以看到它们的消息检查就是用堆积起来的消息值比较语句完成。这就是消息检查者最原始最自然最简单的实现方式,但这种方式缺陷太多。我们的框架将实现一个自动化,具有扩展性的消息检查者,后文详细讨论。
2.4 消息处理者(message handler)
消息处理者是我们最终的目的。GUI 框架所做的一切努力都只是前期的准备,直到消息处理者运行起来那一刻,整个公司才算是真正地运转起来了。消息处理者的具体实现可能是自由函数,成员函数或者其它可调用体,甚至可以是外部脚本,处理完毕可能需要给操作系统返回一个结果。最简单的消息处理者可以就是条语句,比如:
1: //消息处理
2: alert( "窗口创建成功了!" );
3:
4: //返回结果
5: message.result = TRUE;
上面代码中"显示消息框"的动作就是一个消息处理,以上两行代码可视为消息处理者。最常见的消息处理者是函数,比如:
1: //消息处理
2: _handleCreated( message );
代码中的函数 _handleCreated 就是一个典型的消息处理者。消息处理者的实现难处在于,既要支持多样性的调用接口,又要支持统一的处理方式。我们的框架将实现一个支持自由函数,成员函数,函数对象,或者其它可调用体的消息处理者,并且这些可调用体可以具有不同参数列表。后文将进行消息处理者的详细讨论。
在这里有必要再说明一下。一个判断语句的大括号之前(判断部分)是消息检查的动作,大括号之内(执行部分)是实际的消息处理。因此一个判断语句虽简单,却包含消息检查者和消息处理者,以及另外一个神秘的部分(见后文),一共三个部分。代码像这样:
1: if ( //消息检查者 )
2: {
3: //消息处理者
4: }
比如下面的代码:
1: // message.id == WM_CREATE 是消息检查者
2: // _handleCreated( message )是消息处理者
3:
4: if ( message.id == WM_CREATE )
5: {
6: _handleCreated( message );
7: }
8:
2.5 消息分解者(message cracker)
消息分解者是为消息处理者服务的。不同的消息处理者需要的信息肯定不一样,比如一个绘制消息(WM_PAINT)的消息处理者可能需要的是一个图形设备的上下文句柄(HDC),而一个按钮点击消息(BN_CLICK)的消息处理者则可能需要的是按钮的ID,它们都不想看到一个赤祼祼的消息杵在那里。从消息中分解出消息携带的具体信息,这就是消息分解者的工作。最简单的消息分解者可能是一个强制转换,比如:
1: // WM_CREATE 消息参数分解
2: CREATESTRUCT* createStruct = (CREATESTRUCT*)message.lparam;
3:
4: // WM_SIZE 消息参数分解
5: long width = LOWORD( message.lparam );
6: long height = HIWORD( message.lparam );
上面的的代码虽然简单但 100% 完成了消息分解的任务,所以它也是合格的消息分解者。我的框架将实现一个自动化,可扩展的消息分解者。后文将以此为目标进行详细讨论。
2.6 消息映射者(message mapper)
消息映射者是最直接与框架外部打交道的部分,顾名思义,它的工作就是负责将消息检查者与消息处理者映射起来。最简单的映射者可以是一条判断语句,这个判断语句,如代码所示:
1: // if 语句的框架就是一个消息映射者
2:
3: // 消息映射者
4: if ( /*消息检查者*/ )
5: {
6: /*消息处理者*/
7: }
1: // if 语句将消息检查者 message.id==WM_CREATE 和消息处理者 _handleCreated(message) 联系起来了
2: if ( message.id == WM_CREATE )
3: {
4: _handleCreated( message );
5: }
上面的代码 的if 语句中,判断的部分是消息检查者,执行的部分是消息处理者。if 语句把这两个部分组成了一个映射,这是最简单的消息映射者。到这里可以发现,这个简单的 if 语句有多不简单。它低调谦逊但独自地完成了很多工作,就像公司的小张既要写程序,又要扫地倒茶,还义务地给女同事讲笑话。MFC 和 WTL 的消息映射宏展开就是这样的 if 语句。像 jlib2 那样的框架,虽然处理者都虚函数,但在底层也是用 if 语句判断消息然后来进行调用的。当然还有华丽一点的消息映射者,像这样:
1: // 华丽一点的消息映射者
2: window.onCreated( &_handledCreated );
这个 onCreated 也是一个消息映射者,在它的内部把 WM_CREAE 消息和 _handleCreated 函数映射到一起,这种方式最有弹性,但实现起来也比宏和虚函数都要困难得多。SmarWin++ 就是使用的这种方式,它的消息映射者版本看起来一样的阳光帅气,但内部实现有些细节稍嫌猥琐。我们的 GUI 框架将实现一个看起来更美,用起来很爽的消息映射者像这个样子:
1: // 将消息处理者列表清空,设置为某个处理者
2: // 可以这样
3: window.onCreated = &_handleCreated;
4: // 或者这样
5: window.onCreated.add( &_handleCreated );
6:
7: // 在消息处理者列表中添加一个处理者
8: // 可以这样
9: window.onCreated += &_handleCreated;
10: // 或者这样
11: window.onCreated.add( &_handleCreated );
12:
13: // 清空消息处理者列表
14: // 可以这样
15: window.onCreated --;
16: // 或者这样
17: window.onCreated.clear();
值得说一下,这种神奇的映射者是接近零成本的,它没有数据成员没有虚函数什么都没有,就是一个简单的空对象。就像传说中的工作能力超强,但却不拿工资,不泡公司MM,甚至午间盒饭也不要的理想职员。在后文当中会具体详述这个消息映射者的实现。
3 结尾废话
到目前为止我们的框架已经完成了 99% 。下篇准备开始写最简单的消息检查者,但说实话我也不知道下一篇什么时候开始。看看上一篇日志,竟然是一年前写的,这一年内发生的事情很多,但自己浑浑噩噩地的好像一眨眼就到了现在,看着 CPPBLOG 上的好多其它兄弟出的很多很有水准的东西,心里真是惭愧。昨天看了《2012》,现在心里还残留有那种全世界在一间瞬间灰飞烟灭的震撼,2012年也不远了,我也赶紧在地球毁灭之前加把油把这些日志写完了吧。不管怎么样,今天哥先走了,请不要迷恋哥。
Feedback
# re: GUI框架:谈谈框架,写写代码 回复 更多评论
2009-11-15 18:21 by
占个沙发,对这个很好奇:
"值得说一下,这种神奇的映射者是接近零成本的,它没有数据成员没有虚函数什么都没有,就是一个简单的空对象。就像传说中的工作能力超强,但却不拿工资,不泡公司MM,甚至午间盒饭也不要的理想职员。"
# re: GUI框架:谈谈框架,写写代码 回复 更多评论
2009-11-15 18:24 by
@OwnWaterloo
谢谢沙发,后面会详细说说这个消息映射者的实现
# re: GUI框架:谈谈框架,写写代码 回复 更多评论
2009-11-15 18:38 by
如果单就消息的派发,那么一个gui框架可以很容易实现,但是要很好的管理窗口之间的关系,很好的处理键盘加速键,鼠标等这些交互就麻烦了,考虑消息的过滤等。
# re: GUI框架:谈谈框架,写写代码 回复 更多评论
2009-11-15 18:40 by
从这2行代码来说:
window.onCreated.add( &_handleCreated );
window.onCreated.clear();
从UINT message 到 onCreate 的这个分派(dispatch)过程,无论怎样都必定是一个运行时解析过程。
无论使用何种C++高级技巧,也不可能让它变为编译时dispatch。
因为message是一个运行时才能得到的值……
只是,这个dispatch的工作,可能不是由window所在的class完成,而是其他某个层次完成。
总之,这个层次一定存在,这个工作(运行时dispatch)一定免不了。
我猜得对吗……
接下来,框架将message 分派到onXXX之后,客户再将onXXX转发(forwarding)自己的handler这个过程,我相信是可以编译时确定的。
——因为我看过你以前的一些实现~_~
但是,编译时确定,就意味着运行时不可更改。
如果要运行时可更改 —— 如上面2行代码 —— 就一定需要一个"可调用体"(callable)来作handler的占位符(placeholder)。
是这样吗?
而C/C++里面,完全不占用一点空间的callable…… 是不存在的,对吧?
所以我很好奇window是如何零成本完成映射与转发,并且是一个空对象的。
映射肯定需要在某个地方做,可能不是window。
运行时可更改转发目的地而不使用数据, 好像…… 太不可思议了……
# re: GUI框架:谈谈框架,写写代码 回复 更多评论
2009-11-15 18:48 by
@OwnWaterloo
这个消息映射者的实现没有数据成员,没有虚函数存在,它其实就是一个调用,所以它也是有时间成本的。是的,所以我说是接近0成本,而不是真正的0成本。毕竟世界上没有那样传说的员工。但如果好的编译期可以轻松优化掉这个小小调用。
你说不可思议,那倒没到那个境界哈。你可以看看《Imperfect C++》当中的method_property的实现,跟那个很类似,不过他的实现不符合C++标准,应范围有限。
用这种消息映射者的方式,我也实现了值主义的属性,比如:
window.size = Size( 100,100 );
Size size = window.size;
从理论上来说,也接近0成本的。
# re: GUI框架:谈谈框架,写写代码 回复 更多评论
2009-11-15 18:53 by
@OwnWaterloo
你说的“接下来,框架将message 分派到onXXX之后,客户再将onXXX转发(forwarding)自己的handler这个过程,我相信是可以编译时确定的。
——因为我看过你以前的一些实现~_~”
你说的这个是以前另一个版本的框架了,跟这个完全不一样了哦。你看那份代码,它的消息映射是编译期自动进行的,映射者,检查者,分解者三个角色都是由一个东西全部完成的。我将写的这个版本不一样,是完全分离的实现,没有那种编译期映射的功能,但运行时映射可以获得更大的扩展性。
可惜啊,以前那个版本的框架代码我已经找不到了。你那里有?
你说的“以我很好奇window是如何零成本完成映射与转发,并且是一个空对象的。映射肯定需要在某个地方做,可能不是window。运行时可更改转发目的地而不使用数据, 好像…… 太不可思议了……”
看来你有点误会我的意思了,肯定内存当中是有一个 std:map 之类的映射数据存在的。我说的0成本指的是 window.onCreated 这个成员的实现。
# re: GUI框架:谈谈框架,写写代码 回复 更多评论
2009-11-15 18:57 by
@cexer
哦 原来是property模拟。
window.size
window.onCreated
是一个,呃,代理(proxy)对象?
size是一个代理,有一个=操作符和转换函数:
operator=(const Size& s) {
this->window->setSize(s);
}
operator Size() const {
return this->window->getSize();
}
这个代理对象也是要占空间的吧?
呃…… 还是等你的源代码与分析吧…… 不瞎猜了……
看来你也是难得抓到一个更新blog的闲暇……
# re: GUI框架:谈谈框架,写写代码 回复 更多评论
2009-11-15 18:58 by
@cexer
没有完整的,就是你blog上发的一些片段……
# re: GUI框架:谈谈框架,写写代码 回复 更多评论
2009-11-15 19:01 by
@OwnWaterloo
你说“哦 原来是property模拟。”
嗯就是这样的。
你说“这个代理对象也是要占空间的吧?”
理论上来说不要占空间的,但实际是占用一个字节的。因为C++标准规定不允许存在0空间占用的成员变量,因为那会造成 &object.member1,&object.memeber2 两个地址完全相同的情况。
# re: GUI框架:谈谈框架,写写代码 回复 更多评论
2009-11-15 19:07 by
@WXX
你说“如果单就消息的派发,那么一个gui框架可以很容易实现,但是要很好的管理窗口之间的关系,很好的处理键盘加速键,鼠标等这些交互就麻烦了,考虑消息的过滤等。”
各有难处,但我觉得后者更容易一点,以前我也都做过一些。我觉得好的框架应该是在所有东西之前的。先有骨架,然后再说高矮胖瘦。长得是否高大健壮,阳光帅气,就先看骨架如何了。
# re: GUI框架:谈谈框架,写写代码 回复 更多评论
2009-11-15 19:08 by
@OwnWaterloo
你说“没有完整的,就是你blog上发的一些片段……”
我找找看,找到了也给你一分。那些片段隐藏了实现的,看不出什么有价值的东西。
# re: GUI框架:谈谈框架,写写代码 回复 更多评论
2009-11-15 19:43 by
我去看了看Imperfect C++。因为是英文的,以前只看了一部分就停了……
我觉得前面都写得不错,ABI、mangling这些问题确实是很困扰……
但property…… 我觉得这并不算C++的Imperfection。
完全是被其他语言给蒙蔽了。
一门语言就应该有一门语言的样子。
比如用C,如无绝对必要,就不应该模拟出虚函数与异常机制。
书里面提到的7种优势,除了最后一种值得考虑,其他全都不值得称作优势。
property,C#中的,或者imperfect cpp 里面看起来的样子,无非就是让如下语法:
o.p( value ); o.set_p( value );
value = o.p(); value = o.get_p();
变成如下语法:
o.p = value;
value = o.p;
如果C++直接提供property的语法,那当然是好事。
但不提供,也不见得编程就进行不下去了,也没有绝对必要去模拟出这种东西。
书中提到的7个好处,除了最后1个,其他都不是property带来的好处。
1. 读写控制
非property一样可以实现读写控制
2. 内外表示转换
这依然是由Date的算法实现,而不是property的功劳。
不通过property,一样可以实现time_t作为内部表示,对外提供int get_xxx()
3. 验证
这个更搞笑了。
就在书中这句话的下面列出的代码,就明显说明对day的验证是通过:
void Date::set_DayOfMonth(int day);
完成的。
完全没有property的半分功劳。
4. 没看太明白
书中的意思是, Date.Now是一个缓存,Date::get_Now()是一个计算?
其实,这里真正需要的是"两种语意",而不是property。
这两种语意是:
Date::getNow();
Date::updateNow();
书中只是将getNow的语意,用property来表现;get_Now同时完成updateNow和getNow。
5. 可用性?
We didn't have to write date.get_Month(), just date.Month.
少敲点键盘而已……
如果利用重载,用void Month(value)代表set,用value Month(void)代表get,少敲的,就是一个调用操作符……
6. 不变式?
没看太明白。
没有property,也可以维护object的不变式,是吧?
只要将数据hide,通过public interface去操作,就可以了。
7. 范型编程
我觉得就只有这个说到点子上了。 而这个也就是模拟property的真正的作用:改变语法。
template<typename P>
void distance(const P& p1, const P& p2 ) {
假设这是一个计算2个点欧式距离的函数。 它已经通过:
p1.x;
这种语法,而不是p1.x(); 这种语法写成了。
}
如果新实现一个class, 想通过p1.x 这种语法访问,同时还要有读写控制等, 就需要模拟出property。
这可能是property真正的用处。
换句话说,真的存在很多情况,使得我们"不得不"使用如下语法:
o.p = value;
value = o.p;
来代替如下语法:
o.set_p( value );
value = o.get_p();
吗?
对这种情况,property模拟才是有价值的。
对很多情况,setter/getter的语法和property的语法都是可行的。
如果本身提供了property语法的语言,那使用2者之一都没关系。
但C++没有提供。对这种情况也非要去模拟出property的语法,就有点…… 为了property而property的味道了。
我继续去看书中怎么实现的……
我以前了解的实现方式,每一个proxy至少得带一个指针……
这消耗还是蛮严重的……
你怎么看?
# re: GUI框架:谈谈框架,写写代码 回复 更多评论
2009-11-15 19:57 by
我晕……
侵入式的property……
强大……
# re: GUI框架:谈谈框架,写写代码 回复 更多评论
2009-11-15 20:13 by
@OwnWaterloo
你说“我去看了看Imperfect C++。因为是英文的,以前只看了一部分就停了……我觉得前面都写得不错,ABI、mangling这些问题确实是很困扰……我继续去看书中怎么实现的……”
在C++里面属性只是看起来很美的东西,其实用性和想像的有差距。你说得对,用方法能实现所有的功能,所以我的GUI框架已经去掉了属性支持。
你说“我以前了解的实现方式,每一个proxy至少得带一个指针……这消耗还是蛮严重的……”
不一定得带指针的。比如《C++ Imperfect》里实现的属性是不需要带指针的,但它不被C++标准支持,应用有限制。我也实现了不需要带指针的属性,同时也是跟C++标准相容的,以后会说一下这个实现。
# re: GUI框架:谈谈框架,写写代码 回复 更多评论
2009-11-15 20:31 by
@cexer
Imperfect C++中的差不多看明白了……
proxy是包含在object中,通过proxy在object中的offset得到object的地址……
# re: GUI框架:谈谈框架,写写代码 回复 更多评论
2009-11-15 20:48 by
你还是讲讲2012比较好。
# re: GUI框架:谈谈框架,写写代码 回复 更多评论
2009-11-15 22:02 by
@cexer
我想到一个几乎没有消耗的方案……
我再完善一下……
# re: GUI框架:谈谈框架,写写代码 回复 更多评论
2009-11-15 22:07 by
@OwnWaterloo
你说“我想到一个几乎没有消耗的方案……我再完善一下……”
加油,出来了别忘了写出来大家分享。
# re: GUI框架:谈谈框架,写写代码 回复 更多评论
2009-11-15 22:08 by
@空明流转
好的,是这样的,《2012》说的是地球毁灭的故事,好了我说完了。
哈哈!
# re: GUI框架:谈谈框架,写写代码 回复 更多评论
2009-11-15 22:33 by
@cexer
关键点在于:"如何通过proxy地址,得到object地址"。
1. 可以给proxy放入一个object的指针。每个proxy消耗一个指针的空间。
太浪费了……
2. Imperfect C++中利用offset来计算object的地址,使得每个proxy不含任何域。
但C++为了区别各个proxy,会给每个proxy进行填充,依然会造成消耗。
3. 避免proxy在Record中被填充
假设有如下记录:
Record
{
proxy1 property1;
proxy2 property2;
proxy3 property3;
...
}
其中proxy1,proxy2,proxy3, ... 是空结构。
Record r;
r.property1;
r.property2;
r.property3;
...
在C/C++中,如果想要property1、2、3、... 不占空间,也就是它们必须拥有相同的地址。
也就是说…… Record 是union……
4. 避免Record在class中被填充
即使这样,整个union依然是会被填充……
所以,可以将class的数据和union放在一起,就真的不消耗任何空间了……
(如果类本身不需要数据,那这个类也是会被填充的,proxys依然没有消耗多余的空间)
代码:
class date
{
private:
struct second_proxy
{
operator int() const
{
const void* o = this;
int sec = static_cast<const date*>(o)->get_second();
printf("%d second_proxy::operator int(%p);\n",sec,o);
return sec;
}
date& operator=(int sec)
{
void* o = this;
printf("second_proxy::operator=(%p,%d);\n",o,sec);
date* d = static_cast<date*>(o);
d->set_second(sec);
return *d;
}
};
public:
union
{
time_t time_;
second_proxy second;
};
date()
{
printf("date::date(%p);\n",(void*)this);
time(&time_);
}
int get_second() const
{
int sec = localtime(&time_)->tm_sec;
printf("%d date::get_second(%p);\n",sec,(void*)this);
return sec;
}
void set_second(int sec)
{
printf("date::set_second(%p,%d);\n",(void*)this,sec);
tm t = *localtime(&time_);
t.tm_sec = sec;
time_ = mktime(&t);
}
};
second_proxy hour和date的数据time_t time_放在同一个union中。
这样,它们拥有相同的地址。
在second_proxy::operator int() const; 中,可以直接将this转换到const date* ……
就这样得到了…… object的地址……
在C中,必须给那个union取一个名字:
struct date
{
union
{
time_t time_;
second_proxy second;
} must_have_a_name;
};
struct date date;
date.must_have_a_name.second; 必须这样访问……
C++中有匿名联合,所以可以直接:
date d;
d.second;
这样的坏处也很明显……
客户代码可以访问 d.time_; 什么都完蛋了……
我再想想怎么隐藏……
# re: GUI框架:谈谈框架,写写代码 回复 更多评论
2009-11-16 02:48 by
关于隐藏time_成员……
1.
class C
{
public:
union
{
proxy1 property1;
proxy2 property2;
proxy3 property3;
// private: // msvc和gcc这里是不能加入private的(具体要查标准)
}
};
2.
class C
{
public:
union
{
proxy1 property1;
proxy2 property2;
proxy3 property3;
}
private:
struct impl impl_; // 一旦不放在union中
// properties就可能会被填充
};
仔细选择union的位置,可能会起到减少总大小的效果,比如:
class C1 {
char c_;
public:
union { /* properties */ };
private:
int i_;
};
class C2 {
int i_;
char c_;
public:
union { /* properties */ };
};
因为char c_;的附近本来就会被填充,properties刚好占据那个位置。
如果这样,就可能会很糟糕:
class C3 {
char c_;
int i_;
public:
union { /* properties */ };
};
C++标准没有说class的layout应该怎么安排。
每个access secion中的data,必须按声明顺序。
但不同access secion之间,可以打乱顺序 —— 这个也许会有一些帮助。
第2种方案依然需要计算offsetof…… 这是很不靠谱的……
3.
作为base。
struct properties
{
union
{
proxy1 property1;
proxy2 property2;
proxy3 property3;
// ...
}
};
class C : public properties
{
// data
};
这样,可以很容易计算出C的位置:
void property_proxy::operator=( ... ) {
void* p = this;
C* c = static_cast<C*>( static_cast<properties*>(p) );
}
编译器会正确计算出C*,即使在多继承下也行。比如:
class C : other , public properties {};
但是,在msvc和gcc上,properties 都不会执行空基类优化…… 很囧……
仔细安排data的布局(在这2款编译器上,将小对齐的数据放前面),也会影响整个类的大小。
2和3的方案,如果能钻到空子,就可以几乎没有代价的实现property。
空子就是说,那个class本来就有一些需要被填充的空洞……
如果没有空可钻…… 或者没有正确安排位置…… 会多占用一些大小。
额外大小不超过class中所有域中需要最大对齐那个成员的大小。
额外大小和properties的数量是无关的, 多少个properties都占用这么多额外大小, union嘛……
4.
还有一种方案。
其实上面的date,缺陷在于time_t time_; 这个成员会被外界访问。
可以这样:
class date {
class impl {
time_t time_;
friend class date;
};
public:
union
{
impl impl_;
proxy1 property1;
proxy2 property2;
proxy3 property3;
}
// ...
};
这样,客户代码可以访问的,只有impl_这个名字。
但是几乎不能拿它作任何事情。
连它的类型名 —— date::impl —— 都是不可访问的。
这个方案的缺陷就是 —— 通常必须从头设计一个类型。
union中不能放非pod。只能从内建类型开始,构造一些类。
比如已经有某个非pod的类,C,想将C作为一个成员,实现Cex:
class Cex
{
union
{
C impl_; // 不行
}
};
只能用上面的property base或者property member……
5.
范化工作
这个…… 再说吧……
如果能有更好的布局方案…… 范化工作就白做了……
# re: GUI框架:谈谈框架,写写代码[未登录] 回复 更多评论
2009-11-16 09:42 by
针对游戏的每帧更新UI,这种消息结构是没有什么太大价值,一堆回调继承函数会迅速膨胀代码量。
微软用消息做UI最大的好处,是效率。可是当显卡,CPU如此快速,为什么不寻找另外一种UI解决方式?
# re: GUI框架:谈谈框架,写写代码 回复 更多评论
2009-11-16 12:45 by
之前我已经做过一个发在我的页面上了,其实最麻烦的事,Windows所要求的标准界面行为API几乎都没实现,而且很杂,举个例子:
如果有一个存在Default Button的窗口,你需要:
在没有其他按钮获得焦点的情况下,将Default属性设置给那个按钮
在有按钮获得焦点的情况下,将Default属性设置给有焦点的那个按钮
按下Enter的时候,窗口自动激活带Default属性的按钮的事件
还有譬如如何消除task bar上面的窗口按钮等等,或者往菜单加上图片(API绝对没支持,全部都是owner draw上去的!!!),或者shortcut(菜单的Ctrl + C什么的,也是你自己写上去而不是系统自带的,而且你负责关联他们),或者Tab转移焦点啦(WS_TABSTOP完全没用,那是给你实现转移焦点的时候去获取控件的属性然后决定要不要把焦点给它),或者容器被disable之后上面的控件也要跟着显示成disable一样啦,还有Vista以下版本你把一个checkbox放在groupbox里面是黑掉的,微软证实了这个是bug,不过他只在Vista修了。诸如此类,林林总总,你慢慢处理……
# re: GUI框架:谈谈框架,写写代码 回复 更多评论
2009-11-16 12:56 by
@OwnWaterloo
光有属性还不够,一般来说一个完整的属性应该是:
a=Object.Property;
Object.Property=b;
Object.Property.OnChanging+=functor;
Object.Property.OnChanged+=functor;
//仅本类可用
Object.Property.Self.OnChanging+=functor;
Object.Property.Self.OnChanged+=functor;
当一个Property是int的时候,你要重载完所有操作符,才能让Object.Property1 + Object.Property2这样子的表达式成立,这一点不知道你想到了没。
所以为了提供更加好用的Property,你还得实现一个Ref<int>,让Ref<int>同时兼容int&和&Object.Property,并且Ref<int>提供所有操作符重载。同时,如果Property和Ref<X>是一个类的话,最好带上->以便访问成员函数。
在C++提供property是一件吃力不讨好的事情。还是罢了,之前试过,不值得。
# re: GUI框架:谈谈框架,写写代码 回复 更多评论
2009-11-16 12:59 by
@foxriver
请使用.NET的WPF,这就是“当显卡,CPU如此快速”的时候的“另外一种UI解决方式”。
# re: GUI框架:谈谈框架,写写代码 回复 更多评论
2009-11-16 13:12 by
@陈梓瀚(vczh)
我在上面的评论也说了,在C++中没有绝对必要,还是不要模拟这个东西了。
没有property,生活一样可以继续。
至于 o.p1 + p.p2, &p.p1, 等模拟,并不是重点。
proxy本来就和被proxy的对象有所区别。
视你需要模拟的程度来决定编程的复杂度,或者发现某种特性无法实现。
proxy的重点在于如何高效的在proxy中得到被proxy对象。
实现了这个功能点,才是其他op=,op T(), op +, ref proxy的前提 —— 这些技术基本属于滥大街了 —— 去看vector<bool> —— 所以我对这些没什么兴趣。
之所以重新开始研究这个技巧,是因为Imperfect C++中提到了一种代价很低的方案。
我觉得这个有点意思,所以继续深入了一下。
# re: GUI框架:谈谈框架,写写代码 回复 更多评论
2009-11-16 13:19 by
@陈梓瀚(vczh)
还是用代码说话吧……
上面的date的代码,重点在于这2行:
union { /* ... */ };
哦,还有一行在我的测试代码中,并没有抄出来:
typedef int assume[sizeof(date)==sizeof(time_t)?1:-1];
这2行才是我想表达的重点 —— 没有额外的空间消耗,实现property。
写上面那些代码,为的是展示上面那个重点,而不是下面这种烂大街的:
operator = (int);
operator int() const;
这些技术早就被人讨论得七七八八了,相当无聊……
这一点不知道你看出来了没?
# re: GUI框架:谈谈框架,写写代码 回复 更多评论
2009-11-16 13:35 by
@OwnWaterloo
当且仅当好用,才有继续考虑有没有消耗的必要。一个高效的、会制造麻烦的解决方案一般是不被接受的。
# re: GUI框架:谈谈框架,写写代码 回复 更多评论
2009-11-16 13:45 by
@OwnWaterloo
谢谢你分享你的思考,真的很有创意。建议你可以把你的思考写到你的博客里,让更多的人看到。
@陈梓瀚(vczh)
高效的,不会制造麻烦的东西,也是从不高效的,会制造麻烦的东西进化来的。所以我觉得只要在思考都是有价值的。
# re: GUI框架:谈谈框架,写写代码 回复 更多评论
2009-11-16 13:50 by
@陈梓瀚(vczh)
1.
如果一个property需要消耗一个指针的代价,我认为实现得再好用都是不可接受的…… 高得有点离谱。
很容易一个类的property的消耗比类本身还要高。
[data of instance]
[pointer]
[pointer]
[pointer]
[pointer]
....
而且所有pointer都指向同一个地方……
而且上面也提到,真正需要模拟出property的,是类似于point、vector、matrix这种类。
p.x比p.x()更自然。
让这种类附加额外数据,也是让人很难接受的。
2.
是否好用,你可以另外开文章讨论。
只是你提到的那些使得proxy好用的技术,都提不起我的兴趣。
理由很简单 —— 这种文章太多,写出来都不好意思和人打招呼。
我关心的仅仅是proxy如何高效得到被proxy对象 —— 实现proxy的前提。
我以前见过的,包括我自己想到的,都是上面one property one pointer的方案。imp cpp中提到的让我确实有眼前一亮的感觉。
而 op T(), op= (T), ref T op + 这些技术,只让我觉得涨眼睛 —— 看太多,早腻歪了。
如果你的重点依然在proxy如何更仿真上,可能很难提起我的兴趣。
如果你有兴趣思考proxy如何得到被proxy的object上,或者有一些方案,想法,欢迎分享出来~_~
"当且仅当好用,才有继续考虑有没有消耗的必要。一个高效的、会制造麻烦的解决方案一般是不被接受的。"
这句话绝对说得过于绝对了。
如果你用的是C++、而不是C#,效率绝对不是可以不挂在心上的事情。
为什么你要用C++,而不是用编程更方便,语法糖更多的C#?好好想想吧。
# re: GUI框架:谈谈框架,写写代码 回复 更多评论
2009-11-16 14:02 by
@cexer
我再想想如何做得更范化一些。
减少需要编写property的人的重复劳动。
但是遇到了困难……
手工写union base, union member没什么问题。
要将其做成模板…… 会遇到先有鸡还是先有蛋的问题……
比如:
proxy< ..., getter , ... >
实例化一个property:
proxy< ..., &date::get_second, ... >; 这里需要date的定义?
而date的定义无论是union base,union member又需要先实例化proxy< ... &date::get_second, ... >;……
很囧……
手工写没问题是因为:
struct second_proxy {
这里仅仅声明其布局就可以了
};
然后定义date类
然后实现proxy::op ...
这里才需要引用到date::&getter
我再想想……
# re: GUI框架:谈谈框架,写写代码 回复 更多评论
2009-11-16 14:50 by
准备把你的博客加到我的友情链接里,
很喜欢您的风格...
呵呵,
是不是好多好多程序员都很像啊,
还是好多本来就很像的人都选择了程序员这个
独特的
浪漫的
理性的
(倾向于)创意的
...
职业啊
# re: GUI框架:谈谈框架,写写代码 回复 更多评论
2009-11-16 15:26 by
@OwnWaterloo
效率不是一个可以忽略可用性的理由。我们需要的是一个在效率和外观上都达到一定高度的想法,而不是一个因为效率而造成麻烦的想法。即使是C++,我们也要把它变得更方便。而且刚才的那些操作符重载其实跟你那个union没有冲突,这无论有兴趣与否,都是一个要解决的问题。如果别人解决了一个带多余指针的好用的属性,你解决了一个不带多余指针的不好用的属性,那都是没意义的。
# re: GUI框架:谈谈框架,写写代码 回复 更多评论
2009-11-16 15:28 by
@OwnWaterloo
不过用了union,而跟你刚才那样一个属性就要定义一个类,而不是一种类型的属性定义一个类(或者干脆就只有少数几个属性的类然后到处使用——这个要求比较高了一点),我觉得比多一个指针更加不能接受。运行时代价跟开发代价都是需要一并考虑的。
# re: GUI框架:谈谈框架,写写代码 回复 更多评论
2009-11-16 15:29 by
@OwnWaterloo
在关心程序的质量的同时,也要同时关心一下如何将广大程序猿在C++的水深火热之中解放出来。
# re: GUI框架:谈谈框架,写写代码 回复 更多评论
2009-11-16 15:59 by
@陈梓瀚(vczh)
"如果别人解决了一个带多余指针的好用的属性,你解决了一个不带多余指针的不好用的属性,那都是没意义的。"
我那个是没有意义的?
我那个不可以继续实现你所说的那些花哨的用法吗?(为什么说花哨,我下面会说明)
我已经说过很多次了,"通过proxy得到被proxy的对象"是实现proxy的前提。
必须先满足这个前提才能继续实现proxy的其他技巧。
用union会影响实现其他技巧吗? 依然可以。
难道我要把所有我会的技巧都展现一下吗?
我不喜欢这样[color=red]显摆[/color]。
或者说,我也喜欢显摆;但[b]我不喜欢显摆[color=red]众人皆会[/color]的技巧[/b]。
分享一些别人明显了解的技术,只会[b][color=red]涨[/color]别人眼睛[/b]。
我只想展示如何高效实现proxy所需前提这一技巧,如是而已。
btw:为什么我说你那些是花哨的技巧。
首先引入一个概念:"总线式的设计"。
假设你需要实现一个函数f,f需要一个字符串输入(暂不考虑wchar_t)。
如何设计这个输入参数???
1. f(const char* c_style_string);
2. f(const char* first,ptrdiff_t length);
两者取其一,可能1用得更多一些。
然后,其他所有的String,都向这个"总线" —— const char*靠拢。
比如std::basic_string要提供c_str(), CString要提供operator const char*()。
为什么要这么做?
1. 避免API的暴增。 直连线是平方增长,而总线是线性增长。
2. 有些时候,必须这样
对fopen(const char*)你可以继续直连线:
fopen(const std::string& );
fopen(const CString& );
对一个类:
class C {
open(const char* );
};
C不是你写的,你不可能为C加入一个直连线。
你只能绕弯:
open( C& o, const std::string& );
open( C& o, const CStirng& );
回到你说的ref<T>。
假设有一个函数:
f(int& );
你可以为它直连线。
假设有一个类,依然不是你写的,它只接受int &
class C
{
f(int & );
}
C2有一个属性i。
class C2
{
property int i;
}
你要怎么给你的用户解释:
C c;
C2 c2;
c.f( c2.i ); 是行不通的,必须
f( c, c2.i ); ???
你能将所有api的汇合点都直连线吗? 你做不到。
本来模拟属性就是一个很无聊的事情了。
你告诉用户,这就是一个C#中的 get; set; 就ok了,别想着要其他怎么用。
C#中可以 &o.p吗???
"运行时代价跟开发代价都是需要一并考虑的。"
你看你是否自相矛盾了。
将线性开发代价的总线形式,转化为平方开发代价的直连线形式。
# re: GUI框架:谈谈框架,写写代码 回复 更多评论
2009-11-16 16:07 by
@陈梓瀚(vczh)
"不过用了union,而跟你刚才那样一个属性就要定义一个类,而不是一种类型的属性定义一个类(或者干脆就只有少数几个属性的类然后到处使用——这个要求比较高了一点),我觉得比多一个指针更加不能接受。运行时代价跟开发代价都是需要一并考虑的。"
先说运行代价和开发代价。
C程序员可能会因为效率,不使用qsort。 他们都是傻子吗?
"实现属性占用的额外空间大小与属性的数量无关",这难道真的不值得你为其多花点功夫吗?
这是O(n)到O(1)的优化!是不值得的?!
而且我比较怀疑你所说的"一种类型的属性定义一个类"是一种[color=red]侵入式[/color]的实现方式。
需要实现属性的类本来就不应该很多。
我也想如何做得更范化一些, 但目前成效不大。
就像cexer说的"高效的,不会制造麻烦的东西,也是从不高效的,会制造麻烦的东西进化来的。"
如果一开始就采用指针保存,那是一辈子就没办法优化了。
# re: GUI框架:谈谈框架,写写代码 回复 更多评论
2009-11-16 16:08 by
@OwnWaterloo
举个简单的例子,总线形式的开发会让string有一个c_str(),而不是让string消失。我不知道你知不知道解决方案是什么样子的,但是举个例子出来总是要告诉别人说,“哦,这个东西可以这么这么做,只是我懒的写完整”而不是没说完。
模拟属性一点都不无聊,因为一个可以监听和撤销属性变化的系统非常容易扩展,总比你一个类都来一个listener或者就像api一样所有的消息的格式都很混乱的好。特别对于开发GUI的库。当然这就不仅仅是如何响应operator=的问题了。允许阻止变化的listener总是有开销的,所以你无法杜绝它。这也是我第一次举例子的时候就说出来的一个可以认为是“需求”的东西。
# re: GUI框架:谈谈框架,写写代码 回复 更多评论
2009-11-16 16:09 by
@OwnWaterloo
至于说“如果一开始就采用指针保存,那是一辈子就没办法优化了”,这是不对的。这跟接口无关,也就是你一个property的实现怎么变化,this指针放的地方换掉了,调用的代码都是不用改的。
# re: GUI框架:谈谈框架,写写代码 回复 更多评论
2009-11-16 16:14 by
@OwnWaterloo
“需要实现属性的类本来就不应该很多。”也不是很正确,当然这要看情况了。在我看来,每一个控件都要实现可监听的属性,这些属性用事件暴露出来。你愿意设计完属性之后从头设计一次事件的命名和参数什么的好,还是愿意把它们绑定到属性里面去?这样的话开发控件也不需要考虑提供的事件完整不完整的问题了。因为控件的状态都在属性里面,属性变更就是状态变更,也就都有事件了。这样做的话设计出来的控件就不会有winapi那个消息(我们都知道很难清洗化)的影子了,会有一个十分漂亮的设计出来。一般winapi封装出来的东西都是在PC上跑的,你会在意一个textbox占了1k还是1.1k内存吗?在这里多一个指针少一个指针根本不是问题。
特别是对于point,那就是个变量好了,point没有行为,所以不需要属性。
# re: GUI框架:谈谈框架,写写代码 回复 更多评论
2009-11-16 16:15 by
@陈梓瀚(vczh)
"在关心程序的质量的同时,也要同时关心一下如何将广大程序猿在C++的水深火热之中解放出来"
我展示的,只是"proxy得到object"的技术。
你可以继续展示proxy得到object之后的"解救广大C++程序员于水深火热之中"的其他技术。
但说实话,我不觉得那些技术真的能救人于水火。真正救人于水火的,不是玩弄高级技巧,而是设计清晰的接口。
还是上面的例子:
C c;
C2 c2;
c.f( c2.i ); 不能这样做,只会让人感到困扰。
如果i不是属性,而是成员:
c.f( c2.i() ); 不行是很容易理解事情 —— 临时对象不能绑定到非const引用上。
# re: GUI框架:谈谈框架,写写代码 回复 更多评论
2009-11-16 16:21 by
@OwnWaterloo
所以这就是Ref<>的意义了嘛,【在你的系统或由其扩展的系统里面】让你的参数的引用都能接受属性,同时也能接受旧的引用。
# re: GUI框架:谈谈框架,写写代码 回复 更多评论
2009-11-16 16:27 by
"举个简单的例子,总线形式的开发会让string有一个c_str(),而不是让string消失。我不知道你知不知道解决方案是什么样子的,但是举个例子出来总是要告诉别人说,“哦,这个东西可以这么这么做,只是我懒的写完整”而不是没说完。"
我相信看的人能明白我代码中的重点在那里。
我不明白的是你为何看不明白,即使在我说了之后。
我再说一下吧,如果要让我说"这个property"还可以这样玩、又可以那样玩,我会觉得很掉价……
一方面因为我现在本来就不喜欢过分玩弄语言特性。
另一方面,这些技术实在是说得太多……
如果一定要把所有细节都展现出来,不知道评论的字数会不会够。
而且,展现到什么程度才可以停止?
# re: GUI框架:谈谈框架,写写代码 回复 更多评论
2009-11-16 16:32 by
"模拟属性一点都不无聊,因为一个可以监听和撤销属性变化的系统非常容易扩展,总比你一个类都来一个listener或者就像api一样所有的消息的格式都很混乱的好。特别对于开发GUI的库。当然这就不仅仅是如何响应operator=的问题了。允许阻止变化的listener总是有开销的,所以你无法杜绝它。这也是我第一次举例子的时候就说出来的一个可以认为是“需求”的东西。"
模拟属性就是很无聊的工作。
你试着回答一个问题:”这里一定需要operator=吗? 可以使用命名函数吗?
如果回答是“是” ,那这里就宁可采用命名函数,而不是属性。
绝大部分回答都应该是“是”。所以这根本不算一个需求。
从我的直觉上来说,模拟属性使用的价值并不大。没有多少场合非属性不可。
如果你可以想到一个“不是”的例子,请列举出来。
也许我的直觉是错的。
# re: GUI框架:谈谈框架,写写代码 回复 更多评论
2009-11-16 16:32 by
"模拟属性一点都不无聊,因为一个可以监听和撤销属性变化的系统非常容易扩展,总比你一个类都来一个listener或者就像api一样所有的消息的格式都很混乱的好。特别对于开发GUI的库。当然这就不仅仅是如何响应operator=的问题了。允许阻止变化的listener总是有开销的,所以你无法杜绝它。这也是我第一次举例子的时候就说出来的一个可以认为是“需求”的东西。"
模拟属性就是很无聊的工作。
你试着回答一个问题:”这里一定需要operator=吗? 可以使用命名函数吗?
如果回答是“是” ,那这里就宁可采用命名函数,而不是属性。
绝大部分回答都应该是“是”。所以这根本不算一个需求。
从我的直觉上来说,模拟属性使用的价值并不大。没有多少场合非属性不可。
如果你可以想到一个“不是”的例子,请列举出来。
也许我的直觉是错的。
# re: GUI框架:谈谈框架,写写代码 回复 更多评论
2009-11-16 16:34 by
终于能看到一篇提起兴趣的文章了。
我也写过一个类似GUI框架,很lite,相当lite。主要也是因为引擎需要小型一个窗口系统。
风格上也是模拟了NET的方式,实现了一个Application::run(&win);这样的玩意。window的消息映射上没有去追求用标准CPP,直接用了VC特有的__event,__hook这些东西。
期待博主进一步讨论!
# re: GUI框架:谈谈框架,写写代码 回复 更多评论
2009-11-16 16:41 by
@OwnWaterloo
其实举过了嘛,textBox.Text.OnChanging+=a_functor_that_want_to_check_the_input;你觉得在一个【上百个】属性的Form(你不要告诉我一个窗口的状态很少)里面,你打算怎么处理属性和事件之间的关系,我一直都在强调这一点,而不是语法上的问题,operator=其实也是个例子,我没有强调说要不要有个名字之类的东西。
另外,不要觉得什么掉价不掉价的,技术没有贵贱之分,不要搞三六九等。话说我自己在搞语言设计和实现。从来跟这些property什么的关系不大,这估计是职业病。
# re: GUI框架:谈谈框架,写写代码 回复 更多评论
2009-11-16 16:42 by
"当然这要看情况了。在我看来,每一个控件都要实现可监听的属性,这些属性用事件暴露出来。你愿意设计完属性之后从头设计一次事件的命名和参数什么的好,还是愿意把它们绑定到属性里面去?这样的话开发控件也不需要考虑提供的事件完整不完整的问题了。因为控件的状态都在属性里面,属性变更就是状态变更,也就都有事件了。这样做的话设计出来的控件就不会有winapi那个消息(我们都知道很难清洗化)的影子了,会有一个十分漂亮的设计出来。"
消除winapi的影子,一定需要属性吗?命名函数不可以吗?
如果语言本身提供属性,那使用两者之一都没什么问题。
如果语言本身不提供,为何在可以使用命名函数的地方,非要去使用属性???
"一般winapi封装出来的东西都是在PC上跑的,你会在意一个textbox占了1k还是1.1k内存吗?在这里多一个指针少一个指针根本不是问题。 "
在gui上,你的说法是正确的。你甚至可以使用boost::any + boost::tuple来得到一个类似python的开发方式。
我讨论的是"proxy得到object", 请不要将讨论窄化gui上。
作为一个库(非gui库)的提供者,效率不是最主要的,但无端的损害(大量损害)就是不可接受的了。
假设某天你真的需要将proxy应用到critical的场景,你就会后悔当初没有去考虑这个问题了。
"特别是对于point,那就是个变量好了,point没有行为,所以不需要属性。"
需要的。我举个例子:
point
{
property double x { get;set; }
property double y { get;set; }
property double radius { get; set; }
property double seta { get; set; }
};
客户可以用直角坐标或者极坐标去操作这个点。
点的内部表示客户无须关心。
这样的点,就需要行为。
# re: GUI框架:谈谈框架,写写代码 回复 更多评论
2009-11-16 16:43 by
@陈梓瀚(vczh)
至于其他的例子,你可以参考WPF强大的布局功能是怎么【under control】的,用传统的方法根本没法让程序变得清晰。
# re: GUI框架:谈谈框架,写写代码 回复 更多评论
2009-11-16 16:45 by
@OwnWaterloo
【窄化】不是问题。GUI是一个庞大的东西,一旦需要了,那就有做的必要,就算除了GUI以外的所有系统都不能从这个property机制上获得好处,也不是不搞的理由。而且让一个例子举现在一个范围里面可以让我们都少想很多东西。
# re: GUI框架:谈谈框架,写写代码 回复 更多评论
2009-11-16 16:46 by
@OwnWaterloo
行为指的是受改变的时候发生的事情,而不是你能从一个数据里面计算出什么东西。坐标转换是映射,点在任何情况下都不可能因为你设置了一个什么坐标而做了一些什么事情,因此没有行为。当然cache不同坐标系下的结果例外,这不是重点。
# re: GUI框架:谈谈框架,写写代码 回复 更多评论
2009-11-16 16:49 by
@OwnWaterloo
其实举过了嘛,textBox.Text.OnChanging+=a_functor_that_want_to_check_the_input;你觉得在一个【上百个】属性的Form(你不要告诉我一个窗口的状态很少)里面,你打算怎么处理属性和事件之间的关系,我一直都在强调这一点,而不是语法上的问题,operator=其实也是个例子,我没有强调说要不要有个名字之类的东西。
另外,不要觉得什么掉价不掉价的,技术没有贵贱之分,不要搞三六九等。话说我自己在搞语言设计和实现。从来跟这些property什么的关系不大,这估计是职业病。
【刚才看你在我之后刷了,也不知道看到没有,所以再复制一次。如果你看过了就忽略】
# re: GUI框架:谈谈框架,写写代码 回复 更多评论
2009-11-16 16:56 by
【其实举过了嘛,textBox.Text.OnChanging+=a_functor_that_want_to_check_the_input;你觉得在一个【上百个】属性的Form(你不要告诉我一个窗口的状态很少)里面,你打算怎么处理属性和事件之间的关系,我一直都在强调这一点,而不是语法上的问题,operator=其实也是个例子,我没有强调说要不要有个名字之类的东西。 】
textBox.Text.OnChanging+=a_functor_that_want_to_check_the_input;
你觉得改成这样,可接受不?
textBox.Text.OnChanging.add( a_functor_that_want_to_check_the_input );
textBox.Text.OnChangingSet( a_functor_that_want_to_check_the_input );
textBox.Text.OnChangingAdd( a_functor_that_want_to_check_the_input );
C++就是C++,没有必要模拟.net那一套。
【另外,不要觉得什么掉价不掉价的,技术没有贵贱之分,不要搞三六九等。话说我自己在搞语言设计和实现。从来跟这些property什么的关系不大,这估计是职业病。】
因为我觉得通过union来达到property的额外空间与property数量无关这个技巧可能是新奇的。我没有在其他地方看到过,也是我自己想出来的。
而proxy的那些技巧,我已经看太多,说不定大家都知道,我又何必多次一举?
而且很多都不是我独立想出来的。
所以我觉得说出来很掉价……
# re: GUI框架:谈谈框架,写写代码 回复 更多评论
2009-11-16 17:01 by
【至于其他的例子,你可以参考WPF强大的布局功能是怎么【under control】的,用传统的方法根本没法让程序变得清晰。】
我不懂wpf。
不通过属性,而是命名方法,可以做到让程序清晰么?
如果只是从:
textBox.Text.OnChanging+=a_functor_that_want_to_check_the_input;
变化到:
textBox.Text.OnChanging.add( a_functor_that_want_to_check_the_input );
textBox.Text.OnChangingSet( a_functor_that_want_to_check_the_input );
textBox.Text.OnChangingAdd( a_functor_that_want_to_check_the_input );
我觉得依然很清晰。
也许前者对C#转到C++的程序员来说,前者会很亲切。
但后者才是大部分C++程序员的每天都在使用形式。
你不需要给你的用户培训“C++中如何模拟property”这种细节,他就可以使用你的api。
# re: GUI框架:谈谈框架,写写代码 回复 更多评论
2009-11-16 17:01 by
@OwnWaterloo
@陈梓瀚(vczh)
讨论了那么多,突然发现怎么两位同志的观点都扭转了?
“C++中实现属性”这个问题也是讨论过千百遍的了,而且注定是谁都无法说服谁的问题,因为它不纯粹是一个技术问题。很大程序上和性格喜欢相关。就好像实用主义点的程序员,肯定觉得这样费尽力气去模仿不值得,但艺术气质一点的程序员,觉得属性的语法看起来很漂亮。然后各自用自己的偏好去说服对方,肯定不能成功。
我个人的观点是,有总比没有好。存在即合理,要不然怎么会那么多的语言提供语言级的属性支持了。C++当中比这个急迫要解决的问题还很多,但可以预见,属性这东西在未来的某个C++标准当中一定会出现的,它确实有着不可磨灭的价值。
但你们的讨论里各自的观点都很有道理,里面包含的有技术含量的思考很多,我看过也很有收获。所以请你们在友好和谐的气氛当中继续尝试说服对方吧,无论是什么样的讨论都是思想的碰撞,也是学习的很好的方式。
# re: GUI框架:谈谈框架,写写代码 回复 更多评论
2009-11-16 17:03 by
@矩阵操作
谢谢,很高兴又认识志同道合的朋友。以后请多指教!
# re: GUI框架:谈谈框架,写写代码 回复 更多评论
2009-11-16 17:06 by
【【窄化】不是问题。GUI是一个庞大的东西,一旦需要了,那就有做的必要,就算除了GUI以外的所有系统都不能从这个property机制上获得好处,也不是不搞的理由。而且让一个例子举现在一个范围里面可以让我们都少想很多东西。】
就算这种O(n)到O(1)的优化因为它太复杂而永远用不上,【也不是不搞的理由】。
我这么说对吗?
讨论如何优化就是讨论如何优化。
没有必要将这种讨论限制到某个范围内,然后说在这个范围内不需要这种优化。
你不能预见所有场合都不需要这种优化。
# re: GUI框架:谈谈框架,写写代码 回复 更多评论
2009-11-16 17:07 by
@侠客西风
呵呵谢谢你能喜欢,其实我对技术的探求不是很深,只是迷恋于一些更简单更表面的东西。做技术的人其实这样不是很好。
你就不要准备了,直接把我加进去吧。但我不知道怎么加友情链接,在后台加过好像没成功。
很高兴认识你!
# re: GUI框架:谈谈框架,写写代码 回复 更多评论
2009-11-16 17:10 by
@OwnWaterloo
我并没有反对你优化,我只是说“你觉得在一个【上百个】属性的Form里面,你打算怎么处理属性和事件之间的关系”。正好本篇是谈GUI,因此举这么个例子我觉得挺合适的。那我就这么问吧,我以为你是知道我在说什么的:在一个包含请求回调doing和响应回调one(+=也好,.Add也好)的属性,怎么避免开销?
# re: GUI框架:谈谈框架,写写代码 回复 更多评论
2009-11-16 17:11 by
【行为指的是受改变的时候发生的事情,而不是你能从一个数据里面计算出什么东西。坐标转换是映射,点在任何情况下都不可能因为你设置了一个什么坐标而做了一些什么事情,因此没有行为。当然cache不同坐标系下的结果例外,这不是重点。】
可能我们对“行为”这个词有不同的理解。 那我们不用这个词了。
对刚才说的点的例子。半径绝对不能直接读写对吧?
所以必须将实现隐藏起来(实现可能是以直角坐标存储), 通过setter/getter去操作,以维护点的不变式 —— 半径不可能小于0。
我说的就是这个意思。
如果你需要可按极坐标操作的点,就不能直接暴露数据,必须使用setter/getter或者property。
# re: GUI框架:谈谈框架,写写代码 回复 更多评论
2009-11-16 17:13 by
@陈梓瀚(vczh)
话说回来,其实我觉得你后面的重点已经变成了operator=和operator+=与set_和.Add的关系了,这个我不想讨论,因为我也觉得无所谓,用哪个都好,只要
1:先做的事情写在前面,后做的事情写在后面,要花时间看清楚。
2:不要造成括号的嵌套,难以维护。
举个例子,挑选列表里面所有偶数的数字然后平方,如果你写成square(even(a_list))就不满足1和2。
【注意,上面两点你看过就算了,我不讨论好跟不好,因为跟主题没关系】
# re: GUI框架:谈谈框架,写写代码 回复 更多评论
2009-11-16 17:15 by
@OwnWaterloo
对于你的点的例子,我倾向于CalculateRadius(your_point)。你不能否认人们总是下意识地认为获取属性比调用函数的效率要高,所以那些真的做了事情,又不改变状态,又只读的属性变成函数比较好。不过我还是想跟你讨论“在一个包含请求回调doing和响应回调one(+=也好,.Add也好)的属性,怎么避免开销”而不是在这里要不要用只读的属性。
# re: GUI框架:谈谈框架,写写代码 回复 更多评论
2009-11-16 17:17 by
@cexer
我跟OwnWaterloo就借着你的地盘版聊了哈
# re: GUI框架:谈谈框架,写写代码 回复 更多评论
2009-11-16 17:19 by
【我并没有反对你优化,我只是说“你觉得在一个【上百个】属性的Form里面,你打算怎么处理属性和事件之间的关系”。正好本篇是谈GUI,因此举这么个例子我觉得挺合适的。那我就这么问吧,我以为你是知道我在说什么的:在一个包含请求回调doing和响应回调one(+=也好,.Add也好)的属性,怎么避免开销?】
首先,上百个属性是否可以改为上百个×2的getter和setter?
setter不一定难用,setter还可以链式表达呢。
对比:
1. w.p1(v1).p2(v2).p3(v3) ...
2. ( (w.p1 = v1).p2 = v2 ).p3 = v3 ...
3.
w.p1 = v1;
w.p2 = v2;
w.p3 = v3;
其次,如果真要模拟property。
我真的没看明白你说的那个需求是什么…… 囧
# re: GUI框架:谈谈框架,写写代码 回复 更多评论
2009-11-16 17:23 by
【我跟OwnWaterloo就借着你的地盘版聊了哈】
要不我们开一个论坛吧?google groups或者群?
群的聊天资料可能不容易搜出来……
toplangauge越来越水了……
把cppblog上的人都招集起来?
很高兴和你们讨论问题~_~
如果我语气有过激之处,请不吝指出,或者多多包涵一下。
有人响应没……?
# re: GUI框架:谈谈框架,写写代码 回复 更多评论
2009-11-16 17:26 by
@OwnWaterloo
我觉得这里挺好的,其他地方都会水- -b就是cppblog还没那么多乱七八糟的帖子。
# re: GUI框架:谈谈框架,写写代码 回复 更多评论
2009-11-16 17:28 by
@OwnWaterloo
属性变化就是状态变化嘛,你要监听一个状态的变化,还要控制一个状态变化的权限,用属性+事件的组合是很常见的。也就是说,一个属性都要自动提供Changing和Changed两个事件。尝试把它做得高效,就是我之前的需求了。
这才是属性的本质概念,跟语法什么的关系并不大。
# re: GUI框架:谈谈框架,写写代码 回复 更多评论
2009-11-16 17:35 by
@陈梓瀚(vczh)
你说“我跟OwnWaterloo就借着你的地盘版聊了哈”
绝没问题,欢迎有自己思考的同志来此版聊,如果能切紧GUI框架的主题就感谢了哈,你们的讨论能使我的博文增色不少。聊完不要走,此处管饭。
# re: GUI框架:谈谈框架,写写代码 回复 更多评论
2009-11-16 17:43 by
哈,关注~
# re: GUI框架:谈谈框架,写写代码 回复 更多评论
2009-11-16 17:43 by
【
1:先做的事情写在前面,后做的事情写在后面,要花时间看清楚。
2:不要造成括号的嵌套,难以维护。
举个例子,挑选列表里面所有偶数的数字然后平方,如果你写成square(even(a_list))就不满足1和2。
【注意,上面两点你看过就算了,我不讨论好跟不好,因为跟主题没关系】
】
不能算了……
text.onchange_set( handler1 ).onchange_add( handler2 ).onchange_add( handler3 );
满足上面的2点吗?
【
对于你的点的例子,我倾向于CalculateRadius(your_point)。你不能否认人们总是下意识地认为获取属性比调用函数的效率要高,所以那些真的做了事情,又不改变状态,又只读的属性变成函数比较好。
】
点的例子是因为你说point不需要行为而举出的反例。
我也倾向于:
point
{
double x() const;
void x(double xx);
double r() const;
void r(double rr);
// ..
};
我也没有假设get_r( p ); 和 p.r() 或者p.r到底何种效率更高。
这取决与内部表达是直角坐标还是极坐标。
这些都不是重点。重点在于:
1. r绝对不可以是裸域 —— 它需要维护不变式
2. 如果你打算将x、y、r、a由函数变为属性,4个指针的开销你觉得是否合适?
btw:这4个属性都可以是读写的,并不存在x、y读写,r、a只读一说。
【
不过我还是想跟你讨论“在一个包含请求回调doing和响应回调one(+=也好,.Add也好)的属性,怎么避免开销”而不是在这里要不要用只读的属性。
】
我没看明白,能否再详细一点?
# re: GUI框架:谈谈框架,写写代码 回复 更多评论
2009-11-16 17:46 by
@OwnWaterloo
他少打一个字母!done
# re: GUI框架:谈谈框架,写写代码 回复 更多评论
2009-11-16 17:57 by
@cexer
【
绝没问题,欢迎有自己思考的同志来此版聊,如果能切紧GUI框架的主题就感谢了哈,你们的讨论能使我的博文增色不少。聊完不要走,此处管饭。
】
别管饭了…… 正好有一个悬了很久的关于ATL/WTL的疑问,你管管吧~_~
是这样的,ATL/WTL中提到一种atl-style继承。
template<typename D>
class B
{
void f1() { static_cast<D*>(this)->f1_do(); }
void f2() { static_cast<D*>(this)->f2_do(); }
void f1_do() { printf("B\n"); }
void f2_do() { printf("B\n"); }
};
class D : public B<D>
{
void f1_do() { printf("D\n"); }
};
D d;
d.f1();
D::f1不存在 -> 调用B::f1 -> 转型为D* -> D覆盖了f1_do -> 调用D::f1_do ->输出"D"
d.f2();
D::f2不存在 -> 调用B::f1 -> 转型为D* -> D::f2_do不存在 -> 调用B::f1_do->输出"B"
是这样吗?
然后,问题就来了 …… 这个样子,除了将代码写得更复杂以外,相比下面的写法,有什么区别么??
class B
{
void f1() { printf("B\n"); }
void f2() { printf("B\n"); }
};
class D : public B
{
void f1() { printf("D\n"); }
};
D d;
d.f1(); "D"
d.f2(); "B"
# re: GUI框架:谈谈框架,写写代码 回复 更多评论
2009-11-16 18:02 by
@cexer
ATL-style继承,提供的语意是:
基类提供默认实现,派生类继并覆盖自己需要的函数。
是这样吗?
除了这个,还有其他功能吗?
但这个功能,不正好就是非ATL-style继承 —— 普通继承提供的语意吗……
为什么要绕这么大一个圈子?
能得到什么好处? header-only?
# re: GUI框架:谈谈框架,写写代码 回复 更多评论
2009-11-16 18:06 by
@陈梓瀚(vczh)
【
属性变化就是状态变化嘛,你要监听一个状态的变化,还要控制一个状态变化的权限,用属性+事件的组合是很常见的。也就是说,一个属性都要自动提供Changing和Changed两个事件。尝试把它做得高效,就是我之前的需求了。
这才是属性的本质概念,跟语法什么的关系并不大。
】
我觉得你又开始继续扯远了……
本来不是在讨论“proxy的实现技术”“property和命名函数”么……
anyway,换个话题也好。
不过我真没看明白你的需求是怎样的……
这个需求是否高效,与“是否使用union”有关系吗?
与“property vs 命名函数”有关系吗?
是一个新话题吧?
能详细说说你的需求么?
# re: GUI框架:谈谈框架,写写代码 回复 更多评论
2009-11-16 18:08 by
@OwnWaterloo
我的理解是所谓的ATL-Style,其实是用模板在编译期手工模拟的一种多态,ATL 的实现当中大量地使用了这种方式,目的就是为了轻量级,几乎没用过虚函数。
我觉得这种手法的作用不仅仅限于此,因为可以结合其它的编译期技术,实现很多虚函数难以达到的功能,我实现 GUI 框架的时候也用到很多这种东西,以后的说明中应该会遇到。