| ||||
| 现在感觉思绪有些混乱。 那个花括号的问题,我主要的目的是想说明: 虽然自动化分析能够分析出一些东西,但是有些信息,如果程序员没有表达出来,那么自动分析是不可能分析出这些信息的。 代码:
现在的事实是,这i没有传到外面,但是原始设计中,这i是否允许传到外面去呢?如果设计不允许,那么最多就只能用拷贝的方法;而如果允许,那么就可以传递引用。但是这段代码里没有描述出这些信息。如果用栈来管理,变成: 代码:
我又想起一个例子,比如说我经常会使用这样的一段控制代码: 代码:
代码:
这段代码因为我用成了习惯,所以不容易把running的作用理解错,可是如果别人不习惯,那么第二种方式既可以描述更强的约束(提供了更多的信息),又可以依靠编译器来实现这种约束。 总之,我希望能尽量把设计信息融入代码,使得代码不仅现在能运作良好,而且能为以后的维护和复用提供更多的信息。 |
| |||
| 引用:
引用:
就说内存,现代的gc也很复杂,对象分代,大对象自己有存储区域,并不是一个“优雅而通用”,写出来才几行的算法。 引用:
此帖于 2008-03-06 08:16 PM 被 cat 编辑. |
| ||||
| 查了一下,似乎using/Dispose和栈的区别不大——除了语法上没有栈简洁之外…… 而栈的确是我心目中最最优先使用的资源管理技术。 但是,当对象分配在栈上时,构造函数和析构函数仍然是有效的,实际上这是很重要的特点: 用花括号对/栈来说明一个生存期 + 栈上对象的构造函数和析构函数被自动绑定在所在的生存期边界上 1.资源管理是很重要的 2.资源管理是很麻烦的 3.gc机制只能用于管理内存,并且使用它也是有成本的(特别是学习它和理解它的成本) 4.栈、内存池、智能指针都可以把非内存资源绑定上去进行管理 1+2+3+4 ==> 所以我把gc放到候选列表的末尾。 资源的确太广泛了,但正因为如此,我们才需要一个通用的技术。否则内存一个管理技术,文件又是另一个技术……如果没有一个通用技术作为基础,那么最后各种不同的管理机制之间的配合和交互就会成为大问题。 我所期望的资源管理是: 一个良好的内存管理机制+一套把任意行为绑定到内存管理机制上的机制+N套管理各种不同资源的技术 中间那个绑定机制,其实就是C++的构造函数/析构函数。后面那些管理技术是真对于各种资源的,但是它们都应该能够绑定到内存管理上,用内存的生命期来指代各种资源的生命期,这样生命期管理就只需要在内存管理那里实现一次。 现在主要的问题在于最前面那个内存管理机制。栈这样的机制,虽然很好,但是表达力有限;内存池的表达力稍微提高了一些,但是其实主要是比栈多了根据runtime需要而分配的内存的管理能力,生命期模型与栈似乎没多少区别。智能指针存在循环引用问题以及性能问题。所以这些机制大多在表达力上有限,但是虽然它们的表达力有限,但是在其擅长的应用领域里还是用得很好的,所以没有理由丢弃它们。 gc的表达力是最强的,但是如果不考虑性能问题,那么还有一个问题,那就是其不确定性使得把其他资源的管理行为绑定上去后的效果并不好,具体就是在gc环境里使用RAII时的问题。 另外,通用优雅并不是指这个技术需要几行代码,而是说它对外的表现和行为的应用范围和可理解性。 其实,一些os中,很多资源都是以文件描述符的形式出现的,通信就是read/write。这是“通用”的例子。至于即通用又优雅的东西,我一时也想不起来。不过这种追求应该是能取得共识的。 不是我觉得别人看不懂,而是我希望减少别人的负担。如果我把这些控制变量写在实际有意义的范围之外,那么别人就需要花费更多的时间精力来猜测一开始的意图,其实一段时间后可能我自己都不得不进行这样的猜测。如果控制变量多了,代码长了,那么在庞大的代码中到处翻箱倒柜地查看就是很麻烦的事情。一开始的C里,函数的变量必须在函数的前头声明,后来到了C++,连for语句里都可以声明变量了。当实现了一个约束时,一方面是说明这个约束一定会成立,是可以依赖的,另一方面也是说明相应的互斥情况一定不会出现,是可靠的。所以C++里的const是为了提高对设计的表达能力,变量声明的控制以及花括号的作用也是如此。把设计信息以一种良好的方式融入代码,会比写进文档更好。 |
| |||
| 引用:
引用:
代码:
|
| |||
| 引用:
其实,用了gc,内存管理基本上已经不是一个主要问题了。你就可以把精力放在管理其它资源上,而让人惊讶的是,相比于繁琐脆弱的内存管理,其它资源的管理工作相当清闲(随便一个狗屁模式都要涉及内存处理,而谁没事总去鼓捣文件,数据库啥的?)。在using或者try-finally(或者我推荐的auto)的帮助下,也非常容易。因为内存这个最关键的资源得到了保障,其它的资源你弄错了最多得到一个ClosedResourceException,而不是神秘不可琢磨的core dump,甚至undefined behavior(比如我明天忽然惊喜地发现银行的一个bug诡异地让我得到100万 )。此帖于 2008-03-07 01:10 AM 被 ajoo 编辑. |
| ||||
| 引用:
|
| ||||
| 花括号的第一个作用是表达作用,即使会依赖于程序员之间的约定,但是这仍然能够用来表达一些信息,这是为了以后的理解和维护而着想的。以后的需求会变,但是在改动代码之前,至少应该知道这段代码以前的设计情况。 你那段代码的确可以把running泄漏到外面去,但是如果是一开始写代码的人这么做的,那么只能说这是有意而为之,这是初始设计者所设计的;如果是维护者的修改,那么至少代码本身能够表达这样的信息:维护者把一个在最初设计中的局部变量泄露到了最初设计的作用域之外。而如果代码没有那么写,那么维护者根据什么来了解最初的设计?如果没有了解到最初的设计,那么如何判断修改的风险? 这就像reinterpret_cast,虽然编译器不能阻止程序员做危险的转换,但是能够通过“丑陋”的语法来提示程序员其中的风险。把running放进一个更精确的作用域里声明,虽然无法真正阻止一些事情(如果程序员有意乱搞,神仙都没办法),但是作为初始设计者,至少把初始的设计信息都放进了代码。很多机制都是防君子不防小人的。 另外,好像我不记得这个话题跟gc有啥关系,当时只是针对“对代码进行自动化静态分析”这个话题说的,我要表达的就是很多信息(如果程序员不表达出来的话)是无法分析出来的。 内存,未必可以延后释放。延后释放的问题我之前的帖子里谈了。 如果要让gc通过猜测来把数据放到栈上,那相当于程序员写程序时先把这些信息丢弃,然后再让gc来猜测。还不如像C/C++这样直接让程序员来放置。 rvo貌似现在的C++也有,但是却是有副作用的。 重复使用已分配的内存,这是典型的内存池。 所以这些似乎都不能构成支持gc机制的理由。 而Elminster把对象生命期与内存生命期分离开的想法正好与我相反。如果把这两者分开仅仅是产生一个手动版本的析构函数,那么就失去了析构函数最大的优点:在生命期结束时自动调用。 我之所以要把其他资源绑定到内存上,是因为无论是内存还是对象,都需要有生命期管理,而这其实恰恰是现在管理的最大麻烦之一。只要做好了生命期管理,那么把内存交换给系统/内存池这种事情是不会构成多大的麻烦的,而其他资源的释放一般也只是close()之类的操作。 主要的问题是,应该在何时交换内存、close各种资源。只要有一个机制能够确保:在生命期结束时就及时调用指定的行为,那么内存和各种资源的管理都不会太麻烦。析构函数仅仅是在生命期结束时被调用的那个行为。 生命期的管理,我认为是一个普遍的问题,也应该存在通用的解决方案。所以实现了一个良好的生命期管理机制之后,那么所有的资源管理问题都能够从中获得好处。 栈可以看成一种内存管理机制,但是也可以看成一种生命期模型:它是一种嵌套的、FILO的生命期模型。从这个角度看,对于栈来说,内存管理和对象管理是一样的。 至于那些优化,如果会损及语义,那么很可能是不合算的。 |
| ||||
| to sjinny: 你说的让gc猜测并不成立。即使在支持GC的情况下,你也可以让某个对象在出了scope以后立即析构的。过去的语言不支持这样做,只是因为它们不支持,不是GC的错。C++/CLI应该就支持这一点。 关于你说的,“至于那些优化,如果会损及语义,那么很可能是不合算的。”这要看你怎么定义语义了。 代码:
|
| ||||
| 如果我要“让某个对象在出了scope以后立即析构”,那么我会用栈。 我并不是说gc能不能猜出是否有错,我是说这种情况下就应该用栈而不是丢给gc。 我说的“至于那些优化,如果会损及语义,那么很可能是不合算的。”是指ajoo所说的“允许对象进行超光速空间跳跃”。他还给了前提“优化开关打开的情况下”,而如果这种优化会损及语义,那么即使在文档中有所记录也仍然会增加风险。我对编译器优化并不很了解,所以我加了句“如果会损及语义”。 代码:
代码:
代码:
如果要说优化,编译器最好是根据程序员的要求来说,或者只做一些保守的优化。 |
| |||
| 引用: 而Elminster把对象生命期与内存生命期分离开的想法正好与我相反。 唉,又一个被值语义语言“毒害”的人阿。对象的生命周期和内存为什么非得紧密帮定呢?帮定的好处是什么?不知道C+09都要采用move ctor了吗,因为对象和内存帮定对谁都没有好处,除非进行非常危险的指针算术运算。而那东西在最低层的编程才有点用处,所以我们说对象跟内存帮定是弊大于利,对象跟内存绑定,对象的生命周期也就跟内存帮定了,这又是一个看起来很美用起来很傻的特性,其实对象的生命周期可以从逻辑上讲就是构造开始,析构结束。完全可以跟内存的分配和释放无关的。我想所有的内存池都是剥离这个紧密关联的努力的产物。 其实值语义本来未必导致对象跟内存非得是显式的关系,但是值语义的语言确实倾向于促使程序员关注内存这个本来不该关注的东西。 |
| |||
| 普遍的问题就有通用的解决方法?未必吧?这是你逻辑中的一个大错,反例太多了,特别是那个“普遍的问题”非常general的时候。 不过就内存管理来说,目前倒是有一个比较通用的解决方案就是GC. 引用:
此帖于 2008-03-07 08:00 PM 被 cat 编辑. |
| ||||
| fixopen,增加的move语义只不过是一种新的优化手段,并不是要替代原有的一切。 之所以要绑定,是为了在管理非内存资源时也能够用上内存管理中现成的生命期管理。把非内存资源绑定到内上只是个手段,目的是为了借用内存管理中的生命期管理功能。 cat,有限数量的反例并不能证明命题在任何时候都不成立。 gc是针对内存的通用的管理机制,但是如果要把其他资源绑定上去,目前的gc还不够好。 我说过了,using貌似提供的就是栈的功能,只不过语法上还不如栈简洁。 而且无论是using还是try-finally,都要求程序员显示给出生命期边界,能用这些东西的地方就不能用栈吗?而对于不能用栈的时候,这些能用吗?对于能用栈的情况,我觉得栈已经足够好了。现在主要的问题是在栈的表达能力以外的领域。所以才有内存池,才有智能指针,才有gc……而它们都不如栈那么简洁优雅。 内存的延后释放我在前面的帖子里说过的,你感兴趣的话可以翻到前面几页看看。 |
| |||||
| 引用:
代码:
大家不理会你那个“谈话”就说明大家不认可。你的结论和论据之间跳跃过大,要不迁就一下我们,放慢思考的速度? 引用:
引用:
引用:
引用:
1。优化是重要的。一个损及优化能力的语言语义模型是失败的,尤其对c++这种号称关注系统级编程的语言。 2。语义的一致和完整性是重要的。为了优化而不惜破坏语义完整性(如c++的rvo)是可怕的。 综上,既然优化没错,语义完整性也没错,那就是c++错了,是妄图把内存管理和副作用混为一谈错了。 |
| ||||
| 我前面那个例子就是: 代码:
编译器不用猜测人的意图,但是人自己需要知道……编译器不需要知道在初始设计之中那个running的含义和使用范围,更不需要知道以后修改时需要维持哪些约束、哪些约束是可以放弃的。但是人需要知道,所以在代码里表达出来会更好。 我不认为构造和析构函数把内存管理和其它管理混为一谈。构造函数和析构函数仅仅是提供了一种手段,这种手段让你能够把你所期望的行为绑定到某个内存块的生命期边界上:生命期开始时调用构造函数,结束时调用析构函数。构造函数和析构函数并不是专门用来管理资源的,它的使用效果取决于你的选择。就好象多线程中给mutex加锁的操作。一个Guard对象并不是管理mutex的,而仅仅是为了提高加锁/解锁的自动化程度以减少出错的机会。构造函数和析构函数仅仅是把行为绑定到内存块生命期边界上的手段。 至于你所说的在传递T时会有无法预知的行为的问题,这不是构造函数或析构函数的问题,这是抽象引起的问题。当然其中还有值语义的因素,但是这本身也可以看作一种抽象。如果很多种不同的对象被使用时必须要有各自的初始化行为,那么当这行为被放进构造函数时,它们可以用相同的形式来传递;如果没有放进构造函数,那么这些无法省略的初始化行为就不得不在函数内显式调用,那么问题就在于: 1.不同类的初始化接口可能不相同; 2.函数编写者可能会忘记调用初始化接口。 3.当进行GP时,你如何知道这个T是否应该调用初始化接口呢? 构造函数提高了GP的抽象能力。 我不记得模板能不能限制或识别模板参数是否为指针了,似乎用TypeTraits可以做到。但是,即使无法做这种区分或限制,那么: 传入的T是什么,取决于这个模板的使用者。如果使用者需要使用引用语义的传递,那么可以选择把指针类型传给模板,或者做个wrapper。 就好象一个电磨,可以绞肉,也可以打果汁,电磨只知道把放进去的东西搅碎,至于其语义,那是使用者的事情。如果一个模板需要对外界有很多的了解,那么意味着在模板面前的世界的抽象度相对比较低,那还不如用普通的函数和类。 值语义有时会有缺陷,但是它有自己存在的意义和必要性。新标准引入的move语义也并不是值语义的替代。我个人的体会是,值语义比引用语义更容易使用,因为它把一次模块交互的两端(即传递值的源模块和目的模块)分开了,交互的两个模块间没有物理性的关联,传入的值再怎么变动也不会影响到源模块。如果传递的是指针,那么模块间的耦合度就变高了,毕竟目的模块无法得知源模块是否会继续使用这个对象,那么要么就让模块间的了解变多(增加耦合),要么就是增加风险(目的模块对传入的对象的误用可能会引起源模块的错误,甚至正常使用也不是100%安全的)。所以我现在都是尽量使用值语义,即使我知道会有更多的复制,但是在我profile之前我没有任何根据来优化,而profile之后也可以用内存池或其他的手段来优化。 不同的东西的生命期管理有不同的特点。 这点我自然会承认,但是这点并不能证明“不同的东西的生命期管理没有共性”吧?而我要把其他资源的管理绑定到内存上,就是为了使用内存管理中现成的生命期管理功能,更具体的说,借用的无非就是生命期边界的管理功能。这个边界就是生命期的开始和结束。我假设任何资源的生命期都只会有一次开始和一次结束。如果不符合这个假设,那么我觉得完全可以把那种生命期分解为这种结构。比如,如果要复用对象,那么只需要把对象的一次使用看作一个完整的生命期;比如mutex加锁,mutex存在的过程中会有多次加锁/解锁,但是却可以分解为多次Guard的构造和析构。这种“一次生命期中只有一次开始和一次结束”的模型虽然不能表达所有的情况,但是是一种有效的工具。而这种模型就是我所认为的各种资源的生命期管理的共同点。 至于内存管理和FILO的问题。我那段话说明的只是栈的特点,如果某些栈在编译器优化之前不遵循FILO的约定,那么请告诉我…… 其实我那段话只是把栈拿来作个例子,用以说明从生命期的角度来看到的栈。重点是把栈作为生命期模型的实现,至于FILO则是次要的。 关于优化,我认为作为语言和工具,要提供的只是优化的手段和保守的自动化优化。优化的根本还是在于profile和程序员的分析。rvo貌似是不得已而为之的,move语义的出现应该会成为rvo的替代,或者说能够使rvo时函数的语义与语法一致。 而你说的这句:“一个损及优化能力的语言语义模型是失败的”,我的理解是你在说C++的语义模型是损及优化能力的,那么能不能具体的说说C++的什么语义损及了优化能力呢?又有什么语言能够在对应的方面有着不损及优化能力的语义呢?毕竟我没有在这句话中看到“如果”二字。 如果因为一个rvo就能把整个语言否定掉,那我也无话可说了。反正世界上不会有完美的语言,那么任何语言都能因为局部的问题而否定掉全局。 最后那句“把内存管理和副作用混为一谈”中的“副作用”是指什么?我不明白如果一个效果被人认为是副作用,那么这人还会有意地去把内存管理和这种副作用混合起来吗? PS.感觉我经常照着别人的帖子一段一段的回复,而我得到的经常只是无穷的反问,甚至发问者似乎没有仔细看我的文字。这样很累。 |