Skip to content

Instantly share code, notes, and snippets.

@FrankHB
Last active November 19, 2016 15:32
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save FrankHB/056ac033bb68dc2caa0cdbcf498a8d42 to your computer and use it in GitHub Desktop.
Save FrankHB/056ac033bb68dc2caa0cdbcf498a8d42 to your computer and use it in GitHub Desktop.
161109

http://tieba.baidu.com/p/4860024843

56L @福音战士01 :

这是典型地没学明白。

首先是没搞清楚语言的目的和应用领域,其次是不明白现在对语言的应用的一些常见外延。

一条条分析。

每一个指针操作会对内存产生多大的影响;

指针操作不依赖于“内存”。“内存”的抽象和指针也没有直接的关系。会产生影响是实现细节。

提醒:对 C++ (以及 C ),抽象机语义应该算是基本常识,即便市面上的粗浅读物不太会讲到(我倾向于认为是那些读物普遍劣质或流于科普)。

然后,有没有想过 C++ 为什么刻意这样设计?

每一行代码编译后大体会生成什么样的汇编指令;

典型没学清楚。

然而谁教你 C++ 代码就能找到生成的汇编指令了?“每一行”,嗯, #define ?

编译器是如何识别动态类型;

“动态类型”(dynamic type) 是 C++ 中有明确定义的术语,不过看起来不是你说的这个。

因为实际实现中“编译器”不会去识别“动态类型”。所以你指的是什么?

哦,顺便, C++ 没规定实现需要有编译器。

结合上面的问题,对比来讲,你要说 Java 一定会被编译而会生成 bytecode 那还凑合(虽然现实也不一定),因为那算是语言规范直接钦点的。

这几条是理解C++的基本保障。

以我来看,这几条基本保障了你不理解 C++ 。

不掉进与平台相关的CRT坑,比如Win32 API与CRT函数混用引起的资源泄漏;

这仍然是实现细节。另外,严格来讲, CRT 这玩意儿不被要求存在。 顺便, C++ 和 C 真正兼容的部分其实有不少细碎的接口层次上的修改,比这个坑多了。

不掉进STL的坑,比如嵌套使用map引起的内存碎片

STL 的意思嘛……算了,然而这个问题和 map 并没有多少关系,坑主要是因为你不知道或没注意到 std::allocator 的局限性。

至所说的template,说实话这东西很不受待见,更不用说模板元编程,炫技的东西自己玩玩还好,团队项目很少使用,并且项目越大,使用的越少。因为明白的另一层含义是其他人,

不受待见是指啥,容器什么的全部自己撸轮子,顺便不用 template ?用宏元编程代替? 我很奇怪现在有多少这点基础都有障碍的还好意思厚脸皮进 C++ 项目?

即便是水平不如你的人也能很好的看明白并维护你的代码。

“水平”?这是什么奇葩需求,要求对背景都不理解的阿猫阿狗都能维护你的代码?

好好反省,现实中看代码有多少机会比看人更重要?

注意,工程上一个重要的普遍要求就是低成本地快速选型——分辨出什么样的开发者不适合什么样的项目,什么样的项目不适合什么样的语言,什么样的代码是一眼就不合格以至于能及早砍掉避免规约失败挽回损失,等等。

在负责人“水平”充分的情况下,难者不会和人人都会的东西正好站在对立面。而用具有严格规范的语言拿来筛人比库和文档拿过来简单省事多了。 这个现实差距被你反过来扔了?

(其它语言也是一样,尽量少使用语言本身特性算是团队编程的一个基本准则)

提供抽象就是高级语言本身提供的最重要的一类特性,照你这样的说法,是不是应该都去撸 shellcode ?

话说这居然不是坟……?好像很有即视感。。。

我说的确实有些过了,但如果能保证上面的几点,那么别人的项目代码拿过来就能看懂并且简单维护不会有太大问题。

不是过了的问题,是漏洞百出。

如果能补充细节,可能还可以从表面上救一救。不过,隐藏在这些说法下的认知我就不清楚你是从何而来的了。

惯例列提纲。

关于 C++ 辣鸡在哪我是不用提了,一坨玩意儿我放隔壁和 github 上好几年了,自己找得到。

不过还是得补充一下几点:

1.设计者缺乏全局观念。

这倒不是评价 James Gosling 这样区区砍特性偏执狂(在这个话题上他不还够格被评价,JLS 的相关部分看上去也不是他主导的),而是指缺乏更深刻的系统性的观点。

不知道那些 C艹 厨有没有发现,辣鸡之处罄竹难书(不了解这点的我钦定你不怎么懂 C++ ,有异议也不用挣扎了),连言简意赅地概括都有困难?

很大原因就是问题太深刻(从一开始就错得离谱)了,表面缺陷一大堆难以概括(尽管只是要解决这类问题,补救起来不困难,以至于总是被寻常用户妥协)。

上面说了,这里的设计缺乏一种全局观念。提示,这里说的“全局”其实和某一类语言关系不大,而且多数语言设计者恐怕都未必有自觉(因为没有外力推动,很多人根本不会去好好写清楚 spec )。

举例:之前有人(@8pm )提过的移位和模板操作符冲突问题,其实如果不求精确,概括起来也不难,就是“文法辣鸡”。(具体到这个案例,就要分析 less than 为什么用来代替 chevron 以及字符集规范之类的“人文”的破事了……)

注意,重点来了:文法(grammar) 不是语法(syntax) 。(啥叫“语法”我也科普了好几年了……略。)

C++ 在这里有这种破事的根本原因之一就是它的设计实质上缺乏明确的“语法”概念,从现在来看设计者和绝大多数用户也没太关心。具体一点的,可以自己理解 phase of translation (也应该是基本知识点了)以及我对 ISO C++ 的某个 Annex 的修改。

其它反直觉的文法歧义(懂 C++ 的应该都被坑过,至少能随便列举出 1 个)基本上和这个都有一腿。

另外一个主要原因是一开始想从源码上兼容 C 的要求。这个也算是全局问题,不过从历史的角度上来看倒是可以见仁见智。

2.关于这类通用语言的特性应该怎么样的问题,还是得回顾一下 RnRS 的经典前言。

“Programming languages should be designed not by piling feature on top of feature, but by removing the weaknesses and restrictions that make additional features appear necessary.”

每个标榜通用的语言的设计者都应该了解这类规范需求的常识。可惜大部分涉众在这点上完全不称职,甚至根本不懂一个语言标准库存在的意义。

题外话, C++ 原本的核心设计哲学从适用范围来看比这个更加高大上(其弱化版本 zero overhead abstraction 也被 Rust 等照抄),只是实现起来更困难,显然现在的 C++ 也没做好。

3.为什么仍然需要不时钦点 Java 辣鸡?

其实不止是 Java ,不过 Java 一直以来都最典型,从未被超越。

这部分的主要问题还不在于语言的设计,并且更加罄竹难书,只能略点几句。不过对于缺乏独立思考者我还是要关心报道的偏差值的。

从产业角度上来讲,所有之前提的辣鸡不辣鸡的问题,都可以归结为开发者“圈内”人的问题。

然而 Java 之流造成的影响早就不限于产业内部。——而是逼着所有人最后一起 zz 的劣币淘汰良币的更加全局的问题。

典型例子就是关于当设备因为应用使用内存过多而炸了(虽然实际上仍然不一定是 Java 直接造成的),应该采取什么态度?

try { 加内存,买买买? } catch(xxx) finally { 最终用户必须懵逼认怂? }

图样。实际上, Java 吹硬点“内存不用浪费”还是受到相当一部分最终用户的抵制的。不过随着硬件发展和某些行业傻多速等大环境因素,这些用户越来越有被边缘化的趋势。

那么,凭什么浪费资源是开发者而不是最终用户的权利呢?某些开发者因为自己见识短甩锅的这种吃相就实在有些难看了。


惯例列提纲。

关于 C++ 辣鸡在哪我是不用提了,一坨玩意儿我放隔壁和 github 上好几年了,自己找得到。

不过还是得补充一下几点:

1.设计者缺乏全局观念。

这倒不是评价 James Gosling 这样区区砍特性偏执狂(在这个话题上他不还够格被评价,JLS 的相关部分看上去也不是他主导的),而是指缺乏更深刻的系统性的观点。

不知道那些 C艹 厨有没有发现,辣鸡之处罄竹难书(不了解这点的我钦定你不怎么懂 C++ ,有异议也不用挣扎了),连言简意赅地概括都有困难?

很大原因就是问题太深刻(从一开始就错得离谱)了,表面缺陷一大堆难以概括(尽管只是要解决这类问题,补救起来不困难,以至于总是被寻常用户妥协)。

上面说了,这里的设计缺乏一种全局观念。提示,这里说的“全局”其实和某一类语言关系不大,而且多数语言设计者恐怕都未必有自觉(因为没有外力推动,很多人根本不会去好好写清楚 spec )。

举例:之前有人(@8pm )提过的移位和模板操作符冲突问题,其实如果不求精确,概括起来也不难,就是“文法辣鸡”。(具体到这个案例,就要分析 less than 为什么用来代替 chevron 以及字符集规范之类的“人文”的破事了……)

注意,重点来了:文法(grammar) 不是语法(syntax) 。(啥叫“语法”我也科普了好几年了……略。)

C++ 在这里有这种破事的根本原因之一就是它的设计实质上缺乏明确的“语法”概念,从现在来看设计者和绝大多数用户也没太关心。具体一点的,可以自己理解 phase of translation (也应该是基本知识点了)以及我对 ISO C++ 的某个 Annex 的修改。

其它反直觉的文法歧义(懂 C++ 的应该都被坑过,至少能随便列举出 1 个)基本上和这个都有一腿。

另外一个主要原因是一开始想从源码上兼容 C 的要求。这个也算是全局问题,不过从历史的角度上来看倒是可以见仁见智。

2.关于这类通用语言的特性应该怎么样的问题,还是得回顾一下 RnRS 的经典前言。

“Programming languages should be designed not by piling feature on top of feature, but by removing the weaknesses and restrictions that make additional features appear necessary.”

每个标榜通用的语言的设计者都应该了解这类规范需求的常识。可惜大部分涉众在这点上完全不称职,甚至根本不懂一个语言标准库存在的意义。

题外话, C++ 原本的核心设计哲学从适用范围来看比这个更加高大上(其弱化版本 zero overhead abstraction 也被 Rust 等照抄),只是实现起来更困难,显然现在的 C++ 也没做好。

3.为什么仍然需要不时钦点 Java 辣鸡?

其实不止是 Java ,不过 Java 一直以来都最典型,从未被超越。

这部分的主要问题还不在于语言的设计,并且更加罄竹难书,只能略点几句。不过对于缺乏独立思考者我还是要关心报道的偏差值的。

从产业角度上来讲,所有之前提的辣鸡不辣鸡的问题,都可以归结为开发者“圈内”人的问题。

然而 Java 之流造成的影响早就不限于产业内部。——而是逼着所有人最后一起 zz 的劣币淘汰良币的更加全局的问题。

典型例子就是关于当设备因为应用使用内存过多而炸了(虽然实际上仍然不一定是 Java 直接造成的),应该采取什么态度?

try
{
  内存.买买买();
  内存.插插插();
}
catch(资源不够 e)
{
  e.printStackTrace();
  e.爱用用不用滚();
}
finally
{
  最终用户必须懵逼认怂();
}

图样。实际上, Java 吹硬点“内存不用浪费”还是受到相当一部分最终用户的抵制的。不过随着硬件发展和某些行业傻多速等大环境因素,这些用户越来越有被边缘化的趋势。

那么,凭什么浪费资源是开发者而不是最终用户的权利呢?某些开发者因为自己见识短甩锅的这种吃相就实在有些难看了。


69L @福音战士01 :

你的论述中多次提到“实现细节”,没错,我的侧重点就是实现细节,而你和8pm的侧重点更多的是语言的设计,这引起了过度解读。

要学 C++ 更不应该死抠实现细节,因为语言本身就要求宽松。

比如你说 Java 是“编译型”语言,那么问题不大,因为 JLS 就是规定有“编译”这回事(虽然这话营养价值不大,会说这话的也未必就知道得编译成什么玩意儿)。但是不问青红皂白假定 C++ 的实现一定包括编译器,就是错的。注意,毫无疑问,错·的(即便你没有机会看到任何编译器以外的实现)。不过,这种取决于人为设计的是非问题一般人承认刻板印象不靠谱知道结论就好,就不要一厢情愿用臆想浪费时间了。

能严格地允许你忘记实现这点正是大部分语言可用性都不如 C++ 的主要原因之一(也是极少数 C 比 C++ 更强的地方),虽然被规范形式和市面上一般实现的质量给拖累了。

这些并不需要你做到架构师才认识到刻板印象和事实的矛盾而被迫反刍,而是一开始就应该了解的常识。

实际操作中,这意味着你只要你脑袋足够好使,就可以并行地学习语言和体系结构的其它知识,在之后再相互印证巩固基础。

而先学底层再学实现依赖的“高级”玩意儿,往往事倍功半:很可能学到后面的,前面的东西就忘了,但又忘得不彻底,留下一团不干不净的迷雾,给后来重新补课都可能添乱。

更严重的是这种完全不知道减小路径依赖的风险的学习方式容易导致学了之后搞不清领域的界限,最终不能快速找到准确的知识解决问题而纯属浪费。

(题外话:自习principle of least privilege。)

后者还可能是致命的——等遇到没人替你代劳的“冷门”而又不得不用的东西的时候就有的苦头吃了,也许很久之后才会发现失败的原因是基础半吊子没打好,但还是不知道到底是哪些基础有问题,于是要么放弃要么从头把所有课都补一遍。

这很有意思?

如果要说学习底层的知识,那为何不直接看底层的 spec 和现成的参考资料、做相关的项目?

如果说要训练填坑水平,难道不是减少琐碎的非决定性细节效率更高?

为什么这些课题要舍近求远?

话说回来,如果不是你缺乏现实验证的异想天开,是谁怂恿你去用这些东西联系实现细节的普遍低效的学习方式?他们更像是对实现精通的大师,还是样样不精的撸瑟?为什么你会同意他们的看法?

你说的对,我讲的就是实现细节。

这还是没到点上。

重新科普:不管是 C 还是 C++ ,定义语义的模型(还不是严格的形式语义)是抽象机,抽象机要求包括存储(memory) 。

如果你要纠结实现细节,那么层次实在太多了。就最粗浅的来讲,这类存储最常见的实现是机器的主存(更确切地,典型宿主实现中操作系统抽象出的虚存),和“不可离线的联机存储”(“内存条”上的“内存”)这种物理部件严格地是两回事。用“内存”这个词指代平时讨论容纳存储(storage) 的存储(memory) 空间,其实是翻译没词可用的妥协(或者按对岸的说法,“记忆体”可能更达意一些)。

不是我找你茬,是你讲得实在离一个靠谱的专业人员应该了解真正的“实现细节”差的太远,光这里就漏了三层以上的体系结构抽象。要知道上面还没提地址空间和工作集之类的呢……

为了不使你和群众对自然语言产生不必要的迷茫,就不继续展开了。

说回上面的。如果你不了解抽象机语义,你几乎肯定没法说清楚 volatile 是怎么回事。这就是不了解的基础重点的实际阻碍。

至于指针,除了文法和类型系统上的疙瘩,算术操作基本上就依赖数组的一些操作的定义。而数组的存储依赖对象模型和内存模型。对象模型本身依赖于内存模型。内存模型基于聊胜于无的“地址”的概念和抽象机语义。尽管某些情况实现起来可能很直接,不过离你看上去想要的东西差得也挺远的。

非要说不依赖于内存的话,是,有时候还会依赖于寄存器。

抽象机的存储不要求包括寄存器。如果你直接用类似 JVM 的 stack machine 实现 C++ ,也完全符合标准要求。

除此之外,也就是 register 关键字稍微和“寄存器”有些关系。然而 C 的 register 关键字近乎形同虚设, C++ 现在还直接取消了。

原因是这个关键字对无法精确描述RTL(寄存器传输级)作用的高级语言没有实效,纯属累赘。( C 没取消也就是兼容而已,实际上成熟的实现基本都忽略“放在寄存器”的建议。类似的还有 inline 的内联,但 inline 还有另外无法忽略的语义。)

具体来讲,除了表达其它自动存储类一样的含义外毫无卵用——凭空制造不靠谱的未定义行为,反过来还可能阻碍优化,甚至基本上没什么可移植性。

你忘了还有#undef

……你至少还漏了还有 #line ,以及 #ifdef/#else 和包含空预处理翻译单元的 #include ,等等。(嘛,先不管 #pragma STDC FENV_ACCESS 和 #pragma omp 之类的哭晕在厕所, #pragma GCC/Clang dianostic什么的还是能插一脚吧……)

我觉得不需要在这里多科普。重点是不管从语言规范还是实际实现都不保证存在这种“行”的方式和汇编指令的对应(即便保证用到了汇编)。所以你举这个例子的意义何在?

编译器是如何识别动态类型-->编译器如何编译代码使运行期对虚函数的调用绑定到正确的动态类型。对于其它解释不做讨论。

看来你想说的术语是 C++ 里的没错,然而可能因为基础知识不牢固,有些误会。

典型的实际实现中, C++ 编译器不需要识别动态类型,只需要确定静态类型。识别动态类型(及附加一些动态类型检查)在典型实现中由编译器生成的二进制桩代码/跳板配合运行时的二进制代码(具体来讲, libsupc++/libcxxabi 之类)完成,并且不限用于虚函数的 overrider dispatch ,还包括 RTTI 和一般的异常处理。

我是不理解C++,你理解?

你觉得在上面提及的这些问题中,你能找到我有什么地方不理解么。

或者退一步讲,你能找到哪些问题实际上是对理解 C++ 必要的?

CRT依我理解在某种意义上,和java的jvm,python的解释器作用是一样,都是一个操作系统和语言实现之间的适配器。学一门语言必不可少要了解其实现,否则到了真要对效率和安全有强要求的时候会力不从心。

仅仅了解这些“作用”是不够的,因为真没多大用……

如果你真想认真学习实现细节,那么至少需要了解来源,否则参考源码到哪找都找不到那就是笑话了。

对 C 、 C++ 和其它大部分存在 spec 的语言来讲,决定语言特性和库实现的来源的不是具体的实现。不同实现可以有不同的做法,依赖具体实现管中窥豹,会聪明反被聪明误。

CRT(C runtime library) 是微软等厂商对共用 C 函数库的实现的说法,但 ISO C 和 ISO C++ 都并不要求实现以运行时库来实现标准库(此外, C 允许库函数直接实现为宏但 C++ 禁止)。具体举例,如 GCC 可以对一些 C 例程提供 __builtin_foo 来代替运行时库提供的函数 foo 。调优库函数的时候不可能不知道这一点,但要说清楚为什么可能会这样,直接看实现是没办法的(就是编译器手册一般也会引用语言规范而不是自己罗嗦一遍)。

而 JVM 是 JVMS 规范要求必须要有的组件。不仅 spec 上的地位不同,实现粒度也不一样—— JVM 可以相对容易地映射到硬件上,而 C 运行时库离硬件仍然太过抽象,至少还差了一层 ABI 实现。两者完全不在一个层次上。

另外注意,你知道的一些内容,对你现有的知识构成并不是聊胜于无,而是基本上约等于没有,并且因为全局性理解错误可能会自我误导。

例如,对于实现架构你有一个重要的理解错误。 ISO C 和 ISO C++ 明确支持 freestanding implementation ,不要求存在操作系统。允许不依赖操作系统也是比其它多数语言优越的特性之一,虽然因为要求相比存在操作系统的宿主实现(hosted implementation) 低,实现很可能是缩水的。

严肃地说,你的错漏来源于你学习方法论上的缺陷。如果你真心要了解实现,那不是缺几门操作系统和组成原理课程的问题。

vector默认也是用的std::allocator,然而很少会产生内存碎片以及由此引起的crash

说到底std::allocator的行为和new也没差多少,所以默认情况下map这种基于节点的容器在大量操作下就是会掉坑

不是没差多少,是 ISO C++ 要求分配操作直接包装 ::new ……注意还是有区别的,比如你重载了 operator new (不是重写 ::operator new )的情况下。

坑的来源跟语言确实没多少关系,对语言来讲 ::new 不好应付这类数据结构的分配需求,这是 QoI 问题。

当然,用户应该知道可能普遍存在的这类坑——和通用 GC 一样,默认 ::operator new 实现在平坦的地址空间内分配未知大小的连续空间,不得不使用一些启发式预测来适应普遍状况,这对于基于每节点分配不带缓存策略的容器很不友好。

不过,这仍然不属于“实现细节”。缺乏指定策略可以从 API 上看得出来,剩下的则是数据结构方面的常识了。即便 ISO C++17 之前没有强调某些容器用节点实现,在容器要求上只保证复杂度而完全不管常数这点来看也该有心理准备——从来就没谁有办法保证容器的每一次分配都尽量高效,使用具体确定的分配策略是用户的责任。

其它的不想再多解释了,感觉要像你这样钻牛角尖,以后谁也别用“会”和“明白”这样的词。

说了这么多你“会”C++?你“懂”语言设计?你自己都不“明白”还在网上到处讨论?

自称会不会的问题,要懂得审时度势。

以前嘛 BS 说他会六成,现在嘛大概也不会这样说了。

但是好歹我能看到你哪里真不明白(虽然你可能强行自封为明白了),并且之前有过类似你这样的理解对他人产生误导的案例——基于某些立场我需要澄清一下。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment