Smiley face

我的征尘是星辰大海。。。

The dirt and dust from my pilgrimage forms oceans of stars...

-------当记忆的篇章变得零碎,当追忆的图片变得模糊,我们只能求助于数字存储的永恒的回忆

作者:黄教授

二〇二一


一月一日 等待变化等待机会

  1. 对于dynamic programming这个名字我始终难以领会,我的感受就是对于太多递归导致stackoverflow来说,反向的从栈的底层创建而省却递归就是DP。这个著名的找零钱的问题,如果我使用通常的递归当数字太大一定会溢出,所以不得已只能使用DP。
  2. 这个视频提出的所谓的"algebraic data type"对我是一个很大的教育,让我把这些个看似不相干的结构串起来了
    pairtuplevariantoptional
    product typeproduct typesum typesum type
    。 我第一次由衷的认识到optional是解决很多程序员日常小错误的最好的工具,比如我经常要设置一个无穷大的数字来表达根本不存在的值,可是这个本来可以轻松的使用optional来表达的,我应该自觉的使用它!不过逻辑上要小心的使用。联想到上一次观看视频提到的常见的map的错误,就是一个典型,如果map的返回值是一个optional也许就可以防止发生很多的错误吧?不过那个问题是要使用const string&来避免copy,这样子optional返回一个rvalue reference也是无解。
  3. 结果我在查看我自认为good old pair的时候发现了它有一个新鲜的玩意儿:piecewise_construct,这个神奇的东西让我迷惑了好久,我竟然看不懂,直到看这个定义才明白如此,它和in_place之类的一样是一个tag,来标识什么样的ctor我们要调用的。 这里引出的piecewise_construct要紧密联系forward_as_tuple我之前看不懂这个用途,结果现在才知道它的威力。我只能再次感叹c++11之后的大变化让人目不暇接,我现在在为这十年的大跃进来补课。
  4. 这里有一个纪年表
    YearC++ Standard Informal name
    1998ISO/IEC 14882:1998C++98
    2003ISO/IEC 14882:2003C++03
    2011ISO/IEC 14882:2011C++11,C++0x
    2014ISO/IEC 14882:2014C++14,C++1y
    2017ISO/IEC 14882:2017C++17,C++1z
    2020ISO/IEC 14882:2020C++20,C++2a
    那么针对这个表我们要怎么定义一个结构来表达它呢?我决定使用algebraic data type的这些个结构pair, tuple, variant, optional来表达它。这里我想尽量展示他们的不同的创建方式,就好像我们研究“回”字有多少种写法一样。
  5. 这个visit太令人炫目了!

一月二日 等待变化等待机会

  1. 听了这个入门级的关于lambda的总结,我感觉脊背发凉因为这么多的基本的点点滴滴我都不知道!当然lambda在c++11以来已经修改了三次了,估计还会继续修改吧,要准确的掌握真的不容易。我学到了一个很实用的小点滴,就是我常常困扰于map的comp的定义是很麻烦的,通常我在map的ctor里传入comp的实例,这个其实很啰嗦,如果这个比较函数根本不会被别人使用何必要实例,所以在map的模板参数里用它的类型来表达是最好不过的了,这里也就引出了所谓的generic lambda的概念,如果lambda没有capture其实就是constexpr,这和一个类型有什么区别的呢?这里有两种做法,一个是generic lambda 或者是一个模板的lambda 那么我们可以创建两个map使用不同的比较规则 现在我们练习一下structured capture来打印两个map看他们的排序是否正确 结果当然是正确的
  2. 这个visit的例子实在是太奇幻了,我看了好久都无法真正的领会! 这个万能的类是什么东西呢? 注解上说这个c++20就不需要额外的explicit deduction guide,可是我的gcc-10.2.0的编译器依然不认,难道这个是还没有实现的新feature吗? 没有这个deduction guide,我也看不懂它是什么,现在才有些明白是返回一个模板类的实例?而它多重继承的多个类实际上通常是lambda或者函数指针也行,那么当它被实例化的时候,通过参数的类型来做一种类似于specialization的选择最合适的lambda,也就是说它直接选择它继承的lambda,那么它自然就可以作为那个lambda来用了。这个是我第一次意识到多重继承可以作为类似虚拟函数或者说overloaded的函数指针来用!这个实在是太巧妙与神奇了,也许c++的发明者也没有想到可以这样子用吧?这个就是这个语言的生命活力之所在,不可限量?这个依靠编译器根据call operator的参数来匹配最适合的lambda简直就是颠倒我的三观,这个算是specialization吗?这么看来这个并不是真正的多重继承,只能说是“选择性”的继承吧,模板参数代表了它有无数可能继承的选择,它的确继承了多个lambda那么具体调用那个lambda是选择参数的匹配,这个多重继承告诉我们多重继承有什么好处怎么使用!
  3. 我对于visit的不理解来源于我并不熟悉实用variant的场景,这个是我在实用过程才意识到人们的想法是相似的,比如我发现这个帖子才意识到visit是专门为了variant量身定做的,难怪我看不懂这篇论文的动机,因为我压根没有理解实用variant的麻烦地方。我很欣赏论文提到的真实代码的应用部分,它让我意识到一些实用的场景。比如我们遇到这个实际的实例假如没有visit那么我手动写一个处理真的很啰嗦,我想结合optional真的有些鸡肋,似乎多此一举。 顺便说到我忘记去除var的reference结果这个简单的static_assert花了一些时间 所以,对于variant的一致性的访问代码就是visit的动机,这个是oneliner的visit 对于visit的实现当然是非常的不容易理解,我一开始就被它的实现所纠缠完全不理解应用,这个是明显的本末倒置。
  4. 我不是很理解什么叫做free-standing

一月三日 等待变化等待机会

  1. 我一直想做一个很小的简单的东西就是有时候我想make_integer_sequence产生的数列是从1开始而不是从0开始,这个其实可以在产生的数组以后再去处理就容易多了,不过作为一个心病我一直想做这么一个东西,其实很简单但是我总是想不清楚 这里我使用了所谓的familiar lambda,我有点忘记这个名字了,反正就是lambda里面的lambda,而且是一个模板的lambda,至于调用模板还是有些突兀的 这里的结果当然就是1,2,3,4,5,而不是通常的make_integer_sequence的从0开始了。
  2. 那么同样照猫画虎的我们可以制作一个偶数数组
  3. 制作一个奇数数组不是类似的吗?
  4. 买菜回来才意识到创建起始为1的数组根本不需要那么复杂,完全可以仿照偶数数组奇数数组的,所以只要改一下这个parameter pack的操作 类似的平方数的数组是这样子
  5. 我终于发现了我犯下的一个极其不可饶恕的错误概念,就是index_sequence实际上是一个类型而不是一个实例。当然它是一个结构,没有ctor,但是我应该把它当作类型来处理。我觉得这个简直就是理所当然的为什么我会把它当作什么特殊的函数呢?它是一个metafunction而不是普通的函数。
  6. 怎样reverse一个index_sequence,我记得mpl里有类似的算法,我尝试了好久不知道这个递归要怎么写,google发现可以简单的实现一个特例,就是假定是make_index_sequence产生的
  7. 这个视频还是很有启发的,虽然这个制作fibonacci常数的想法并不是很难,但是做了才发觉还是底子不够。这个是主讲人的做法 我曾经试图把函数改成模板类,但是好像有困难,递归有些麻烦,反正都是常数其实差不多吧。于是我加了一个小的warpper来计算所有的结果并存储在数组里 因为函数返回的是integral_constant,那么我只能使用tuple来存储,现在想想看也许把Fib函数改变返回值为整数更好吧?只不过这个常数特性不知道是否有影响?照理不会。结果我在使用apply的时候又一次的卡壳了,之前的照猫画虎习惯了实际上并不理解apply的函数参数是要对全体tuple成员起作用的,所以,apply的lambda参数是一个parameter pack,我却始终认为和普通的遍历无区别所以完全没有意识到lambda的参数类型根本不是一个类型而是所有的类型!注意这个参数auto&&...p它是所有的tuple element 的类型,那么针对这一点我对于parameter pack的展开和“fold"的操作符又一次的模糊了,这里(v.push_back(p.value), ...);我们使用了comma(,)所以是fold operation,可是我却鬼使神差的拼命想要使用parameter pack的本身的binary operator想要试图直接返回一个数组,这个如果parameter pack是一个Integer_sequence我想直接return {(p...)};或许是可以的吧?其实有时候是最简单的反而是最后才想道的,我根本就不应该使用call operator或者parenthesis,因为他们都是为了作为函数参数等等复杂操作,简简单单的使用Brace-enclosed initializers就可以了,所以就是返回一个向量,当然如果你不指示返回值的话编译器要抱怨这个是initializer_list不允许返回。所以 可是现在p代表的是一个一个integral_constant,我需要取得他们的value成员变量,但是parameter pack期待的是一个binary operator。

一月四日 等待变化等待机会

  1. 现在回过头来再次浏览腾讯的广告系统的介绍分享可以感受到真正的大规模c++项目所实际面临的困难与解决方案。尽管是一家之言但是都是实干出的真知,远比一些坐而论道的虚拟方法来的真切,因为模板只有实例化才能发现错误也才能真正得到应用。当然很多介绍是基于大规模项目才能应用的但是还是能够遥想一些感悟,尤其是十年前的那段经历也算是比较大的项目吧,因为任何一个手机系统的全部从最底层的linux的内核到应用全部是c/c++绝对是巨大的挑战。当时使用过distcc,甚至包含敏感代码库管理,我甚至之前还写过一个简单的敏感库控制系统,现在还是颇为自豪的。这位先贤赞成不依赖二进制码而是一切从新编译,不赞成预编译头文件这一点我还没有体会到,但是我也隐约感觉预编译头文件有它的局限性,主要还是对于代码文件引用的限制以及在多大程度上能够加快编译速度我也不是很乐观。这一点
    可执行文件尽量采用静态链接,包括libgcc和libstdc++,可以减轻部署的复杂性,减少对编译器升级的负担。
    让人有些启发,不过我曾经经历过静态库的循环依赖,在link过程中很痛苦的调整库之间的顺序,甚至一个库要反复出现,也许是我的编译方法有问题,但是编译静态库显然要复杂的多吧,就是要把所有的依赖库都编译一遍压力可想而知,难怪编译速度是大问题。
  2. 我尝试打印我的编译信息,令我吃惊的是__cplusplus的值的定义。 这个输出1;201709;gccTest.cpp;3228;Jan 4 2021;05:20:07;1;里为什么__cplusplus定义为2017呢?我的编译器明明已经指向了gcc-10.2.0了?这里有位大侠这么解释
    • The 199711L stands for Year=1997, Month = 11 (i.e., November of 1997) -- the date when the committee approved the standard that the rest of the ISO approved in early 1998.
    • For the 2003 standard, there were few enough changes that the committee (apparently) decided to leave that value unchanged.
    • For the 2011 standard, it's required to be defined as 201103L, (again, year=2011, month = 03) again meaning that the committee approved the standard as finalized in March of 2011.
    • For the 2014 standard, it's required to be defined as 201402L, interpreted the same way as above (February 2014).
    • For the 2017 standard, it's required to be defined as 201703L (March 2017).
    • For the 2020 standard, the value has been updated to 202002L (February 2020).
    • Before the original standard was approved, quite a few compilers normally defined it to 0 (or just an empty definition like #define __cplusplus) to signify "not-conforming". When asked for their strictest conformance, many defined it to 1.
    实际上我看到有人指出标准委员会的网页,其实并没有提到c++20,今天已经2021年了,已经通过的新标准还没有被委员会接受吗? 这个是所谓的标准(它的出处我还需要再查询),那么源代码怎么说的呢?我搜索gcc-10.2.0的源代码,在changelog里有这样的log: 看来我的编译文件没有错误,这个的确是gcc-10.2.0的情况,进一步查询这个init.c可以看到 这个说明了什么呢?这个CLK_CXX2A显然是c++20的代号,至于是否是最终代号我不知道,也许是某个过渡?但是也许就是最终的。但是无论如何对于gcc-10.2.0它就是定义的201709。
  3. 进一步查看比较新的gcc-11的一个snapshot 20201122这部分并没有改变。我的结论就是在gcc感到比较有信心支持c++20标准之前是不会声明自己是真正的c++20的吧?通过这部分libcpp/init.c代码也看到不少有趣的东西,比如一个很大的语言的feature的矩阵 那些个缩略语在下面的flag里有全部的体现
    initialsfull flag
    c99c99
    c++cplusplus
    xnumextended_numbers
    xidextended_identifiers
    c11c11_identifiers
    stdstd
    digrdigraphs
    ulituliterals
    rlitrliterals
    udlituser_literals
    bincstbinary_constants
    digsepdigit_separators
    trigtrigraphs
    u8chlitutf8_char_literals
    vaoptva_opt
    scopescope
    dfpdfp_constants
  4. 这个c++标准的工作版本有1800多页,我还是保存一个拷贝吧,省的正式文件出来要花钱才能下载了。我在这里看到它(N4878)实际上是c++23的建议书?不过怎么说它都是一个比较完整的c++标准虽然是面向c++23,但是向后兼容应该是可以作为手册使用吧?最大的好处是免费的。我查到了它关于cplusplus的定义 所以,毫无疑问的c++20应该是要设定的,但是gcc-10/11没有打算声称自己实现了c++20吗?
  5. 针对一个类如果继承自多个类而这个类自己不知道继承的父类有哪些的问题,我想了一个办法,就是设定一个成员变量tuple来存储这些继承类的实例。这个完全是因为凑巧我的继承类需要父类的实例作为ctor参数才有可能吧?因为这个是模板CTAD方便节省给定模板参数的设计,并不是普遍的情况。话虽是这么说但是假如继承的父类都是consttructable的话我也可以不用实例来创建tuple,而是用继承类的类型来,比如也许可以使用复杂的type_traits来过滤掉继承类是虚类的部分,但这个超出了我的能力。我仅仅是想看看有没有什么办法记录下继承的情况,这个是只有在constructor才有办法的吧。 同样的一个辅助类来说明继承的父类 这个是一个简单的测试 结果是可期的0,1,2,
  6. 原来可以使用所谓的Mathematical Markup Language来显示非常漂亮的数学符号公式!
  7. 我找到讲座里的打印日历的源代码,作者说这个是受到这个网站的思想的启发。

一月五日 等待变化等待机会

  1. 我尝试着无事生非,也就是看着运行正常的eclipse结果主动升级导致indexer不再正常工作,这个是预料中的,因为所有的eclipse for c++对于旧版本的workspace的setting一律说不兼容建议不要覆盖,最安全的应该是创建新的workspace,而eclipse里有一个非常贴心的一键转换到不同的workspace,所以我认为这个是最稳妥的。然而我大概是出于不可告人的目的故意让工具不工作以便自己逃避劳动吧。总之要重新创建新的工程看能否调整好,主要的问题在于我使用的不是Ubuntu自带的gcc-7.5的编译器,而是自己编译的gcc-10,那么怎么让eclipse了解这一点呢?在eclipse里编译是一个小问题,使用自己的Makefile也可以做到,但是要利用IDE的syntax parser就必须使用eclipse知道的方式,与其修改路径不如直接把编译设置的g++命令改成全路径的我希望的gcc-10,然后eclipse就能自己发现。另一个当然是在compiler dialect里设置c++20是最好的选择,而不是简单的在misc里设定-std=c++20这个是具体的编译,但是我们要让indexer明白我们的目的这个才是真正需要的。
  2. 我究竟对于编译懂多少呢?如果我是纯粹的使用std library的话,我需要其他的库来链接吗?似乎顺理成章的不需要,难道不是吗?都是头文件你为什么需要链接呢?g++自带的库是预设的,当然libc++的库我使用的不兼容是另一回事,但是编译是不需要的,那个是运行期的问题。至少我应该早就质问为什么pthread还要加上呢?更加搞笑的是我的项目设置始终都遇到在链接的时候“阿三”(libasan.so)捣乱的问题,难道我没有意识到这个-fsanitize=address开关是一个特殊的问题,它必须要在链接的时候加上。这个问题我还没有想明白,但是这个是一个特例吧,当然我之前的做法我都忘记了所以才会犯糊涂,因为我讨厌运行期要设定LD_LIBRARY_PATH指定我的编译器的目录(/home/nick/opt/gcc-10.2.0/lib64/)(我在/home/nick/opt/gcc-10.2.0这里部署我的gcc-10.2.0,所以相对路径就是lib64下面的运行库,不知道当时是怎么想的就把libc++abi.a作为静态库来链接,这个肯定是一个好办法,但是我随后就忘记了,导致我一直认为链接是必须的。现在总结一下,还是使用eclipse原生的managed的项目来做比较好,压根不需要写自己的makefile,在include里指定自己的precompiled.h作为触发PCH的手段,也就是说-include precompiled.h强迫寻找precompiled.h.gch,当然自动化编译PCH我还是觉得只有自己makefile才能做到。其他不需要做什么路径,只需要把编译器的命令由默认gcc/g++改为决定路径的我的gcc-10,让后让eclipse自己去发现好了。
  3. 我只能这样解释给我自己,如果在链接的时候加上-fsanitize=address我是要求gcc使用自带的libasan来链接,而并没有明确说明我要使用这个库来链接,而我之前是讨厌这个动态库,需要使用静态库,但是遇到编译gcc的时候编译静态asan不成才想到要用llvm的替代。这个隐含的意思就是说如果我不加这个开关的话,gcc很自然的要求你做链接因为有很多symbol无法解决,那么我这个时候只能使用自己的链接语句,我的情况就是直接静态链接llvm编译成的静态库,这个和gcc自带的是等效的,我认为。
  4. 首先在c/c++ general/preprocessor include path Macro etc/providersCDT GCC Builtin settings修改命令/home/nick/opt/gcc-10.2.0/bin/c++ ${FLAGS} -E -P -v -dD "${INPUTS}" -std=c++20 其次似乎不是很必要吧在CDT GCC Builtin Output parser修改为(g?cc)|([gc]\+\+)|(clang) -std=c++20这样子可以自动产生编译器的默认设置。
  5. 回过头来在CDT User Settings Entries里添加/usr/include;/home/nick/opt/gcc-10.2.0/include;等等,(也许有用吧?)
  6. 在c/c++ build/Build Location修改为当前项目的根目录这样子就可以直接使用我自己的Makefile了。
  7. 在c/c++ General/Path and symbols/include设定GNU C++路径为/home/nick/opt/gcc-10.2.0/lib/gcc/x86_64-unknown-linux-gnu/10.2.0/include
  8. 尝试一下做一个double linked list结果内存总是泄漏,难道使用shared_ptr有什么问题吗?
  9. 我最后一个尝试是设定项目为cross compilation,因为我感觉我使用另一个版本的编译器有一点点像是交叉编译,就是编译器路径需要指定,这个和交叉编译没有什么区别的。不过我还是不停的遇到eclipse报错一会儿是stackoverflow,一会儿是什么waiting exclusive thread access之类的看不懂的错误。不过总结一下似乎就是这么几个要点。创建普通的managed c++ project然后改变build directory,在preprocessor的provider处改变默认的编译器c++为我的绝对路径,使用我自己的makefile by untoggle automatically generating Makefile的选项。大概是这样子吧。总之,受制于人是没有办法的。

一月六日 等待变化等待机会

  1. 你不能说我纯粹就是无事生非,总之在有闲暇的时候折腾这个工具是一种加深认识也是一种学习。总之我也明白了一些基本常识,比如.metadata存储是workspace的设定,eclipse的这个workspace实际上是非常不错的设定,允许我在多个workspace快速切换因为我可能在不同的workspace有不同的设定,而每一个project本身也可以有自己的设定,这个让我对于一些general的设定就少了很多的顾虑,因为大不了就换一个workspace完全全新的,我就在也不用反复安装新的版本了。这个是非常好的设定。其次,在.metadata/.plugin/org.eclipse.cdt.core下面[project-name].[timestamp].pdom是我的indexer的数据,和他并列的.xml是project的配置,这个对于了解project配置很有意义。就在我检查新版的eclipse时候注意到这里的org.eclipse.cdt.core已经分拆为两个目录了,一个后缀是resource,一个是runtime,也许这就算和旧版本不兼容的原因吧。看错了是org.eclipse.core.resources和org.eclipse.core.runtime,他们是无关的。
  2. 这个帖子不错它告诉我怎么过滤掉一些文件让indexer不要徒劳的parsing。我下载了llvm想看看怎么编译libcxx的,eclipse应该是卡在了clang的parsing吧。我根本不需要他们,正好过滤掉了。
  3. 无意中发现了一个关于质数的网站挺有意思的。
  4. 这篇文章可能是很好的关于indexer的指引,我现在没有耐心去读。以后在说吧。
  5. 你能否使用metaprogramming的技巧编一个函数判断一个数是否为质数?听上去似乎很容易。我还是化了一些时间的。首先这个是一个简单的实现效率不是首要考虑因为是编译期的事情,没有占用运行期的资源,因此我也没有计算平方根而是简单的减半作为上限来实验整除。其次,我想说这个做法不是完全没有意义的,因为有一些算法依赖于寻找质数,虽然可以很容易的用一个事先设定的表来存储不多的质数。(我看到这个在c++的hash函数里有这么做)实际上也可以使用这种constexpr的函数来事先生成。但是如何把运行期的数值和编译期的来结合我还有一个鸿沟没有明白,也许我去看看boost::hana就能够有些理解了吧? 这个lambda是检验对折的数列是否能够被整除,注意这里的效率肯定是不高的因为我无法中间返回而是返回一个tuple的余数来最后使用apply里的fold操作返回结果。不过这个都是编译期的计算可以原谅吧。注意这里我有意使用没有必要的参数integral_constant就是为了简化调用避免使用模板参数而是使用实参让编译器去推理模板参数否则lambda的模板参数传递是比较另类的,会让很多人产生不适感。至于调用的wrapper是很简单的 这里同样的我为了避免lambda的模板参数传递而把模板的参数变成了实参integral_constant,这样子就避免了调用的不适感,而且我计划将integral_constant存储在一个数组里,这样子就可以使用运行期数据来检验了,也就是运行期调用编译期结果的想法,这个还依赖于我测试才行。这个是不可能的因为他们不是一个类型,tuple也没有static的,所以,我觉得不可能。
  6. 我写了一个测试程序,这个还是很不错的。这个测试程序把一个数列都去呼叫我们要测试的isPrime来打印出结果,我跳过了0和1,所以才会+2,这里使用的是comma作为operator的fold,写的比较绚丽。 测试的主程序传入一个数列来测试 这个是打印的结果
  7. 我找了半天才发现calendar还没有实现。比如day
  8. 我终于搞明白了为什么有内存泄漏的问题了,所谓的double link list我的设计导致了类似于deadlock的形态。因为这个说明了我并没有真正理解shared_ptr的真正的机制,它的目的是责任,就是说你拥有一个指针不是说只有利益还是有责任的,而你的责任就是你拥有释放对方的义务,所以,这里应该是一种类似singlely link list,也就是说像军队的chain of command的形式一样,当父亲节点被消灭它有义务通过释放子节点的指针而达到释放子节点的效果,这个就好像是多米诺骨牌一样。相反的之前的prev反向指针现在改成了weak_ptr,它是不增加引用计数的这样子就保证了它达到单链表能够依次释放的能力,而反向weak_ptr可以在需要的时候lock来操作父节点。 这个是测试代码用来打印链表 创建的过程以及我有意识的释放头节点来看看是否每个节点依次被消灭,中途我还做了一个特别实验hold住一个中间节点观察到它必须要等到这个节点被释放后边的节点才被依次销毁。 结果是正确的

一月七日 等待变化等待机会

  1. llvm里包含了libcxxabi和libcxx,我当初为什么要链接这个静态库呢?我查看以前的笔记才想起来是因为gcc-10.2的glibcxx升级了"/usr/lib/x86_64-linux-gnu/libstdc++.so.6: version `GLIBCXX_3.4.26' not found"的问题需要我运行期来解决。不过这里我还是有些模糊。不过我想说的是llvm可以单独把libcxxabi,libcxx,llvm三个目录保留下来,不要用llvm的总的cmake编译,因为它又需要你编译clang的东西,对于我只需要编译libcxx来说直接在它下面创建一个build目录,然后使用3.18以后版本的cmake就可以成功编译libcxx了,libcxxabi也是类似,不过我不太明白它的用途,似乎是一个套层也许是clang的编译器需要的隔离的东西吗?总之我认为都是为了保持运行期接口一致的。这个当然都是废话否则为什么需要abi,不就是这样子吗?
  2. 看到libcxxabi里使用很多宏来改变一些libcxx的接口,难道是隐藏吗?因为使用inline就可以不用考虑abi的改变了?这样子就不需要持续升级运行环境?我不明白它的用途,不过这里对于inline的解释,我还是要收藏一下,因为还是有些模糊。以下这段话让我非常的费解:

    The three types of inlining behave similarly in two important cases: when the inline keyword is used on a static function, like the example above, and when a function is first declared without using the inline keyword and then is defined with inline, like this:

    extern int inc (int *a);
    inline int
    inc (int *a)
    {
      return (*a)++;
    }
    

    In both of these common cases, the program behaves the same as if you had not used the inline keyword, except for its speed.

    这个是什么意思呢?是说inline无效吗?inline难道不是我们理解的inline吗?
    When a function is both inline and static, if all calls to the function are integrated into the caller, and the function's address is never used, then the function's own assembler code is never referenced. In this case, GCC does not actually output assembler code for the function, unless you specify the option -fkeep-inline-functions.
    这个当然是我们理解的,那么为什么无效呢?这里我对于gcc的声称也是半信半疑,实际上我确实看到了函数体的汇编代码,即便是static(就是local function)但是似乎在没有优化的情况下没有变化的。也许说的是优化以后的情况吧?我做了一下-O2/-O1的编译的实验结果inc函数的代码彻底就被优化掉了,因为他们本身就没有存在的价值,所以看不出来这个被优化成了我们希望的inline的模式。inline有和没有确实没有区别!
    我们来做实验吧这个是源代码,就是文档说的如果定义了static,但是第一次声明的时候没有说它是inline的而且我们使用c++编译器来编译,这个符合原文的要求 编译不带任何参数的c++编译g++ test.cpp -o inline.exe 然后来看汇编吧objdump -CtTS inline.exe | less
    000000000000066a <main>:
     66a:   55                      push   %rbp
     66b:   48 89 e5                mov    %rsp,%rbp
     66e:   48 83 ec 10             sub    $0x10,%rsp
     672:   64 48 8b 04 25 28 00    mov    %fs:0x28,%rax
     679:   00 00 
     67b:   48 89 45 f8             mov    %rax,-0x8(%rbp)
     67f:   31 c0                   xor    %eax,%eax
     681:   c7 45 f4 00 00 00 00    movl   $0x0,-0xc(%rbp)
     688:   48 8d 45 f4             lea    -0xc(%rbp),%rax
     68c:   48 89 c7                mov    %rax,%rdi
     68f:   e8 16 00 00 00          callq  6aa <inc(int*)>
     694:   48 8b 55 f8             mov    -0x8(%rbp),%rdx
     698:   64 48 33 14 25 28 00    xor    %fs:0x28,%rdx
     69f:   00 00 
     6a1:   74 05                   je     6a8 <main+0x3e>
     6a3:   e8 98 fe ff ff          callq  540 <__stack_chk_fail@plt>
     6a8:   c9                      leaveq 
     6a9:   c3                      retq   
    
    00000000000006aa <inc(int*)>:
     6aa:   55                      push   %rbp
     6ab:   48 89 e5                mov    %rsp,%rbp
     6ae:   48 89 7d f8             mov    %rdi,-0x8(%rbp)
     6b2:   48 8b 45 f8             mov    -0x8(%rbp),%rax
     6b6:   8b 00                   mov    (%rax),%eax
     6b8:   8d 48 01                lea    0x1(%rax),%ecx
     6bb:   48 8b 55 f8             mov    -0x8(%rbp),%rdx
     6bf:   89 0a                   mov    %ecx,(%rdx)
     6c1:   5d                      pop    %rbp
     6c2:   c3                      retq   
    
    这里在main里我们看到了调用inc的语句
    00000000000006aa w F .text 0000000000000019 inc(int*)
    所以,这个inline的确无效。不过我们注意到这个inc是一个"weak" symbol。而且我即便把源代码的extern或者static都去掉也是一样,该调用函数还是条用函数,就是说函数的压栈出栈一点也没有少。 但是如果我们把extern改为static iniline呢?注意编译器警告不能或者不应该同时,extern或者static只能二选一。
    inline static int inc (int *a){
    结果函数还是函数不过是从全局的weak变成了local函数
    000000000000066a l F .text 0000000000000019 inc(int*)
    那么如果我们按照gcc的指示使用__attribute__((always_inline))在函数的声明呢?
    inline int inc (int *a) __attribute__ ((always_inline));
    这个是函数main的改变,你再也看不到调用inc了,因为在symbol里找不到它了。 这里你还能认出inc里的代码吗?偏移从-0x8
    mov -0x8(%rbp),%rdx
    变成了-0x10
    mov -0x10(%rbp),%rax
    是因为之前函数里a是inc的参数,而现在a是main里的栈里的临时变量。
    000000000000066a <main>:
     66a:   55                      push   %rbp
     66b:   48 89 e5                mov    %rsp,%rbp
     66e:   48 83 ec 20             sub    $0x20,%rsp
     672:   64 48 8b 04 25 28 00    mov    %fs:0x28,%rax
     679:   00 00 
     67b:   48 89 45 f8             mov    %rax,-0x8(%rbp)
     67f:   31 c0                   xor    %eax,%eax
     681:   c7 45 ec 00 00 00 00    movl   $0x0,-0x14(%rbp)
     688:   48 8d 45 ec             lea    -0x14(%rbp),%rax
     68c:   48 89 45 f0             mov    %rax,-0x10(%rbp)
     690:   48 8b 45 f0             mov    -0x10(%rbp),%rax
     694:   8b 00                   mov    (%rax),%eax
     696:   8d 48 01                lea    0x1(%rax),%ecx
     699:   48 8b 55 f0             mov    -0x10(%rbp),%rdx
     69d:   89 0a                   mov    %ecx,(%rdx)
     69f:   48 8b 75 f8             mov    -0x8(%rbp),%rsi
     6a3:   64 48 33 34 25 28 00    xor    %fs:0x28,%rsi
     6aa:   00 00 
     6ac:   74 05                   je     6b3 <main+0x49>
     6ae:   e8 8d fe ff ff          callq  540 <__stack_chk_fail@plt>
     6b3:   c9                      leaveq 
     6b4:   c3                      retq   
    

一月八日 等待变化等待机会

  1. 人的正确思想是从哪里来的?

    毛泽东 一九六三年五月

    人的正确思想是从哪里来的?是从天上掉下来的吗?不是。是自己头脑里固 有的吗?不是。人的正确思想,只能从社会实践中来,只能从生产斗争、阶级斗 争和科学实验这三项实践中来。人们的社会存在,决定人们的思想。而代表先进 阶级的正确思想,一旦被群众掌握,就会变成改造社会、改造世界的物质力量。 人们在社会实践中从事各项斗争,有了丰富的经验,有成功的,有失败的。无数 客观外界的现象通过人的眼、耳、鼻、身这五个官能反映到自己的头脑中来,开 始是感性认识。这种感性认识的材料积累多了,就会产生一个飞跃,变成了理性 认识,这就是思想。这是一个认识过程。这是整个认识过程的第一个阶段,即由 客观物质到主观精神的阶段,由存在到思想的阶段。这时候的精神、思想(包括 理论、政策、计划、办法)是否正确地反映了客观外界的规律,还是没有证明的 ,还不能确定是否正确,然后又有认识过程的第二个阶段,即由精神到物质的阶 段,又思想到存在的阶段,这就是把第一个阶段得到的认识放到社会实践中去, 看这些理论、政策、计划、办法等等是否能得到预期的成功。
    其实在我看来人是否需要正确的思想这个基本问题很多人都没有搞明白,或者不屑于肯定。为什么呢?人的思想是否正确要由谁来评判?各个人内心自己创造出的灵魂主宰命名之为神的精神存在来裁决吗?一个人平常并不需要有什么正确的思想,因为一个人很多时候可以按照他的意愿相信任何他愿意相信的理论,只要他不和时间和世界发生碰撞他可以永远生活在自己的世界里相信任何他自己愿意相信的理论与信仰。唯一能够检验与校验的是现实世界的碰撞,只有当他受到了自然规律的惩罚他才有可能有机会选择采取正确的思想来解释来应对。当然这里仅仅是有机会,因为他的头脑也可以采取另一种策略将一切的现实的惩罚与挫折当作神迹的考验与磨难从而更加坚定自己的信仰与信念。而更加困难的是,同样的正确的思想很多时候也是以概率规律来表现的,并非每一次的负反馈都是现实的正确反应,有时候需要长期的反复的应证才能揭开自然规律的真实面目。这个认识过程是如此的复杂与困难,而当前的人工智能希望透过几个简单的小范围的反复就证明他们的算法已经接近了真理的边缘是不现实的,很多人透过一生的试错都不一定能够积累出足够的数据来正确的解释世界,而我们能够期待人工智能能够这么快就取代人类吗?
  2. 问题是很多人也并不是很在乎需要不需要了解真相,因为掉进兔子洞里漫游仙境并不是所有人的喜好,生物体的本意只是存在与繁殖,其他都是副产品,就好像三极管线性放大仅仅适用于整个输入输出函数中一小段的取值范围,人类也不过是偶然的利用了生物体重多的副产品的一小部分发展出了所谓的意识以及思考,或许他们对于生存与繁殖是有利的不断的得到加强,但是对于大多数个体这个反馈不是绝对必须的,尤其在生存环境不是那么特别严酷的社会而言,有时候所谓的文明的进步在减少自然淘汰的比例的同时也降低了这种严酷筛选的必要性,很多个体可以自由自在的生活在兔子洞以外通过人工智能的输液管维持生命,而matrix创造的虚拟娱乐环境可以满足人脑的基本的功能需求,而人脑释放的各种激素类物质也许对于维持整个生命体体征是必要的。这就是matrix。
  3. 人类需要不需要正确的思想对于个体而言是有时候能与不能的问题,对于社会集团有时候是愿意不愿意的问题。社会集团是否在明知正确的理论方法规律而为了本集团的利益故意掩盖或者扭曲它,至少有意识的展示给本集团的敌对集团从而迷惑对手而得利呢?这在军事学上是有意义的。兵者,诡道也。对于对手的欺骗与迷惑贯穿于整个军事斗争的全部过程。然而在激烈交锋范围之外是否也需要如此的扭曲表达本集团内心承认的正确的思想与理论呢?也许是有必要的,这就是双重标准的起源,因为可以采用对自己有利的方式来解释。然而长此以往在本集团核心层以外依赖于核心层引领获得学习正确思想方法论的外围个体是否有被引入歧途的危险呢?主要取决于本社会是否需要所有的社会成员都需要采用正确的思考方法来认识周围世界。平常我们所说的愚民政策如果对于获得稳固统治是有利,也许就是采用双重标准的理由。然则,国无外患常亡,很多时候来自敌对集团的生存竞争威胁往往需要本集团每一个个体都成为生存竞争的胜利者,那么愚民政策也许就不是最好的选择了。由此证明双重标准并不是一个好的解决办法,它不一定会迷惑自己的对手反而误导了本社会的成员,是得不偿失的权宜之计。

一月九日 等待变化等待机会

  1. 之前我有注意到这个thread声明被误当作函数声明的问题,我以为lambda可以很好的防范它的出现,但是具体是怎么样子的呢? 比如对于这个lambda如果你还是像以前那样子把它当functor那样随手写下的话thread t3(HelloFunction());编译器是会报错的,对于错误的具体所指我不是很明了因为lambda的类型一般很难理解也基本上不希望程序员非常清楚,因为是编译器产生的也许也可以变化,总之程序员不应当依赖于它的类型来操作,这个相比于functor来说是一个进步因为限制了定义functor的随心所欲就能够防止一些莫名其妙的副作用。比如对于打印出来的这个lambda HelloFunction的类型我就是不理解 似乎你还是能够强制做到,但是这个已经最大限度的防止了错误发生了。比如我故意定义个错误的函数 这个函数是怎么调用的我都不理解因为参数要怎么传递呢?从这个t1的类型打印出来看也看不清楚std::thread (test298_namespace::HelloFunction::{lambda()#1} (*)())我知道它的返回值是thread,但是参数是一个函数指针,我只好这么调用它 总之,在thread初始化中使用lambda比直接的functor来的好。
  2. 练习了一下八皇后问题的思路,我只是想利用valarray的特性来检验结果,至于怎么搜索结果我还没有想过更好的办法,通常的bfs/dfs我感到不是很有效率,也许最简单的就是一个八个数字的数组要满足一些基本的条件,那么利用这个数组来解决,能不能用什么dangling link之类的算法来找呢?我记得knuth有一个这样子的类似的算法,据说是exact cover的一个搜索方法。但是具体怎么做我还没有想好。

一月十日 等待变化等待机会

  1. 八皇后不用递归只用一个一维的八个元素的数组来找出所有的结果看来是有些难度,我想的不清楚,还是放一下吧。
  2. 回到基本面我读了一下gcc里的编译指示,这个还是值得留心的,虽然是基本可是我似乎很模糊。通常为了省事我基本上都是用g++而不是用gcc,以前工作上遇到不同的第三方库也许是c语言的,导致makefile里要重新定义编译器的宏,这个就不是很清楚了。
    C++ source files conventionally use one of the suffixes ‘.C’, ‘.cc’, ‘.cpp’, ‘.CPP’, ‘.c++’, ‘.cp’, or ‘.cxx’; C++ header files often use ‘.hh’, ‘.hpp’, ‘.H’, or (for shared template code) ‘.tcc’; and preprocessed C++ files use the suffix ‘.ii’. GCC recognizes files with these names and compiles them as C++ programs even if you call the compiler the same way as for compiling C programs (usually with the name gcc).
    这个解释了我当时的一些疑惑,因为有的时候好像根本改变编译器宏的定义并不起作用,当时我并没有意识到有些库是c和c++代码混合的产物,尤其是我们自己包装了c的库的结果。 不过这个转折就难说了
    However, the use of gcc does not add the C++ library. g++ is a program that calls GCC and automatically specifies linking against the C++ library. It treats ‘.c’, ‘.h’ and ‘.i’ files as C++ source files instead of C source files unless -x is used. This program is also useful when precompiling a C header file with a ‘.h’ extension for use in C++ compilations. On many systems, g++ is also installed with the name c++.
  3. 这个控制c++的所谓dialet的选项堪称高级,我还是不可能短期内有什么理解的。
  4. 这个-fvisibility-inlines-hidden选项让我联想起了前几天做的关于inline实验。
    The effect of this is that GCC may, effectively, mark inline methods with __attribute__ ((visibility ("hidden"))) so that they do not appear in the export table of a DSO and do not require a PLT indirection when used within the DSO.
    我觉得我应该没有设定这个选项,似乎它是异曲同工吧?
  5. 这个选项也许是有用的-Wno-pessimizing-move因为它让我联想到我看的视频里关于std::move的常见错误。在可以利用copy elision的地方错误的添加了std::move导致了效率的下降。
  6. 而另一个相似的选项-Wno-redundant-move很像但是有很微妙的差别。具体是什么我肚子饿了看不下去了。
  7. 吃完饭我看以前的关于range的视频遇到一个小问题:究竟view的定义是什么?演讲者说普通的说法是a range without ownership但是这个不准确。这里其实挺复杂。我的一个简单的问题就是这个为什么编译不过 简单的解释传入右值不行,可是为什么不行?我查到views::all的定义如下 这个虽然是很复杂看不太懂,但是最起码它是接受rvalue的,不是吗?我于是看这个因为所谓的concept view就是而我的情况是enable_view失败了,而它的定义是derived_from 其实根本不需要考虑前面两个if,第一个说的是参数是一个view,第二个是参数是一个view的reference,我只能选择第三个subrange 从这里我觉得更加靠谱的是和borrowed_range里的要求:std::is_lvalue_reference_v因为我创建的subrange实际上用的是这个ctor 也就是说要满足borrowed_range这个concept,而它是要求is_lvalue_reference的 。至此似乎又回到了起点,难道不是吗?我们能够简单的说所有的view必须要lvalue才行?如果参数本身不是一个view或者它的reference的话。这个结论似乎很简单,但是我这一趟探求原因真的很不容易。至少我现在明白了所谓的views都是基于这个subrange来转化的,而这个subrange的要求还真的很复杂,其中sentinel是一个相当复杂的东西,我记得range的作者曾经提到过他的演示的calendar的程序的问答的时候提到sentinel不应该要求和begin iterator一样。这个很深奥的,因为无限的range怎么实现我一点也不敢去想。 如果你明确要求subrange的move ctor的参数是lvalue reference,那么何必要定义个move ctor ?为什么不直接delete掉呢?所以这里不是允许你传入rvalue reference,必须是lvalue reference那么这里的&&其实是universal reference的意思,也就是说我不能看到这个&&的参数类型就以为函数允许你传入rvalue。不过这个确实有些逻辑上的困难。

一月十一日 等待变化等待机会

  1. 找到了gcc文档关于symver的部分,就是说可以这么定义一个函数指明它的版本号。 __attribute__ ((__symver__ ("foo@VERS_1"))) int foo_v1 (void){} 这里的VERS_1和实际的代码怎么联系呢?
  2. 我们首先来看看实际的libstdc++.so里的version是什么样子的:objdump -p libstdc++.so有两个部分是我感兴趣的, 一个是
    Version definitions
    这个部分我觉得是对应elf里的
    .gnu.version_d This section holds the version symbol definitions, a table of ElfN_Verdef structures. This section is of type SHT_GNU_verdef. The attribute type used is SHF_ALLOC.
    elf的program header的另一部分是这个
    Version References
    应该是对应这个部分
    .gnu.version_r This section holds the version symbol needed elements, a table of ElfN_Verneed structures. This section is of type SHT_GNU_versym. The attribute type used is SHF_ALLOC.
    那么这个在编译过程是怎么做的呢?找到了原始的Makefile里引用一个版本定义的脚本“libstdc++-symbols.ver”这个脚本定义了复杂的类似于regex的形式,而在Makefile里用到了一个linker的命令:-Wl,--version-script=libstdc++-symbols.ver 从libstdc++-symbols.ver的复杂程度猜想不大可能是手工打造的,果然在源代码里是没有这个文件的,它是自动产生的。搜索makefile的配置确实很复杂,我放弃了,总之这个不是一个普通的自然的过程,需要配合其他的版本文件比如glibcxx的版本等等。
  3. 我似乎已经遗忘了我为什么要这么做,现在想起来了因为我使用gcc-10.2的编译器产生的可执行文件是依赖于新版的libstdc++.so的而这个新版的有版本限制导致我的可执行文件必须在运行期链接新版的libstdc++.so才行,这个是所谓的ABI的不兼容吗?symbol没有改变但是symbol的version改变应该也是一种不兼容吧?我看到llvm里面有大量的关于这个libstdc++的symbol的包装也许就是要解决这个问题吧?可是这个能够解决吗?你的c++的函数的行为改变了所以才有新的版本表示新版不兼容旧版,我们不能保证你的旧程序在新版下正常所以我设定一个版本号让旧版继续正常,而新版的编译结果对应新版的库。除非llvm想要自己改变什么但是不想增添新的版本号想要做到和gcc兼容吧?
  4. 理论说了一大堆实际做起来根本不是一回事,我做一个最简单的例子就不成功。定义一个最简单的函数__attribute__ ((__symver__ ("sayhello@VERS_1"))) int sayhello(){return 42;} 结果这个编译命令g++ -fPIC -shared lib.cpp -o libtest.so报出警告我的attribute无效,当然实际的DSO自然不会有version这个部分。查看linker的文档是很痛苦的,很难懂。更新我自己的本地版本吧。
  5. 再次观看金一南教授的令人振聋发聩的解答,保留一个拷贝看看十年以后是否应验!
    也许不用十年吧?
  6. 无意中看到DSO可以这么做,我虽然知道可以是脚本,但是没有想到可以group多个动态静态库于一身。这个是一个很好的解决方法。
  7. 关于llvm里的libcxx/libcxxabi我在编译了文档才搞明白了一些基本状况。首先编译文档只需要你额外安装python-sphinx,然后在libcxx的cmake命令里加上-DLLVM_ENABLE_SPHINX=ON -DLIBCXX_INCLUDE_DOCS=ON,然后make docs-libcxx-html 实际上这些都是写在readme里的,照着做而已。唯一想强调的是目前libcxx/libcxxabi还是支持单独编译的,仅仅依赖于llvm目录下的一些cmake文件。然后我读了一些关于为什么不继续libstdc++以及其他项目的原因才恍然大悟,它是一个瞄准取代libstdc++的项目,列举的一些原因有开源许可证的因素和不愿意如libstdc++仅仅捆绑gcc的因素等等。然后简单的google发现这个似乎是盖棺定论了
    libc++ is not 100% complete on GNU/Linux, and there's no real advantage to using it when libstdc++ is more complete. Also, if you want to link to any other libraries written in C++ they will almost certainly have been built with libstdc++ so you'll need to link with that too to use them.
    难道只有mac os才会有人要用它吗?查看官方网页目前c++14是完全支持的,c++17还在进行中。于此对应目前gcc/libstdc++对于c++17的支持是"almost full support"我花了一点时间比较两者对于c++各个标准的实现情况似乎差距不是那么的大,对于以上说法里其他库链接的是libstdc++而你必须再次链接我觉得可能是最大的原因。我一开始想libcxx有包含所有的symbol不就行了吗?转念一想不行因为链接DSO是写在elf里的无法改变,这个不是symbol级别的问题。所以,作为llvm的封闭系统苹果最喜欢这么另搞一套让你不兼容了。苹果是统一大环境的最大的公敌!

一月十二日 等待变化等待机会

  1. 我现在的学习一种方式就是温故而知新,阅读以前的笔记总有惊人的发现。可是这个问题是什么呢?
    ifstream in(__FILE__);
    string str(istream_iterator<char>(in), istream_iterator<char>());
    
    编译器报出的错误是有些模糊的: 这个是什么意思呢?进一步的debug显示我们的str变量的类型不是我们期待的string 我打印了str的type,它是这样子的: 这个庞然大物是什么呢?它说明了str是这样一个函数类型,它的返回值是string,它需要两个参数第一个是istream_iterator,第二个搞笑的是一个函数指针,这个函数指针它返回值的类型是istream_iterator,它不需要任何参数!怎么会这样子呢?其实和之前遇到的问题是相似的,都是被编译器错误的解读为函数声明的。怎么会这样子的呢?
    1. 首先这个是另一个著名的问题,我们第一个参数istream_iterator<char>(in)被解读成了什么?in被当作了istream_iterator的变量,这个在str整个被编译器当作函数之后它的型参这种临时变量是不会被编译器计较的,因为我们没有开放-Wshadow-compatible-local的警告,对于参数的重复声明的警告是不会有的。
    2. 看明白了第一个参数第二个参数istream_iterator<char>()就很好理解了,很关键的是()都会被当作函数类型的标志,所以整个第二个参数是一个函数指针的类型来解读而不是我们期待的istream_iterator的ctor,这个就是c++为什么要引入{}初始化,就是因为ctor在很多时候更确切的说是叫做conversion operator,所谓的constructor实际上是一种人为的想像。
    3. string原本是我们期待的str的类型,可是却变成了函数str的返回类型!
    这类问题我其实不止一次的遇到了,最好的解决办法其实不是简单的在第一个参数加一个括号()比如
    
    string str((istream_iterator<char>(in)), istream_iterator<char>());
    
    而是使用{}比如
    
    string str(istream_iterator<char>{in}, istream_iterator<char>());
    
  2. 关于gcc的帮助,使用的maillist,而搜索是一个很好的办法。
  3. 再重复一遍,作为了解编译器配置的命令是这样子的:g++ -std=c++20 -dM -E -x c++ - < /dev/null这里根据c++的dialect的不同而有不同的定义,gcc里面鼓励大家不要用宏来条件编程,而是使用变量值的条件来转向。
  4. gcc里面的libcpp也许就是我们需要的,我看到c++语法的parser而且似乎可以独立编译,当然需要gcc的configure配置config和include目录,这个是至少。只不过我还没有看到template是怎么处理的。此外,看到一些不明白的东西,什么NFKC 这里我也学到了一个时髦的词汇:TLDR===>Tool Long; Didn't Read
  5. libcpp不包含template的支持,那么libcc1是否包含呢?我看不清楚,然后发现似乎这个plugin的用途是和gdb紧密结合提供很高端的在debug时期编译代码的功能的,即便这个不是主要目的但是也是它的一个功能,所以,我猜想它只是一个Plugin并不是parser的一部分吧?
  6. 我发现在gcc/gcc目录下有各种语言的支持,其中cp应该就是c++的支持,我只看这个目录下的代码可能就足够了。
  7. 在代码里看到#line 23 "cfns.gperf" 看来这个是一个固定log输出的一个好办法
  8. 关于运行gcc测试的命令有些混乱。文档上说的可能不对吧?我发现这个好像可以必须在编译完成的gcc目录下运行make check-g++

一月十三日 等待变化等待机会

  1. 对于ranges我感觉有些困难,不是在algorithm部分因为这个部分第一在boost接触过了,第二虽然众多其实原理很简单,基本上大部分都是相似的,只有极少数需要特别处理帮助返回值可以作为一个range在pipeline里操作。我感觉困难的反而是view和它的iterator部分,因为这个和span在某种程度上很微妙,我始终把握不住突破的地方。感觉串不起来。实际上它的iterator是它的精华之所在,这部分却又是大多数人的弱点,只有真正精通iterator的人才敢说自己掌握了c++标准库的高手。
  2. 再一次阅读再一次领悟,首先view的concept是什么?它必须是range,而range的要求其实很简单就是要实现begin/end不过我对于cppreference.com有些遗憾就是它也没有办法完全更新因为c++正在蓬勃的发展演化中,不要说代码实现就是文档标准也很难保持正确。这里的experimental我找不到。,注意很微妙的是这个要么是类的成员函数要么必须是类所在的namespace里面实现,我很确定觉得后一点是对的,只是还没有在代码里找到。其他就是enable_view要从ranges::view_base继承而来从这里能够体现出concept有多么伟大,如果没有它模板编程就是一个个的噩梦。
  3. 我看到这么一行代码让我兴奋不已 因为这个是典型的Curiously Recurring Template Pattern,它强制要求了_Derived必须实现view_interface的接口方法,否则你编译就不行。
  4. concept和requires是太高级的东西了,这个是library developer的本领,我只能看看。
  5. 我怀疑我的eclipse的mark text occurance不工作是因为模板不认识的缘故,只好安装glance这个插件,然后ctl+alt+F果然很棒!不过它和原生的不能比因为它不理解上下文的语义只是机械的字符比对,不过对于模板参数这类复杂的东西这个是最好的了。
  6. 看了几乎一个下午才有点入门,就是所谓的view是非常的复杂的实现,至少我是这么认为的,你要实现view,必须要使用CRTP实现view_interface的接口。这里有一个重要的概念:一个view一定是一个range,但是range不一定是view。因为view是一个很轻量级的range。
    The view concept specifies the requirements of a range type that has constant time copy, move, and assignment operations (e.g. a pair of iterators, or a generator Range that creates its elements on-demand. Notably, the standard library containers are ranges, but not views)
    这段话的信息量很多的,view是一个range的特殊形式要求constant time的copy/move/assignment,这个实现起来有两个途径,或者是用一对iterator或者是create on demand,或者lazy的方式创建。相比之下range只有很简单的要求,就是支持begin/end操作就可以了。那么view的实现是否仅仅是继承自view_interface就能自动实现吗?我以为不然,这个取决于两种途径,如果是两个iterator的形式,自然的就是满足了constant time的copy/move/assignment。但是如果是一个什么generator的形式呢?这个我还没有看到例子,不敢说怎么实现。因为大部分的view都是用range的begin/end这两个iterator。所以成本很低的就实现了。注意到view_interface要实现这么些个方法我一开始没有找到这个view_interface在cppreference的链接。
  7. 我现在有一个认识就是range实现的那些algorithm的确不是很难以至于我轻视了view的复杂性,因为我始终分不清楚range和view的区别,看到一个简单转换就能够把stl之前的所有的algorithm都使用range的确没有什么太高深的地方几乎人人都能做,可是view就是两码事了,我被误导了,因为是两个完全迥异的库,尤其这些神奇的adaptors,他们是一片富矿。
  8. 有趣的是人们常常有一个习惯就是喜欢把view和span来比较,至少我是这样的。而现在看来两者在某种特定情形下有很像的效果,但是span更像一个数学概念的人为投射制作,只有当view的inifinite boundary的时候两者有异曲同工之效。

一月十四日 等待变化等待机会

  1. 故事的起源是我看到这个模板就惊呆了,因为我的头脑还没有更新c++20的一些新特性 这里的__Referenceable怎么就取代了我们熟悉的typename/class的位置呢? 我看到这里我根本就不认识什么是template了,因为这个template template parameter远远的超过了我的理解。
  2. 对于模板的模板的应用是一个老话题了,我想这个大侠指出的variadic template我一直以来总是把它和template parameter pack混为一潭,究竟是什么我也不确定,但是这里的例子还是要收藏一下想一想 我模糊记得以前总是对于container class的众多的模板参数感到头疼,因为stl的容器需要的模板参数除了元素的类型之外的参数有时候还是有所不同的,以前似乎遇到过固定模板参数个数导致不能适应所有容器类型的问题?也许这个variadic templates就是解决的办法? 当然现在的generic lambda除了不能全局重载操作符写起来似乎更简单。我想起来大师曾经说的话,现在的c++看上去比以前简单了,实际上是更复杂了,因为对于用户使用者感到顺理成章的简单的实现的背后是比从前几何级数的复杂度的提升。
  3. 看到c++20里面大多数东西我是不熟悉的,比如这个看似等闲的CTAD初始化有多少令人不安的实现呢?我以前以为自己明白了,可是这里又是有些迷惑到底要呼叫哪一个ctor呢? 现在看起来是trivial的,因为pa2实际上是aggregate initializer,它只能找A::A(int, int),可是对于pa5的结果我不奇怪是A::A(int),问题是我有没有办法使用A::A(int, int)呢?我发现我的记忆力下降的太多了,这种刚上学的问题我都忘了:auto pa6=new A[1]{{1,2}};还是那个字aggregate!
  4. 我每天都有学习new的东西:这个语法令我震惊因为我从来没有这么用过
  5. iterator library对于我来说似乎还是有些高攀不起,我还是先解决view吧。
  6. 经常在头条上看什么标题某某大牛公司的大师一年精通c++,实在是让人感到不适,转念一想这些人都是靠卖书为生的,内容都是学习c++必须读几本书,其中任何一本能够几个月读通都是非常神奇的,更不要说是从零基础开始,简直是天方夜谭。
  7. span可以这么简单的理解就是说它是一个真正的轻量级的“影像”,你有一个庞大的容器或者数组,而他们的访问是有规律的比如实现或者支持operator[],可是拷贝来拷贝去是一个巨大的浪费,那么使用引用也许本来是可以的,可是如果我需要一个subrange之类的,使用引用就不行了,那么span只需要你给我它的起始指针或者是可扩展的大小或者是结束的sentinel,那么这么一个轻量级的结构就可以传递使用。不过这其中的危险是不言而喻的,我实在是想不出来这个做法的经典应用是什么?比如我们常常犯这样子的越界的错误要怎么防止呢? span还不如使用view这样子不让你任意指定长度的来的安全。我的记忆力又打脸, string_view就是一个span,我实在是想不出这个span有什么意义,c程序员直接使用指针也许比这个span还要安全一些,因为我知道它是危险的,我不会姿意妄为的乱指一通,可是你现在给我一个类似数组的东西,而它的长度是你瞎编的这不是害我吗?
  8. 我没有真的理解view,以为只要我实现了view_interface就行了,实际上它是在range的基础上的。也就是说这么简单的用任意一个类是糊弄不过去的, 但是让我感到以外的是concept居然没有报错。而我自己随便的实现了一下那几个接口的方法,他们这些都是要求实现类满足range::begin/end之类的方法的,可是居然没有报错?实际上concept/requires是有起作用的,只不过他们的要求是我的ChronoView要是一个range,而对于range的要求是那么的低只要你实现了begin/end的成员函数就可以的。而其他的众多的检查都是围绕着这个。比如iterator_t的这个就是不大起作用: 可是这个本应该检查iterator的仅仅是看看有没有实现begin,因为 这里首先看看我们的类是不是数组is_array,如果不是的话就看有没有一个begin的成员函数,最后是在不行了才尝试ranges::begin这个函数能否有重载参数类型包含我们的类的。所以,一切的一切就是强调我的类是不是一个range。其他都不重要。 所以,如果你使用一个range来创建一个view是顺理成章的,因为这个CRTP是一个基于模板的继承,只要你的代码没有用到view_interface的其他方法如front/back/size/empty/data等等都不用实现。但是begin/end是一定要实现的,因为这个是range的基本要求。
  9. 原来TL;DR在论文里是abstract的意思。

一月十五日 等待变化等待机会

  1. 找了好久才找到这篇论文原来这个是c++17的一个新的feature,就是好像boost/lambda的placeholder的用途一样可以达到类似std::tie(std::ignored,...)的效果,可是这个google起来真的很不容易,因为大家都不知道你要问什么!我要问的其实很简单就是这里的underscore(_)是怎么定义的?以后要记得这个名词叫做structured binding 论文里还有更多的例子,比如函数的不需要用的参数,这个很有用是在于有的有default值的时候需要填补一个空缺。而模板的不需要使用的参数就更重要的,因为很多时候的spectialzation等等很细微的差别就在于参数的存在与否。
  2. 可是就在我为找到这篇论文高兴的时候发现gcc并没有实现这篇论文的影子,我甚至连它是否被委员会接纳都不确定,这个是标准吗?我又google到了这篇论文它似乎是在支持这个structured binding,这个简直就是一场永无止歇的争论让我无所适从,究竟_是否是一个合法的变量名呢?这里的答案是值得铭记的
    A name (identifier) consists of a sequence of letters and digits. The first character must be a letter. The underscore character,_, is considered a letter.
    至此我才明白我又闹了一个笑话,这个根本就不是什么c++17的新feature,这个是从大师定义c/c++语言的第一天就存在的!这个让我联想起在boost的lambda广泛的使用underscore的场景,这个压根儿就是一个一直存在的现象,因为_的独一无二的命名特性,它可以作为一个理想的局部变量的命名,因为每个人都会担心自己的变量名被人重复使用于是任何正常的程序员都不会在广大范围内命名自己的变量名为_,因为太容易重复了,但是并不等于你不可以!这就是我惊讶莫名的原因,这个叫做少见多怪!所以我现在才理解为什么有上述论文希望禁止它作为变量名,也才明白为什么那篇论文没有被接受,因为对于任何改变_的地位特性的做法可能都是某种灾难,因为谁知道有多少代码依赖于它?
  3. 早上三点起来花了两三个小时就弄明白了一个underscore!我还试图在gcc的parser里面去找相关的代码,然后就明白那么多人对于gcc的批评所言不虚,首先没有文档,其次我连lex的symbol的定义都找不到,很显然的有些代码是某种自动机制产生的吧?而且gcc/cp和libcpp以及libstdc++v3/libcc1等等的关系是怎么样子呢?似乎gcc的c++部分语法并不是都在一个地方完成的?
  4. 什么叫做举步维艰,我似乎每前进一步就遇到一个个的地雷,很多时候都不是c++的新feature而是基础的语法而我似乎是遗漏的,一方面是有些偏僻高深平常不大用,另一方面是我从来没有认认真真的完整的读过任何一本c++的经典著作。面对几百页的大作,很多时候我看没几天就着急了。读书要一字一句的阅读太慢了,可是这种书又不是小说可以看了后面忘了前面,这个真难啊。
  5. 对于structured binding这个数组是让人感到陌生的。这里需要注意的就是针对数组必须和数组长度一致才行,这个似乎是废话。
  6. 很快打脸,因为使用subrange一定要小心,切不可使用原始的指针,我就犯了如下的错误 第一个问题是x,y是什么?我一开始以为是数组的元素,其实是指针,因为这里的ary是裸指针根本没有数组的概念,subrange就是认为你给了我一个int*,而它的边界就是
    reinterpret_cast<long long>(std::addressof(ary))+4*sizeof(std::ptrdiff_t)
    我这里又犯了一个大错误,就是addressof返回的是指针还是地址?我一直以为是地址,其实是指针,结果addressof(ary)+4是使用指针加法!名字害死人!望文生义害死人!这里要非常小心因为不要以为它是addressof(ary[4]),因为static_assert(sizeof(int)==4);。明白了第一个问题就不难理解第二个问题了,为什么这个做法会过界 那么正确的做法是什么呢?要使用std::begin!
  7. 对于structured binding绑定类成员变量我开始担心能够访问私有成员,但是显然的委员会早就想到了,这个是编译不过的。
  8. 看到大师的帖子,只能说不明觉厉。
  9. 我始终也不是很清楚为什么ranges::views::iota不能够使用任意的类,究竟怎么样才能算是支持weakly_incrementable呢?我一直卡在iterator_t没有定义difference_type的问题上,而我脑子想不清楚究竟是什么要做什么?iterator是很不容易的一个,看起来容易,做起来很不容易。比如为什么这个是成立的 这个也是可以的,那么是否我需要制作一个针对我的类的iterator呢? 理解其中的concept非常的,我已经把编译器开关加到了-fconcepts-diagnostics-depth=3可是依然看不明白。
  10. 散步后静下心来想了一下,看了一下views::all发现它根本就是一个lambda完全不是我所设想的实现什么view_interface之类的做法。

一月十六日 等待变化等待机会

  1. projected是听说过的神奇传说,需要以后再学习。
  2. ranges的这些adaptors各有各自的特点。
  3. 意外的发现cppreference原来有offline的archive版本,甚至在ubuntu上有官方发行版,真的是宅心仁厚啊!这个一定要点赞!!!不过这个archive只有更新到c++17,这个是可以理解的。
  4. 一个上午就看懂了几个view的一些代码,现在肚子饿得不得了了。
  5. 我曾经一度想给敬爱的cppreference加一些缺失的网页,结果还是能力不足害怕误人子弟就放弃了,等我再掌握一些吧。
  6. 我自以为明白什么是partial specialization了,似乎这个是任何刚开始学习c++的小学生的问题,可是实际上一上阵我就掉链子,不动手练习永远不知道自己其实真的不会。连PS都做不好更不要说SFINAE了, 更何况仅仅是soupçon的SFINAE。 我后面的讲座实在是看不下去了,又饿又头晕。

一月十七日 等待变化等待机会

  1. 看过了ranges adaptors回过头来看什么是range factories

一月二十日 等待变化等待机会

  1. 之前看到这个关于c++20的预告对于这个stringstream的这个新方法view还是期待了一下的,没想到现在还没有实现,也难怪我对于这个方法的期待来自于自己犯的这种低级错误的代价,但是这个错误明显的是生生创造出来的,我的意思是几乎不会有这么白痴的写法,这个似乎纯粹就是“找茬”式的bug,为什么需要这么使用呢?把这个作为引入view方法的理由太牵强了吧?

    DO NOT DO THIS!

    不知道未来的view是不是能够解决这个问题?
  2. 对于string literal,我一直记不住因为几乎没有用过,包括u8我唯一的经验就是cout之类的不接受这个类型,这个目的是强迫程序员自己重载吗?而使用literal的动力还不如这个例子来的更加有说服力,就是不如直接使用string_view的literal
    prefix lettertypesize of characterremark
    LWide string literal. 4"...n code units of the execution wide encoding"这个是不是说不同的code point有不同的wide encoding?也就是说size是不确定的?
    u8UTF-8 encoded string literal.1其实和char没有区别,但是类型不同强制要求你去针对性的处理?
    uUTF-16 encoded string literal.2是否有人真的使用它呢?我对于它的原理还是不清楚
    UUTF-32 encoded string literal.4同上
    RRaw string literal1?它实际上可以和以上组合,所以它的长度依赖于以上的类型,当然默认是char吧
  3. 我有时候想要找一些有说服力的例子来强调view的on demand或者说lazy是很强大,可是总是感觉力不从心,感觉不是很convincing。比如使用filter_view来实现一个stable_partition,因为我发现普通partition虽然不是stable的但是效率更高一些,因为用view意味着必然要2*O(N) 可以看到结果是类似的
  4. 我以前觉得source_location实在是太trivial了不明白有什么必要使用它,现在通过讲解才理解关键是它的current是consteval就是说以前我们使用macro才能达到的效果现在它可以做到,比如我以前总是把一个错误处理爬出异常写log写成一个小函数,在try-catch里调用,但是函数里的__FILE__,__LINE__最后变成了这个小函数而不是catch呼叫的地方让人很郁闷,只好使用macro代替函数。现在使用这个source_location就不同了。只要呼叫
  5. 看到另一个很好的consteval的应用的例子是这样的,就是定义一个constexpr的函数并不能检查编译期的非常数的问题,比如我们比较这样子两个函数 他们的定义分别是constexpr和consteval,他们的区别究竟是什么呢?对于如下的常量他们的检查是不一样的:consteval对于非常量inch6在编译期是可以被发现的 而constexpr则不能 这里给出的编译错误可以说非常的清晰
  6. 无法忍受eclipse for c++的缺陷决定尝试visual studio code结果发现很惊人,我现在才意识到这个实际上是最好的选择之一,也许是最popular的。那么它内部利用的gnu global也许是最棒的之一吧?这里有一个比较,还不是很确定比较的结果。这里是一个关于使用gnu global和vim结合的blog,以后再读吧。
  7. vscode里的gtags显然好一些,但是遇到像是ranges::copy这一类的symbol它也是无能为力的,即便用肉眼搜索也是很困难的,我怀疑还是语法的问题,我费了九牛二虎之力才找到他的定义:在bits/ranges_algobase.h定义了一个functor结构__copy_fn
    inline constexpr __copy_fn copy{};
    普通的parser对于一个函数首先去找我们熟悉的c函数,对于这种functor可能是放在比较靠后吧? 这个可能是一个难题,除非gtags里的parser能够与时俱进的话,我猜想这个比较困难,比如eclipse for c++还不认识consteval这个关键字,vscode也是一样,这个是太新的语法了。
  8. 我看了一下gnu global的libparser下的cpp的实现似乎是很简单的,作者也承认是非常古老的不过早已开放接口兼容其他的实现。因为基本上使用简单的类似于regex的方式估计对付不了c++的高级的语法吧?我觉得概念不是很清楚,问题不是几个keyword的更新的问题,而是新的语法是semantic的问题,怎么理解新的模板alias能够定义新的类型这类语义,parser即便解决了symbol能否想gtags那样给你来个symbol的比对列表一长串的match?这个是否就是你需要的呢?gcc里的libcpp也许要好很多,但是我估计没有人能够使用因为无法剥离,太困难了。

一月二十一日 等待变化等待机会

  1. 这个GNU global的manual值得阅读。我很快决定放弃了,因为我觉得这个太简陋了,没有什么特别难的,也许我使用regex也能做的很好,仅仅修改一个文件后缀名我就折腾不出来决定放弃了。
  2. 我觉得这个bison/lex值得看看。
  3. 但是首先看到这个c++ in BNF让我高兴了许多。
  4. 这个标准委员会c++文档可以在线查询还是不错的。仔细一看发现是c++98的1997年预览版!
  5. 评论区说的好:为什么要花钱从ISO网站下载标准呢?这里有一个资源站,我实验一下。这里似乎是源头 编译了一份c++20的working draft。这样要感谢这些人的奉献。
  6. 还是复习一下regex吧,这个是基本的语法保存一个版本吧。
  7. 没想到过了一年我在同样的问题又犯了同样的错误!istream_iterator不是一个好办法,它会过滤掉所有的whiltespace。那么这个是不是就可以呢? 所以,这个实在是比较的微妙,istream_iterator和istreambuf_iterator是不同的,只有使用后者才能保留其中的所有字符。
  8. 这个是读取一个html文件并打印所有链接的代码:

一月二十二日 等待变化等待机会

  1. 这里又让我学习到了原来An Alternative to Laziness是直接使用negated character class
    In this case, there is a better option than making the plus lazy. We can use a greedy plus and a negated character class. The reason why this is better is because of the backtracking. When using the lazy plus, the engine has to backtrack for each character that it is trying to match.
  2. 这个关于注释的regex还真的不是看上去那么容易。我第一次就掉进了字符串的陷阱。这个自动机要好好看看
  3. 这个大侠给出了c-style comments的很好的循序渐进的过程值得学习。不过他并没有回答在字符串内的问题。作者指引我们使用工具去做这个事情。我搜索到这个博客让我很怀疑之前的那个是抄袭这个大侠了,当然这个想法有些龌龊。
  4. 这里似乎是一个非常完全的解决方案,还没有看仔细,但是他已经指出了std::regex可能不适合这项工作,看来boost::regex才行?

    TL;DR

    我对于大侠的帖子佩服不已,其中很多部分我还理解不了,保留一个版本我google了一下这个帖子还是被不少地方转载了,但是作者是否是原创我不是很肯定,但是这有什么关系呢?这个帖子的水平是很高的。
    我查了一下boost::regex的文档关于backtrack control verb

    Backtracking Control Verbs

    This library has partial support for Perl's backtracking control verbs, in particular (*MARK) is not supported. There may also be detail differences in behaviour between this library and Perl, not least because Perl's behaviour is rather under-documented and often somewhat random in how it behaves in practice. The verbs supported are:

    • (*PRUNE) Has no effect unless backtracked onto, in which case all the backtracking information prior to this point is discarded.
    • (*SKIP) Behaves the same as (*PRUNE) except that it is assumed that no match can possibly occur prior to the current point in the string being searched. This can be used to optimize searches by skipping over chunks of text that have already been determined can not form a match.
    • (*THEN) Has no effect unless backtracked onto, in which case all subsequent alternatives in a group of alternations are discarded.
    • (*COMMIT) Has no effect unless backtracked onto, in which case all subsequent matching/searching attempts are abandoned.
    • (*FAIL) Causes the match to fail unconditionally at this point, can be used to force the engine to backtrack.
    • (*ACCEPT) Causes the pattern to be considered matched at the current point. Any half-open sub-expressions are closed at the current point.

    Lexing a source file is a good job for regexes. But for such a task, let's use a better regex engine than std::regex. Let's use PCRE (or boost::regex) at first. At the end of this post, I'll show what you can do with a less feature-packed engine.

    We only need to do partial lexing, ignoring all unrecognized tokens that won't affect string literals. What we need to handle is:

    • Singleline comments
    • Multiline comments
    • Character literals
    • String literals

    We'll be using the extended (x) option, which ignores whitespace in the pattern.

    Comments

    Here's what [lex.comment] says:

    The characters /* start a comment, which terminates with the characters */. These comments do not nest. The characters // start a comment, which terminates immediately before the next new-line character. If there is a form-feed or a vertical-tab character in such a comment, only white-space characters shall appear between it and the new-line that terminates the comment; no diagnostic is required. [ Note: The comment characters //, /*, and */ have no special meaning within a // comment and are treated just like other characters. Similarly, the comment characters // and /* have no special meaning within a /* comment. — end note ]

    # singleline comment
    // .* (*SKIP)(*FAIL)
    
    # multiline comment
    | /\* (?s: .*? ) \*/ (*SKIP)(*FAIL)
    

    Easy peasy. If you match anything there, just (*SKIP)(*FAIL) - meaning that you throw away the match. The (?s: .*? ) applies the s (singleline) modifier to the . metacharacter, meaning it's allowed to match newlines.

    Character literals

    Here's the grammar from [lex.ccon]:

     character-literal:  
        encoding-prefix(opt) ’ c-char-sequence ’
      encoding-prefix:
        one of u8 u U L
      c-char-sequence:
        c-char
        c-char-sequence c-char
      c-char:
        any member of the source character set except the single-quote ’, backslash \, or new-line character
        escape-sequence
        universal-character-name
      escape-sequence:
        simple-escape-sequence
        octal-escape-sequence
        hexadecimal-escape-sequence
      simple-escape-sequence: one of \’ \" \? \\ \a \b \f \n \r \t \v
      octal-escape-sequence:
        \ octal-digit
        \ octal-digit octal-digit
        \ octal-digit octal-digit octal-digit
      hexadecimal-escape-sequence:
        \x hexadecimal-digit
        hexadecimal-escape-sequence hexadecimal-digit
    

    Let's define a few things first, which we'll need later on:

    (?(DEFINE)
      (?<prefix> (?:u8?|U|L)? )
      (?<escape> \\ (?:
        ['"?\\abfnrtv]         # simple escape
        | [0-7]{1,3}           # octal escape
        | x [0-9a-fA-F]{1,2}   # hex escape
        | u [0-9a-fA-F]{4}     # universal character name
        | U [0-9a-fA-F]{8}     # universal character name
      ))
    )
    
    • prefix is defined as an optional u8, u, U or L
    • escape is defined as per the standard, except that I've merged universal-character-name into it for the sake of simplicity

    Once we have these, a character literal is pretty simple:

    (?&prefix) ' (?> (?&escape) | [^'\\\r\n]+ )+ ' (*SKIP)(*FAIL)
    

    We throw it away with (*SKIP)(*FAIL)

    Simple strings

    They're defined in almost the same way as character literals. Here's a part of [lex.string]:

      string-literal:
        encoding-prefix(opt) " s-char-sequence(opt) "
        encoding-prefix(opt) R raw-string
      s-char-sequence:
        s-char
        s-char-sequence s-char
      s-char:
        any member of the source character set except the double-quote ", backslash \, or new-line character
        escape-sequence
        universal-character-name
    

    This will mirror the character literals:

    (?&prefix) " (?> (?&escape) | [^"\\\r\n]+ )* "
    

    The differences are:

    • The character sequence is optional this time (* instead of +)
    • The double quote is disallowed when unescaped instead of the single quote
    • We actually don't throw it away :)

    Raw strings

    Here's the raw string part:

      raw-string:
        " d-char-sequence(opt) ( r-char-sequence(opt) ) d-char-sequence(opt) "
      r-char-sequence:
        r-char
        r-char-sequence r-char
      r-char:
        any member of the source character set, except a right parenthesis )
        followed by the initial d-char-sequence (which may be empty) followed by a double quote ".
      d-char-sequence:
        d-char
        d-char-sequence d-char
      d-char:
        any member of the basic source character set except:
        space, the left parenthesis (, the right parenthesis ), the backslash \,
        and the control characters representing horizontal tab,
        vertical tab, form feed, and newline.
    

    The regex for this is:

    (?&prefix) R " (?<delimiter>[^ ()\\\t\x0B\r\n]*) \( (?s:.*?) \) \k<delimiter> "
    
    • [^ ()\\\t\x0B\r\n]* is the set of characters that are allowed in delimiters (d-char)
    • \k<delimiter> refers to the previously matched delimiter

    The full pattern

    The full pattern is:

    (?(DEFINE)
      (?<prefix> (?:u8?|U|L)? )
      (?<escape> \\ (?:
        ['"?\\abfnrtv]         # simple escape
        | [0-7]{1,3}           # octal escape
        | x [0-9a-fA-F]{1,2}   # hex escape
        | u [0-9a-fA-F]{4}     # universal character name
        | U [0-9a-fA-F]{8}     # universal character name
      ))
    )
    
    # singleline comment
    // .* (*SKIP)(*FAIL)
    
    # multiline comment
    | /\* (?s: .*? ) \*/ (*SKIP)(*FAIL)
    
    # character literal
    | (?&prefix) ' (?> (?&escape) | [^'\\\r\n]+ )+ ' (*SKIP)(*FAIL)
    
    # standard string
    | (?&prefix) " (?> (?&escape) | [^"\\\r\n]+ )* "
    
    # raw string
    | (?&prefix) R " (?<delimiter>[^ ()\\\t\x0B\r\n]*) \( (?s:.*?) \) \k<delimiter> "
    

    See the demo here.

    boost::regex

    Here's a simple demo program using boost::regex:

    #include <string>
    #include <iostream>
    #include <boost/regex.hpp>
    
    static void test()
    {
        boost::regex re(R"regex(
            (?(DEFINE)
              (?<prefix> (?:u8?|U|L) )
              (?<escape> \\ (?:
                ['"?\\abfnrtv]         # simple escape
                | [0-7]{1,3}           # octal escape
                | x [0-9a-fA-F]{1,2}   # hex escape
                | u [0-9a-fA-F]{4}     # universal character name
                | U [0-9a-fA-F]{8}     # universal character name
              ))
            )
    
            # singleline comment
            // .* (*SKIP)(*FAIL)
    
            # multiline comment
            | /\* (?s: .*? ) \*/ (*SKIP)(*FAIL)
    
            # character literal
            | (?&prefix)? ' (?> (?&escape) | [^'\\\r\n]+ )+ ' (*SKIP)(*FAIL)
    
            # standard string
            | (?&prefix)? " (?> (?&escape) | [^"\\\r\n]+ )* "
    
            # raw string
            | (?&prefix)? R " (?<delimiter>[^ ()\\\t\x0B\r\n]*) \( (?s:.*?) \) \k<delimiter> "
        )regex", boost::regex::perl | boost::regex::no_mod_s | boost::regex::mod_x | boost::regex::optimize);
    
        std::string subject(R"subject(
    std::cout << L"hello" << " world";
    std::cout << "He said: \"bananas\"" << "...";
    std::cout << "";
    std::cout << "\x12\23\x34";
    std::cout << u8R"hello(this"is\a\""""single\\(valid)"
    raw string literal)hello";
    
    "" // empty string
    '"' // character literal
    
    // this is "a string literal" in a comment
    /* this is
       "also inside"
       //a comment */
    
    // and this /*
    "is not in a comment"
    // */
    
    "this is a /* string */ with nested // comments"
        )subject");
    
        std::cout << boost::regex_replace(subject, re, "String\\($&\\)", boost::format_all) << std::endl;
    }
    
    int main(int argc, char **argv)
    {
        try
        {
            test();
        }
        catch(std::exception ex)
        {
            std::cerr << ex.what() << std::endl;
        }
    
        return 0;
    }
    

    (I left syntax highlighting disabled because it goes nuts on this code)

    For some reason, I had to take the ? quantifier out of prefix (change (?<prefix> (?:u8?|U|L)? ) to (?<prefix> (?:u8?|U|L) ) and (?&prefix) to (?&prefix)?) to make the pattern work. I believe it's a bug in boost::regex, as both PCRE and Perl work just fine with the original pattern.

    What if we don't have a fancy regex engine at hand?

    Note that while this pattern technically uses recursion, it never nests recursive calls. Recursion could be avoided by inlining the relevant reusable parts into the main pattern.

    A couple of other constructs can be avoided at the price of reduced performance. We can safely replace the atomic groups (?>...) with normal groups (?:...) if we don't nest quantifiers in order to avoid catastrophic backtracking.

    We can also avoid (*SKIP)(*FAIL) if we add one line of logic into the replacement function: All the alternatives to skip are grouped in a capturing group. If the capturing group matched, just ignore the match. If not, then it's a string literal.

    All of this means we can implement this in JavaScript, which has one of the simplest regex engines you can find, at the price of breaking the DRY rule and making the pattern illegible. The regex becomes this monstrosity once converted:

    (\/\/.*|\/\*[\s\S]*?\*\/|(?:u8?|U|L)?'(?:\\(?:['"?\\abfnrtv]|[0-7]{1,3}|x[0-9a-fA-F]{1,2}|u[0-9a-fA-F]{4}|U[0-9a-fA-F]{8})|[^'\\\r\n])+')|(?:u8?|U|L)?"(?:\\(?:['"?\\abfnrtv]|[0-7]{1,3}|x[0-9a-fA-F]{1,2}|u[0-9a-fA-F]{4}|U[0-9a-fA-F]{8})|[^"\\\r\n])*"|(?:u8?|U|L)?R"([^ ()\\\t\x0B\r\n]*)\([\s\S]*?\)\2"
    

    And here's an interactive demo you can play with:

    function run() {
        var re = /(\/\/.*|\/\*[\s\S]*?\*\/|(?:u8?|U|L)?'(?:\\(?:['"?\\abfnrtv]|[0-7]{1,3}|x[0-9a-fA-F]{1,2}|u[0-9a-fA-F]{4}|U[0-9a-fA-F]{8})|[^'\\\r\n])+')|(?:u8?|U|L)?"(?:\\(?:['"?\\abfnrtv]|[0-7]{1,3}|x[0-9a-fA-F]{1,2}|u[0-9a-fA-F]{4}|U[0-9a-fA-F]{8})|[^"\\\r\n])*"|(?:u8?|U|L)?R"([^ ()\\\t\x0B\r\n]*)\([\s\S]*?\)\2"/g;
        
        var input = document.getElementById("input").value;
        var output = input.replace(re, function(m, ignore) {
            return ignore ? m : "String(" + m + ")";
        });
        document.getElementById("output").innerText = output;
    }
    
    document.getElementById("input").addEventListener("input", run);
    run();
    <h2>Input:</h2>
    <textarea id="input" style="width: 100%; height: 50px;">
    std::cout << L"hello" << " world";
    std::cout << "He said: \"bananas\"" << "...";
    std::cout << "";
    std::cout << "\x12\23\x34";
    std::cout << u8R"hello(this"is\a\""""single\\(valid)"
    raw string literal)hello";
    
    "" // empty string
    '"' // character literal
    
    // this is "a string literal" in a comment
    /* this is
       "also inside"
       //a comment */
    
    // and this /*
    "is not in a comment"
    // */
    
    "this is a /* string */ with nested // comments"
    </textarea>
    <h2>Output:</h2>
    <pre id="output"></pre>
  5. 意外的发现我对于一个c++11的强大武器一无所知,它就是raw string literal!这个的基本语法我居然不知道以至于在regex里看得稀里糊涂,也就是这里我才深切的感受到他的贴心,为了escape这些特殊字符实在是头疼。重复一下它的语法就是prefix(optional) R"delimiter(raw_characters)delimiter"这里的prefix是其中任意一个L, u8, u, U
  6. 对于这个小虾的建议我觉得也有可取之处,只不过对于raw string literal这种复杂的处理是无能为力了。
  7. 我发现perl regex非常的强大,文档我保存一下。我现在也明白了perl的regex是最强大的,而std::regex现在不支持。

一月二十三日 等待变化等待机会

  1. TL;DR

    这个例子包含了多少的perl-regex的技术呢?我几乎是一行一行的查询文档来破解它的天书般的文法,也许依靠这个在线regex debugger有很大帮助,不过仅仅拷贝中夹杂的看不见的回车字符就折磨了我好久,而且有些语法和boost::regex还是不一样的,所以还是要自己先阅读才有帮助。 这个花了我一个早上的时间,我看这个代码都要吐了!
    1. 第一步就是这个 (?(DEFINE) ...)
      (?(DEFINE)never-exectuted-pattern) Defines a block of code that is never executed and matches no characters: this is usually used to define one or more named sub-expressions which are referred to from elsewhere in the pattern.
      所以,很明显的它是一个定义不要执行,不要执行,不要执行!
    2. 这个是什么鬼?(?<prefix> ... ),
      You can create a named subexpression using:

      (?<NAME>expression)

      所以,这个是一个named定义。
    3. 这个是什么?(?:u8?|U|L)

      Non-marking groups

      (?:pattern) lexically groups pattern, without generating an additional sub-expression.
      所以,它是一个无名的定义,如果我们不需要index它的话。
    4. 这个是什么意思呢? // .* (*SKIP)(*FAIL)
      • (*PRUNE) Has no effect unless backtracked onto, in which case all the backtracking information prior to this point is discarded.
      • (*SKIP) Behaves the same as (*PRUNE) except that it is assumed that no match can possibly occur prior to the current point in the string being searched. This can be used to optimize searches by skipping over chunks of text that have already been determined can not form a match.
      • (*FAIL) Causes the match to fail unconditionally at this point, can be used to force the engine to backtrack.
      翻译成人话就是说SKIP制止了backtracking,这个和lazy有什么不同呢?FAIL也是制止了backtrack,那么他们有什么不同呢?
    5. 这个是什么意思? | /\* (?s: .*? ) \*/ (*SKIP)(*FAIL)

      Modifiers

      (?imsx-imsx ... ) alters which of the perl modifiers are in effect within the pattern, changes take effect from the point that the block is first seen and extend to any enclosing ). Letters before a '-' turn that perl modifier on, letters afterward, turn it off.

      (?imsx-imsx:pattern) applies the specified modifiers to pattern only.

      翻译成人话就是说要做改变,改什么呢?我们知道s代表了space那么重新定义为.*那么就是说dot(.)包含了space了?错了!错了!错了!s不是space因为\s才是!这里的<?s:..>指的是改变了single line的模式回想一下这个定义

      Wildcard

      The single character '.' when used outside of a character set will match any single character except:

      • The NULL character when the flag match_not_dot_null is passed to the matching algorithms.
      • The newline character when the flag match_not_dot_newline is passed to the matching algorithms.
      我们知道.默认不一定包含回车或者空字符的。根据这个regex的权威网站 这个内容没有错但是和本语句无太大关系,因为如果不考虑modifier它就是一个简单的.*?也就是普通的non-greedy适合任何字符的比对。并不是我之前错误的理解说是把space这个character class加入到了.字符集了,这个纯粹是痴人说梦!
      \s stands for “whitespace character”. Again, which characters this actually includes, depends on the regex flavor. In all flavors discussed in this tutorial, it includes [ \t\r\n\f]. That is: \s matches a space, a tab, a carriage return, a line feed, or a form feed. Most flavors also include the vertical tab, with Perl (prior to version 5.18) and PCRE (prior to version 8.34) being notable exceptions.
    6. 所以这个语句/\* (?s: .*?) \*/ (*SKIP)(*FAIL)究竟是什么意思?关键在于理解?s:是什么意思!single line modifier引出了无穷多的内容!
      • (?i) makes the regex case insensitive.
      • (?c) makes the regex case sensitive. Only supported by Tcl.
      • (?x) turn on free-spacing mode.
      • (?t) turn off free-spacing mode. Only supported by Tcl.
      • (?xx) turn on free-spacing mode, also in character classes. Supported by Perl 5.26 and PCRE2 10.30.
      • (?s) for “single line mode” makes the dot match all characters, including line breaks. Not supported by Ruby or JavaScript. In Tcl, (?s) also makes the caret and dollar match at the start and end of the string only.
      • (?m) for “multi-line mode” makes the caret and dollar match at the start and end of each line in the subject string. In Ruby, (?m) makes the dot match all characters, without affecting the caret and dollar which always match at the start and end of each line in Ruby. In Tcl, (?m) also prevents the dot from matching line breaks.
      • (?p) in Tcl makes the caret and dollar match at the start and the end of each line, and makes the dot match line breaks.
      • (?w) in Tcl makes the caret and dollar match only at the start and the end of the subject string, and prevents the dot from matching line breaks.
      • (?n) turns all unnamed groups into non-capturing groups. Only supported by .NET, XRegExp, and the JGsoft flavor. In Tcl, (?n) is the same as (?m).
      • (?J) allows duplicate group names. Only supported by PCRE and languages that use it such as Delphi, PHP and R.
      • (?U) turns on “ungreedy mode”, which switches the syntax for greedy and lazy quantifiers. So (?U)a* is lazy and (?U)a*? is greedy. Only supported by PCRE and languages that use it. It’s use is strongly discouraged because it confuses the meaning of the standard quantifier syntax.
      • (?d) corresponds with UNIX_LINES in Java, which makes the dot, caret, and dollar treat only the newline character \n as a line break, instead of recognizing all line break characters from the Unicode standard. Whether they match or don’t match (at) line breaks depends on (?s) and (?m).
      • (?b) makes Tcl interpret the regex as a POSIX BRE.
      • (?e) makes Tcl interpret the regex as a POSIX ERE.
      • (?q) makes Tcl interpret the regex as a literal string (minus the (?q) characters).
      • (?X) makes escaping letters with a backslash an error if that combination is not a valid regex token. Only supported by PCRE and languages that use it.
    7. 至于说这个问号我们已经反复碰到了 | /\* (?s: .*? ) \*/ (*SKIP)(*FAIL)

      Non greedy repeats

      The normal repeat operators are "greedy", that is to say they will consume as much input as possible. There are non-greedy versions available that will consume as little input as possible while still producing a match.
      • *? Matches the previous atom zero or more times, while consuming as little input as possible.
      • +? Matches the previous atom one or more times, while consuming as little input as possible.
      • ?? Matches the previous atom zero or one times, while consuming as little input as possible.
      • {n,}? Matches the previous atom n or more times, while consuming as little input as possible.
      • {n,m}? Matches the previous atom between n and m times, while consuming as little input as possible.
    8. 这个是什么鬼?| (?&prefix)? ' (?> (?&escape) | [^'\\\r\n]+ )+ ' (*SKIP)(*FAIL)

      Recursive Expressions

      (?N) (?-N) (?+N) (?R) (?0) (?&NAME)
      • (?R) and (?0) recurse to the start of the entire pattern.
      • (?N) executes sub-expression N recursively, for example (?2) will recurse to sub-expression 2.
      • (?-N) and (?+N) are relative recursions, so for example (?-1) recurses to the last sub-expression to be declared, and (?+1) recurses to the next sub-expression to be declared.
      • (?&NAME) recurses to named sub-expression NAME.
      也就是说 (?&prefix)代表“recurses to named sub-expression prefix”?这个是什么意思呢?它后面的?当然是non-greedy,可是什么叫做递归到它为止?是不是就是说这个定义不要再翻译了,像很多脚本语言定义可以再定义,变量可以再定义???
    9. 这个是什么意思?(?> (?&escape) | [^'\\\r\n]+ )

      Independent sub-expressions

      (?>pattern) pattern is matched independently of the surrounding patterns, the expression will never backtrack into pattern. Independent sub-expressions are typically used to improve performance; only the best possible match for pattern will be considered, if this doesn't allow the expression as a whole to match then no match is found at all.
      这里的简单粗暴比对不允许backtrack和末尾添加了(*SKIP)(*FAIL)是什么关系?似乎作者反反复复的不遗余力的防止backtracking,已经到了无所不用其极的地步了,这个只有专家和对于效率极其敏感的高手才如此行事。 看到这里我才对于作者定义的escape这个定义有了一些理解,原来它是一个c++的escape\和所有literal可能的组合,这个多么的浅显我一开始却不理解。
    10. 那么c++里的character literal是不允许空的,而string literal是允许空的 注意前者结尾是+而后者结尾是*这个和regex的关系不大,但是值得注意。
    11. 对于raw string literal的定义(?&prefix)? R " (?<delimiter>[^ ()\\\t\x0B\r\n]*) \( (?s:.*?) \) \k<delimiter> " 这个\k是什么意思?

      Back references

      • An escape character followed by a digit n, where n is in the range 1-9, matches the same string that was matched by sub-expression n. For example the expression: ^(a*)[^a]*\1$
      • You can also use the \g escape for the same function, for example:

        Escape

        Meaning

        \g1

        Match whatever matched sub-expression 1

        \g{1}

        Match whatever matched sub-expression 1: this form allows for safer parsing of the expression in cases like \g{1}2 or for indexes higher than 9 as in \g{1234}

        \g-1

        Match whatever matched the last opened sub-expression

        \g{-2}

        Match whatever matched the last but one opened sub-expression

        \g{one}

        Match whatever matched the sub-expression named "one"

      • Finally the \k escape can be used to refer to named subexpressions, for example \k<two> will match whatever matched the subexpression named "two".
      所以这里的\k<delimiter>就是reference之前定义的delimiter。 而这里要提醒一下\x0B是所谓的vertical tab在c/c++里几乎不会遇到,但是它只不过是delimiter的定义不能包含而已,这个似乎是c++ standard定义的吧?
  2. 我研究了一个上午发现这个表达式是有bug的因为它不能够强迫longest match,就是说raw string literal不会被优先尝试,结果全部都返回了普通的string literal。 我几乎花了一个下午才有些眉目:首先,代码没有问题,因为我没有理解题主的需求,他要求的仅仅是把c++代码里的字符串提取出来,这个首先要过滤注释和character literal,后者并不是他的需求。所以,我才理解到使用(*SKIP)(*FAIL)目的不是为了效率,而是为了drop掉它们,这里我们需要辨识注释和character literal,但是一旦match我们就抛弃它们。第二,我试图在 里定义一些pattern然后直接去调用\k<raw_string>这样子是不行的因为所谓的reference是matched而我压根就没有使用任何pattern去调用,所以是不能reference的,这个不是一个纯粹的可以像定义函数一样的来调用的。

一月二十四日 等待变化等待机会

  1. 我花了一两个小时在debug一个无厘头的东西,不知道这个是boost::regex的特殊性还是通用的,看来我对于perl-regex极度缺乏了解导致的举步维艰。就是这么一个表达式里 \\begin\{bnf\}(?s:.+?)\\end\{bnf\} 是否有sub_match,我自然而然的认为有。难道这个()不是可以作为sub_match吗?结果不是的,我必须再加一对((?s:.+?))才行。这个真的是让人无语。
  2. 这里有一张很好的c++ timeline的图景,很好看的。
  3. 花了很多时间犹豫迷惑其实就搞明白了一件小事情,就是说BNF的形式并没有什么错误,我看到的并不是什么BNF的变种而是LATEX的BNF的形式,所谓的typesetting,所以,我需要自己写一个regex把它解析出来,因为使用其他如detex之类的不一定能够满足我的需求因为其中的command之类的可能就被抛弃了。

一月二十五日 等待变化等待机会

  1. 我一开始对于recursive的regex有疑虑,这里的R似乎在我看来不像是递归recurse而更像是重复repeat,因为对于这个(\w)(?:(?R)|\w?)\1,我感到无法理解,这里的(?R)怎么就做到recursion了呢?前面的pattern就是一个字母怎么能够递归呢?这个不是重复是什么?但是实际上我自己用我的直觉写的一个所谓的我自己造出的recursion(?:\w+)\w?\1很明显的不工作,boost的异常说
    Invalid back reference: specified capturing group does not exist. The error occurred while parsing the regular expression fragment: '(?:\w+)\w?>>>HERE>>>\1
    我随后发现了错误改造成了这个(?<name>\w+)\w?\1可是这个只能match最短的palindrom,比如我的输入是
    this is a palindromordnilal andna
    结果只有而那些最长的palindromordnilalandna却无法发现。 这个说明了什么呢?我自己给自己的解释是所谓的recursion在这里更像一个captured context,就是说在(?R)这里重新开始它的context?这个解释我自己都不明白,反正这个就是regex的机制。

一月二十六日 等待变化等待机会

  1. 我在被递归折磨中,几乎是盲目的尝试所有的可能,感觉boost的perl-regex引擎似乎和regex101.com的不太一样,难道有什么bug吗?
  2. 作者讲述的regex recursion对于我太高深了,我还是从破解所有的?开始吧。

一月二十七日 等待变化等待机会

  1. 包含escaped的sequence是很难把握的东西
  2. 前进是异常的艰苦,一方面是递归的困难,另一方面是c++语法里有大量的操作符作为terminal,面对这个庞然大物 我的脆弱的regex必须要彻底重写。

一月二十八日 等待变化等待机会

  1. 似乎是遇到了不可逾越的鸿沟,想记录下来并不是成功的道路,反而是四处碰壁的经过,希望整理过去发现有什么遗漏的方向。
  2. 首先,我要应对的是有递归的需求的,之前我想绕过递归结果遇到无法应对的case 比如,其中的\caret{}作为\terminal的一个成员不使用递归的表达式很难解决配对的{}
  3. 纯粹的递归里是依赖于特殊字符{}作为delimiter所以,传统的递归是这样子的 这里必须假定分隔符{}不能单独出现,于是它无法应对这个情况
  4. 我找了很久去理解lookaround最后总算尝试出了这么一个atomic的做法,因为lookaround似乎我不知道要怎么写。 这里我把\{\}作为两个atomic group和非[^{}]的集合并列来解决递归包含escaped {}的问题。
  5. 但是接下来又遇到一个看似简单的问题,就是这两种表达式并列的 其中的binary-literal远远比 \opt{integer-suffix}看上去简单,而且不需要递归,应该很容易解决 这个看似解决了问题,对于以上的问题都能解决。
  6. 结果我遇到了这个表达式卡在了这里 一开始我对于错误Ran out of stack space trying to match the regular expression.百思不得其解,根本没有意识到为什么出错,显然它并没有什么超出之前的范围的特殊现象,为什么它就出错呢?经过了痛苦的一个晚上的盲目的debug才意识到 \textbackslash{}的字符长度导致了递归的增大,总之它的长度决定了这个错误,这让我极其的灰心丧气,这个问题看不出有什么好的解决路径因为递归机制我极其的迷惑,完全不理解是怎么产生的。目前就卡在这里。看来需要再次回头来看这里的black belt program
  7. 看来整理思路是有用的,经过整理我觉得不需要使用递归,因为我目前仅仅处理BNF的语法而已不需要那么高级的完美的regex,所以,我改变了一下就ok了。 这里我仅仅需要解决一个{}对子的问题。仅仅使用atomic group的(?<!\\)\{[^{}]*(?<!\\)\}。 这个是c++的grammar的BNF的源文件

一月二十九日 等待变化等待机会

  1. 在泥沼中跋涉了几天似乎终于走到了对岸,实际上才真正接触到问题的本质,也就是理解BNF语法的层面,一开始我以为遍布其中的分隔符\br是latex的附带的分隔符我可以使用简单的regex_replace把它过滤掉,现在才明白它的功能至少是BNF里的“或”,比如 这里的q-charq-char-sequence q-char代表了BNF的语法的循环递归的机制,我是不能简单的去除的。这个是一个大问题,只能说是预料当中的,因为语法的定义本身就是一个大问题,否则compiler-compiler岂不是人人都写的出来的吗?至少我已经把parsing latex BNF的简单工作完成了大部分了,现在就是选择的问题了。究竟你要做什么?
  2. 这个的确让人打开眼界,让人对于gdb的看法立马改变,一个简单的crtl+xa的快捷键就进入了TUI阶段,正如演讲者说的那样:关键你要知道gdb存在着那些功能你才会去找来用。因为我从来不知道有这个TUI的功能啊!
  3. 君欲何为?

一月三十日 等待变化等待机会

  1. 语法里看到所谓的source character set,我看了这里的解释依旧是一头雾水。
  2. 搜索到了另一个c++标准草稿的网站,我有些不明白他们彼此的关系,反正是草案吧?
  3. bison里面提到典型的shift/reduce conflicts,而解决的办法是operator precedence这类办法,可是我满怀希望的在BNF语法里却找不到关于precedence的语法,搜寻标准文档是这么说的
    The precedence of operators is not directly specified, but it can be derived from the syntax.
    这个是从何说起呢?难道从这个operator-or-punctuator庞然大物里能够推理出来吗? 这里有一些有趣的操作符我是从来没有见到过的,比如<:,这个是c++官方语法里不承认的,难道这个是将来可以拓展的?现在我知道这个是所谓的alternative token,就是说preprocessor里需要处理的,它代表的是[
  4. 有一个小的不能再小的问题困扰了我许久就是在语法里\textbackslash被当作terminal可是我却无法找到这个textbackslash的定义,因为在我看来凡是有\开头的就是一个代号必定要有定义吧?可是找不到,在lex.tex里也是直接使用,我百思不得其解后来才意识到这个会不会是tex本身的预定义的呢?正如\caret也是类似的?如果这个猜测是对的,我就必须先把它们从latex的预定义里先翻译出来了吗?
  5. 这里好像是证明了我的猜想,的确它们是latex的预定义的symbol
  6. 我本来满心欢喜想从opendetex里的源码找出它如何辨别symbol的,可是发现它肯定是用yacc/bison之类产生的,因为代码很简单就是一个自动机的表而已。
  7. 我现在开始意识到我写regex都昏了头了,居然没有意识到.tex里有大量的escaped symbol,它们是latex的,而这个需要在我抓取之前就要处理掉,也就省却了一些问题吧?可是假如是这样子的,那么我要怎么处理递归的{}呢?也许变的简单了,不是吗?我根本就不需要再去考虑有已经被escaped{}的问题了!我想验证一下想法发现opendetex很老旧根本就不行,而latex2html之类的更加不行,也许它们是期待latex而不是.tex,这个应该很容易才对,我其实仅仅是对于tex里的symbol不是很理解有些迟疑,结果找不到一个可以验证的程序。
  8. 行有不得,反求诸己
    孟子曰:“爱人不亲,反其仁;治人不治,反其智;礼人不答,反其敬——行有不得者皆反求诸己,其身正而天下归之。《诗》云:‘永言配命,自求多福。’”
    孟子的话真的好感人啊,漂亮国的领导人真的应该好好学习一下。
    “仁者如射:射者正己而后发;发而不中,不怨胜己者,反求诸己而已矣。”
    我觉得这个应该反省,对于.tex这么一个简单的文本文件,我不求诸己反而搜寻无数的不着四六的小程序来参考是注定失败的。也许根本就是一个简单的小问题。我根本就不知道这个bnf语法是用什么工具生成的,仅仅有个别的符号被使用了latex的escape方法我去掉它们不就行了吗?值得这么大惊小怪的吗?
    “其身正,不令而行;其身不正,虽令不从。”(《论语·子路》)
    “爱人者,人恒爱之;敬人者,人恒敬之。”“人必自侮,而后人侮之;家必自毁,而后人毁之。”
    中国古人对于做人的道理讲的实在是太透彻了,这真的是世界人民的财富啊。
    “各自责,天清地宁;各相责,天翻地覆。”

一月三十一日 等待变化等待机会

  1. 看到头条上有一篇关于c++新的variadic template parameter的文章,其中谈到关于parameter pack展开的问题,我对于这个问题印象深刻,忍不住要再次总结一下。
  2. 先保存一个关于latex的symbol的文档。

二月一日 等待变化等待机会

  1. 有时候英语和对于其他西方语言的不熟悉居然会成为很大的障碍,比如对于这个trigraph我一开始始终不理解,甚至还在想是不是指的就是escaped的字符,但是应该是digraph啊?事实上这个digraph也是非常陌生的提法,至少对于我来说,我也从来没有意识到字符处理的麻烦事情。比如这句话要怎么理解呢?
    Also, unlike standard C, trigraphs have no special meaning in Bison character literals, nor is backslash-newline allowed.
    这句话让我感到惶恐,为什么我不知道标准C语言有支持trigraph呢?对于此我是闻所未闻啊。 对于这个大侠的引用我觉得有必要保存一下

    Before any other processing takes place, each occurrence of one of the following sequences of three characters (“trigraph sequences”) is replaced by the single character indicated in Table 1.

    ----------------------------------------------------------------------------
    | trigraph | replacement | trigraph | replacement | trigraph | replacement |
    ----------------------------------------------------------------------------
    | ??=      | #           | ??(      | [           | ??<      | {           |
    | ??/      | \           | ??)      | ]           | ??>      | }           |
    | ??’      | ˆ           | ??!      | |           | ??-      | ˜           |
    ----------------------------------------------------------------------------
    
    我赶快搜了一下,还好在我的语法输入文件里没有这些??前缀。总结一下我阅读大侠们的解释就是c/c++编译器应当会把这一类的“三元符”替换吧? 实践的是检验真理的唯一标准 当我编译printf( "What??!\n" );的时候gcc报出了警告:三元符 ??! 被忽略,请使用 -trigraphs 来启用 [-Wtrigraphs]
  2. 所以bison支持digraph也就是c语言的escaped的字符,但是不支持trigraph,这么一个简单的信息让我忙活了大半天。
  3. 我忽然明白了一个白痴都明白的道理,bison本身并没有集成flex,我先前不知道从哪里得到的印象是语法里包含了flex,而这个观念让我一直对于很多事情感到困惑。这个错误的观念从哪里来的呢?而且阅读flex让我遇到新的难题,就是它期待着regex的表达式,我有办法把BNF翻译成regex吗?还是不要急着下结论,毕竟看文档是会有歧义的:RTFC!实际上这个软件开发的基本原则,即便不集成代码肯定也是调用其接口,断断没有让别人束缚的道理,我们不可能在高级的parser里面再去操心低级的lexer的问题。所以,有时候先入为主是我认为理所当然。flex有它自己的表达,可是我们比它高了一个维度,我们对于它是利用而已。
  4. 阅读手册是很费力的,我似乎对于这个过程总是感到抵触或许是害怕,主要还是莫名其妙的担心,害怕走错了路,而一旦表达式开始几乎就没有debug的可能,因为那个实在是超出了人力的可能了。所以,慎重也是应该的。累了就看看赏心悦目的吧,对于昨天头条上的那篇文章,我再想想其实很不错的。比如这个关于泛化工厂函数的做法就值得我好好收藏,虽然之前已经反复看到过,但是似乎就没有真正的有体会,这次提醒的好 首先就是一个很好的技巧在于如何传递模板参数和类的ctor的实参,这一点就是其中的关键,我往往就是粗枝大叶疏忽了细节: 注意到这里我们怎么传递模板参数的,只有类的类型是需要的,而类的ctor参数我们直接传递实参让编译器自己补脑了。我不确定这个是否有包含CTAD的成分在里面,应该是没有吧。因为这个不是class啊,只是一个函数,但是这个推理能力也许从模板的第一天就有了吗?不一定因为现在已经是variadic template了,至少这部分是新的吧。
  5. 我一时间老眼昏花把mem_funmem_fn搞混了,前者是前朝遗老已经不为很多人所熟知的过时的东西了,后者是攀龙附凤于function的新贵,两者不可同日而语了。首先前者是c++98年代的上个世纪的老古董,按照标准应该在c++11就是叛了死缓,在c++17就枪决了,也正因为它是c++17的死刑执行期而c++20依然死不改悔不愿意把gcc-10/11的内部版本从c++17改成c++20导致它还居然活着。两者有一个显著的区别就是参数传递上老古董要传类的指针,新贵只是类对象引用吧?不,我觉得是最大的改进是允许传递rvalue reference,这个应该是最大的动因之一吧。 所以,我对于作者的delegate的实现感到一些些的不敢苟同,不是说不好,但是这个不是等于是重新实现std::mem_fn吗?作为了解机理深入理解是好的,但是差一点把我带到沟里去,因为我感觉不应该这么麻烦。比如这个就有重新造轮子的嫌疑,因为简单的调用std::mem_fn就能够达到同样的效果。而且作者尝试实现rvalue reference的重载operator()看来没有那么容易,还是使用标准库吧。

二月二日 等待变化等待机会

  1. 这个实在是考验一个人的耐心,对于.tex文件处理latex的语法实在是让人头疼啊。我刚刚才意识到\quadlatex的macro command,这个真的是烦人啊。
  2. 我反复在质问这么费力处理这个latex的txt文件的意义在哪里?难道我们不能直接使用其他方式吗?可是当我使用pdf2txt抓取pdf的文档发现它丢失了关键的\opt信息,同时分行处理也许可以推理出来逻辑or,但总之是有些半信半疑,所以,没有办法只好继续。
  3. 期间我还遇到诸如\mname,\xname这样子的宏,它的定义在macro.xxx里看得一头雾水,我也不想去钻研latex宏的定义方法了,可能只能手动修改吧,类似的关于export-keyword,module-keyword,grammarterm这一类的宏我已经绝望了,干脆全部自己脑补后期处理吧。

二月三日 等待变化等待机会

  1. 我决定放弃使用regex处理原始的latex的tex文件,因为有些问题实在是很难解决,因为脱离不了手工的工作,比如不少的类似于注解一样的\textnormal里面的指示让我把以下的symbol都包含进来,这个实在是得不偿失,并且这个注释里面包含了很多的terminal,我感觉我肉眼都被骗过更不要说一开始写的regex了。毕竟语法的变动不是那么大,并且这里还有一个学习的过程也相当重要,比如我第一次听说有所谓alternative tokens,就是说绝大多数程序员使用curly bracker{}可是谁知道远古时代居然也有人用<%%>,这个实在是有些让人惊诧莫名,对于and替代&&大家是熟知的,可是这个替代[]<::>呢?那么对这个表你有什么想法呢?
    alternative tokenprimary token
    <%{
    %>}
    <:[
    :>]
    %:#
    %:%:##
  2. TL;DR

    This is why the standard does not attempt to provide a complete formal grammar, and why it chooses to write some of the parsing rules in technical English.

    这段时间我最重要的“发现”就是一个基本的疑惑的解答:为什么标准没有给出c++语法的[AE]?BNF的形式?为什么我满世界搜索找不到任何人的尝试?因为不可能!

    这篇长文是我的救命菩萨。我想我应该多读几遍,保存一个拷贝在这里。

    Below is my (current) favorite demonstration of why parsing C++ is (probably) Turing-complete, since it shows a program which is syntactically correct if and only if a given integer is prime.

    So I assert that C++ is neither context-free nor context-sensitive.

    If you allow arbitrary symbol sequences on both sides of any production, you produce an Type-0 grammar ("unrestricted") in the Chomsky hierarchy, which is more powerful than a context-sensitive grammar; unrestricted grammars are Turing-complete. A context-sensitive (Type-1) grammar allows multiple symbols of context on the left hand side of a production, but the same context must appear on the right hand side of the production (hence the name "context-sensitive"). [1] Context-sensitive grammars are equivalent to linear-bounded Turing machines.

    In the example program, the prime computation could be performed by a linear-bounded Turing machine, so it does not quite prove Turing equivalence, but the important part is that the parser needs to perform the computation in order to perform syntactic analysis. It could have been any computation expressible as a template instantiation and there is every reason to believe that C++ template instantiation is Turing-complete. See, for example, Todd L. Veldhuizen's 2003 paper.

    Regardless, C++ can be parsed by a computer, so it could certainly be parsed by a Turing machine. Consequently, an unrestricted grammar could recognize it. Actually writing such a grammar would be impractical, which is why the standard doesn't try to do so. (See below.)

    The issue with "ambiguity" of certain expressions is mostly a red herring. To start with, ambiguity is a feature of a particular grammar, not a language. Even if a language can be proven to have no unambiguous grammars, if it can be recognized by a context-free grammar, it's context-free. Similarly, if it cannot be recognized by a context-free grammar but it can be recognized by a context-sensitive grammar, it's context-sensitive. Ambiguity is not relevant.

    But in any event, like line 21 (i.e. auto b = foo<IsPrime<234799>>::typen<1>();) in the program below, the expressions are not ambiguous at all; they are simply parsed differently depending on context. In the simplest expression of the issue, the syntactic category of certain identifiers is dependent on how they have been declared (types and functions, for example), which means that the formal language would have to recognize the fact that two arbitrary-length strings in the same program are identical (declaration and use). This can be modelled by the "copy" grammar, which is the grammar which recognizes two consecutive exact copies of the same word. It's easy to prove with the pumping lemma that this language is not context-free. A context-sensitive grammar for this language is possible, and a Type-0 grammar is provided in the answer to this question: https://math.stackexchange.com/questions/163830/context-sensitive-grammar-for-the-copy-language .

    If one were to attempt to write a context-sensitive (or unrestricted) grammar to parse C++, it would quite possibly fill the universe with scribblings. Writing a Turing machine to parse C++ would be an equally impossible undertaking. Even writing a C++ program is difficult, and as far as I know none have been proven correct. This is why the standard does not attempt to provide a complete formal grammar, and why it chooses to write some of the parsing rules in technical English.

    What looks like a formal grammar in the C++ standard is not the complete formal definition of the syntax of the C++ language. It's not even the complete formal definition of the language after preprocessing, which might be easier to formalize. (That wouldn't be the language, though: the C++ language as defined by the standard includes the preprocessor, and the operation of the preprocessor is described algorithmically since it would be extremely hard to describe in any grammatical formalism. It is in that section of the standard where lexical decomposition is described, including the rules where it must be applied more than once.)

    The various grammars (two overlapping grammars for lexical analysis, one which takes place before preprocessing and the other, if necessary, afterwards, plus the "syntactic" grammar) are collected in Appendix A, with this important note (emphasis added):

    This summary of C++ syntax is intended to be an aid to comprehension. It is not an exact statement of the language. In particular, the grammar described here accepts a superset of valid C++ constructs. Disambiguation rules (6.8, 7.1, 10.2) must be applied to distinguish expressions from declarations. Further, access control, ambiguity, and type rules must be used to weed out syntactically valid but meaningless constructs.

    Finally, here's the promised program. Line 21 is syntactically correct if and only if the N in IsPrime<N> is prime. Otherwise, typen is an integer, not a template, so typen<1>() is parsed as (typen<1)>() which is syntactically incorrect because () is not a syntactically valid expression.

    template<bool V> struct answer { answer(int) {} bool operator()(){return V;}};
    
    template<bool no, bool yes, int f, int p> struct IsPrimeHelper
      : IsPrimeHelper<p % f == 0, f * f >= p, f + 2, p> {};
    template<bool yes, int f, int p> struct IsPrimeHelper<true, yes, f, p> { using type = answer<false>; };
    template<int f, int p> struct IsPrimeHelper<false, true, f, p> { using type = answer<true>; };
    
    template<int I> using IsPrime = typename IsPrimeHelper<!(I&1), false, 3, I>::type;
    template<int I>
    struct X { static const int i = I; int a[i]; }; 
    
    template<typename A> struct foo;
    template<>struct foo<answer<true>>{
      template<int I> using typen = X<I>;
    };
    template<> struct foo<answer<false>>{
      static const int typen = 0;
    };
    
    int main() {
      auto b = foo<IsPrime<234799>>::typen<1>(); // Syntax error if not prime
      return 0;
    }
    

    [1] To put it more technically, every production in a context-sensitive grammar must be of the form:

    αAβ → αγβ

    where A is a non-terminal and α, β are possibly empty sequences of grammar symbols, and γ is a non-empty sequence. (Grammar symbols may be either terminals or non-terminals).

    This can be read as A → γ only in the context [α, β]. In a context-free (Type 2) grammar, α and β must be empty.

    It turns out that you can also restrict grammars with the "monotonic" restriction, where every production must be of the form:

    α → β where |α| ≥ |β| > 0  (|α| means "the length of α")

    It's possible to prove that the set of languages recognized by monotonic grammars is exactly the same as the set of languages recognized by context-sensitive grammars, and it's often the case that it's easier to base proofs on monotonic grammars. Consequently, it's pretty common to see "context-sensitive" used as though it meant "monotonic".

  3. 搜到了这么一个很有趣的论文,看abstract似乎很诱人,先保存一个版本
  4. 现在回想我的错误也许就在于认为c是ContextFree,所以,c++也是,其实,我对于前者是否正确也半信半疑,不过似乎有人作出过c的bnf语法,好像也有人作出过c++11的bnf语法,我看到的帖子说制作着自称符合ISO的标准,那个时候我就纳闷这个为什么要自称,难道没有公论吗?委员会怎么不吭声?现在才明白为什么!花了两个星期搞明白了一个基本的事实算是贻笑大方还是自我满足?总之,对于我自己来说总算是进步。
  5. 作者提到一个g++选项-fdump-translation-unit可是似乎最新的gcc-10/11不支持?然后看gcc-10.2.0的文档,看来这个选项是被删除了,在developer option里有大量的有趣的dump选项,所以,作者的研究途径还是可能的吧?
  6. 那篇论文的结论是很有用的,他们公司的目标要求排除了其他选项结果只有eclipse CDT for c++和clang是唯一的候选,而作者的研究方法也是值得借鉴的,就是究竟要怎么使用AST,这个问题我其实不是很清楚。我决定看看LLVM里的Clang的做法是怎样的。我一直不是很清楚原来clang是为了llvm做前端而存在的。看了clang的介绍我觉得这个是c/c++程序员都盼望要翻身的解放的东西。gcc虽好但是太复杂了,完全是一个黑盒子,当然这是我辈凡夫俗子的看法,在大神眼里gcc依然是最好的。
  7. 这几天折腾regex难道是白费功夫吗?我不忍心丢弃辛苦换来的一点点的经验。比如我们要识别若干个pattern,我觉得这样子就挺好:首先我们定义一组的regex,它们是所有的可能的类型。 然后在regex_match中我们搜索它的match的index,注意必须从1开始,因为0总是matched,我们要找的是submatch。 所以,这个可以作为一个搜索pattern的范式来做。

二月四日 等待变化等待机会

  1. 感觉我之前的认识还是有误,flex和bison要紧密结合的,我的意思是我以为可以直接在bison的定义里写flex的定义,看来是不行的。下载了这本书来看看吧。flex && bison
  2. 关于大侠的长文,我看了十几分钟,却盯着例子程序,这个是我之前的尝试,我完全偏离了重点,虽然大侠的程序有缺陷比如1,2,3这些trivial的case都出错,但是这个很容易用speicialization来解决。重点是他只搜索所有的奇数就比我聪明。我怎么没有想到呢?还有这个讨论的重点是context free grammar,我走神了。
  3. 继续走神中,我曾经武断的说编译期都是递归,实际上是不对的,因为对于type之类的复杂的类型计算可能只有递归是常用的方法,可是现在有了constexpr/consteval这样子的神器,完全可以定义一个常用常数一样的小函数实现编译期的常数值。比如 我不确定究竟是否需要consteval,但是constexpr是足够的,因为这样子的static_assert(isPrimeFn(104729));是证明。所以说这个做法就突破了递归只有900层的限制了。
  4. 笔记本无端端的黑屏死机让我恼火万分,发誓今后坚决不买AMD的CPU,但是也让我怀疑是不是M$的teams在捣乱,想要卸载发现它的自动启动是写在udev的一个70-snap.teams.rules的文件里的,原来Ubuntu官方的teams包和我自己直接下载的包是两个完全不同的东西,难怪我卸载了官方的teams包居然还会自动启动,这个下载的是snap的包,我直接删除那个文件夹不成功才意识到snap的机制不是简单的文件夹,查看mount才明白它是squashfs的mount,那么在鲁莽的手动卸载前我决定尝试官方的snap remove发现这个过程还是挺复杂的,我怀疑是chroot之类的做法,linux的所有的“虚拟”机制最根本的都会chroot,不过设备通信是大学问。我因为尝试过一次ubuntu snap感觉很别扭就再也没有关心过,感觉很抵制新鲜事物,宁可沿用古老的技术。
  5. 如果直接从标准的html版本来parser也许更容易一些吧?因为少了latex的语法不熟悉的问题,可以有很多的html parser来用,但是各有利弊吧,总之,这个可以作为手工校对来用,比pdf版本好用。保存一个本地版本。现在需要把一部分的terminal放在flex的token里去使用regex,看来bison里是不接受regex的吧?
  6. 花了这么多天的初步成果就是这个c++20的语法BNF文件,其中有几个很难在bison文件里表达准备在flex里用regex表示。保存一个版本

二月五日 等待变化等待机会

  1. 我感觉即便flex/bison再怎么简单也需要循序渐进的学习使用,何况它们绝对不是什么很简单的玩意儿。所以,沉下心来一步一步牙牙学语吧。
  2. 才刚刚看第一个例子就累了。休息时候看这个视频让人很感动,因为我一向对于design pattern不感冒,原因是个人的偏见,也许也是孤陋寡闻。或许我只相信我能看得见的东西,因为大多数人是因为看见才相信,而只有少数人是因为相信才看见。很多时候我对于谈论pattern的人感觉是空谈,因为就好像俗话说的“外行谈战略,内行看后勤”一样的意思,战略和pattern有相似之处就是基本上哪怕再怎么外行的人都能说出貌似高深莫测的话语,很多时候无法辨别是真知灼见还是拾人牙慧,同样的要辨别写过关于pattern的真正的计算机专家可能是一件非常困难的事情,因为pattern本身的定义就是一个很成问题的问题,就好比人工智能如今这么热门但凡是个人就会出口引用,什么是大数据,什么是AI?真的都理解吗?别人我不知道,我自己知道我不能理解。图灵测验是一个检验的标准还是一个概念的定义?就算是检验的标准它的量化指标是怎么定义的?大数据指的是什么?多大?什么数据?用户偏好与选择之间的关联就是唯一的要分析的数据吗?分析数据是为了什么?是预测吗?这个视频从一开始就首先定义了以下心什么是我们谈论的design pattern
    [Software]Design pattern is a repeatable, commonly recognized and understood solution to a design problem commonly occurring in software engineering.
    • Design problem
    • Common occurring
    • Widely accepted solution
    • Known advantages and trade-offs
    这说明什么?
  3. 作者的一个很发人深省的问题就是虽然我们常常听到说design pattern是高出于语言本身的具有普遍意义的高级抽象,然而语言本身的特性与发展是否反过来影响design pattern呢?回答否的人很多是那种竭力维护所谓基本原则教条不肯与具体实践相结合的人是典型的本本主义,理论当然应该与实践相结合,不同语言与特性对于编程实践的要求必然导致design pattern作出适应性的演化与变革。所以,作者犀利的指出c++11/14/17中涌现出的一些引人注目的编程模式的新特性以及变迁,这个是真正的值得关注与研究学习的。作者在谈到visitor的例子里让我联想到了他应该指的是enable_shared_from_this的设计,就是利用了模板继承的机制,即所谓的CRTP,但是作者说almost, but not quite。我感觉累了,收藏起来再看。
  4. 作者的代码在这里,在Chapter18里。他的代码需要深入的理解,但是有一点让我产生共鸣的是使用模板的parameter pack做成的多重继承的虚拟的ctor,作者使用的是所谓的deduction guide,然后使用了高级的技术我猜想是类似于高级的回调函数之类的,超过了我的想像。
  5. 直到现在我才真正开始接触问题的本质,使用bison/flex不是什么大问题,这个随便跟着例子看看就理解了怎么使用,这里我曾经来过,再次来明白的更多,现在明白为什么没有c++的BNF,因为是mission impossible。这里是引用的源头。下载了这篇论文也许有帮助,保存这里。论文是一篇巨文,我光看目录标题就被吓坏了。

二月六日 等待变化等待机会

  1. 前辈的论文可以是一个很好的学习工具。

    The dragon book [Aho86] recognises that the boundaries between lexical, syntactic and semantic analysis are not clear cut.

    Traditional C++ approaches seek a correct high resolution parse. As a result, the boundary between syntactic and semantic analysis has to be shifted to exploit semantic information during syntactic analysis by the parser and to leak semantic information through to the lexer. Use of semantic information during syntactic analysis requires very tight coupling to ensure that scope context is honoured and that changes of name visibility in mid-statement are correct.

  2. 又下载了一些作者其他的论文,大概明白了他要做的是什么吧?不知道这个是不是相关的,作者似乎后来的文章转向了其他领域,也许这个方向是没有很大吸引力吧?
  3. 下载古老的废弃的遗迹openc++作为“考古”发现。遇到大家经常遇到的windows下的换行符的错误
    bin/bash^M: 坏的解释器: 没有那个文件或目录
    由于在Windows下换行是\n\r,在Linux下打开多了\r,所以需要删除。删除命令 :sed -i 's/\r$//' filename
  4. 其实最麻烦的一个地方是preprocessor的问题,这个在我下载的语法里甚至都不是很全,这个似乎是另一个领域的问题。而我感觉meta-compiler的作者和openc++的作者的一个很大的出发点是解决这个问题。我想做个实验结果又忘记了regex的(*SKIP)(*FAIL)的用法,解决断章取义是不行的,就是说它实现了类似这个regex的语法A(*SKIP)(*FAIL)|B等价于过滤掉A之后的B,这个在regex的表达式里很难表达如果不借助(*SKIP)(*FAIL)的话。
  5. 另一个让我哭笑不得的是我又又又又一次犯同样的错误,在eclipse里__FILE__是一个可能被重定义的宏不是重定义而是传入gcc的参数是相对路径,运行期在不同的路径自然找不到源文件了。以前写makefile的时候大家通常喜欢相对路径的原因是在现场debug时候gdb容易找到代码文件。,结果我在编译期得到的是想对路径,运行期时候就不对了,这个才又一次让我体会到source_location的必要性!
  6. 再一次的让我体会到preprocessor的部分是不列入语法的,即便c语言也是如此。也许这个微软的定义可以参考一下。 应该说不全,比如像comment的部分大家都选择性忽略,也许太简单,也许太麻烦?

二月七日 等待变化等待机会

  1. 早上起来对于昨天被虐待的regex的谜题依然不解,如果一个人想要被折磨那么regex一定是一个领域,我的想法是,能不能稍微了解一下它的实现来更加深入的理解它的问题呢?阅读这里给了我不少的解释,那么结合代码也许理解更多吧?
  2. 另一个想法就是每隔几天我应该抽时间回顾一下过去的笔记,所谓学而时习之不亦乐乎?

二月九日 等待变化等待机会

  1. 我对于cppreference的不完整部分比如set_difference有一些抱怨,但是等我自己查看源代码我也就明白了,这个确实不能有,因为gcc-10.2让我困惑,不是说好的range的库吗?怎么好像没有实现呢?还是我理解错误?
  2. 我压根儿没有关心过c++的具体语法,于是在我做试验来检验我整理的bnf的时候发现了这些个r-chard-char,s-char之类的看得令人费解。raw-string我可以理解,但是其他的呢?在我看来string literal尤其是宽字符是非常麻烦的一个领域,因为平常用的少感觉特别的啰嗦。
  3. 最后我的小工具花了好几天才完成,其实就是想检验一下我手工打造的BNF的正确性,我在nongnu里看到古老的年代这样类似的想法就可以写一个工具软件,生活在那个时代是幸运的因为如同亚当夏娃在伊甸园里的闲聊的单词都成为人类语言的创造,但是同时也是不幸的因为你的任何的牙牙学语会给后人带来无穷的困惑,但是更糟糕的是被后人所摒弃与遗忘。这个是胡说八道,我看到的bnf parser应该是类似与yacc/bison的小工具,只不过我实在是懒得看不懂代码。 不过我确实发现了不少的重复和缺失,尤其是又一次遇到新增的我认为还没有修订的部分就是 module部分的语法
    module-import-declaration:
    	import-keyword module-name attribute-specifier-seq opt ;
    	import-keyword module-partition attribute-specifier-seq opt;
    	import-keyword header-name attribute-specifier-seq opt ; 
    
    回过头来看在preprocessing的部分的说明
     pp-import:
    	exportopt import header-name pp-tokensopt ; new-line
    	exportopt import header-name-tokens pp-tokensopt ; new-line
    	exportopt import pp-tokens ; new-line 
    
    这里说到这些xxx-keyword是怎么来的,可是我感到很难理解,这个是说preprocessor怎么产生的token要被parser来使用吗?感觉类似flex定义的token被bison使用一样。那么这种语法我要怎么定义呢?也放在flex里?

    In all three forms of pp-import, the import and export (if it exists) preprocessing tokens are replaced by the import-keyword and export-keyword preprocessing tokens respectively.

    [Note 1: This makes the line no longer a directive so it is not removed at the end of phase 4.
    — end note]
    
    我尝试理解就是import/export这些在preprocessor directive之后能够得以保存是归功于把它们重新定义为token就是说它们所跟随的是在preprossor里处理因为有包含头文件等等,但是也包含了类和函数,所以,这些import/export随后还必须在parser里分析语义不能像#include一样被去除掉?
  4. 我清除了重复部分在这里保存了一个c++语法的bnf临时版本,不想覆盖旧的说不定还有用,因为我还搞不清楚为什么我会有重复的部分,也许是从lex拷贝带过来的?这就是手动修改的问题。
  5. 我领悟到了一个最粗浅不过的道理:同样是bnf,但是preprocessoring的BNF是要有行动的,我原本不想触及syntax的语义分析和动作,但是preprocessor是一个绕不过的坎,它的结果是下一步的输入,所以同样是BNF但是parser的部分不一定要再去处理,可是preprocessor的部分是不是就应该要放在flex部分来实现呢?

二月十日 等待变化等待机会

  1. 我找到了我想知道的c/c++ preprocessor的阶段

    TL;DR

    • 把不符合所谓96个的source character set的字符
      a b c d e f g h i j k l m n o p q r s t u v w x y z
      A B C D E F G H I J K L M N O P Q R S T U V W X Y Z
      0 1 2 3 4 5 6 7 8 9
      _ { } [ ] # ( ) < > % : ; . ? * + - / ^ & | ~ ! = , \ " '
      
      转化为universal-character-name
      \u hex-quad
      \U hex-quad hex-quad
      
      原来所谓的ISO/IEC 10646就是我们常说的 Universal Coded Character Set (UCS)

      The UCS has over 1.1 million possible code points available for use/allocation, but only the first 65,536, which is the Basic Multilingual Plane (BMP), had entered into common use before 2000. This situation began changing when the People's Republic of China (PRC) ruled in 2006 that all software sold in its jurisdiction would have to support GB 18030. This required software intended for sale in the PRC to move beyond the BMP.

      这里让我不清楚GB18030是否在这个65536之内还是之外呢?
      UCS-2 thereby permits a binary representation of every code point in the BMP that represents a character. UCS-2 cannot represent code points outside the BMP.
      这里说的很明白就是UCS-4是真正能够解决所有的UCS定义的code point:
      UCS-4 allows representation of each value as exactly four bytes (one 32-bit word). UCS-4 thereby permits a binary representation of every code point in the UCS, including those outside the BMP.
      我的疑问也许可以通过编码的比较事实上空洞的规定必须有具体的实现来承载,否则空谈code point让人无所适从,不如拿出干货让程序员明白具体的是什么。来回答,因为GB18030是中国标准,虽然兼容但不是相同,是有一些特殊的地方的。编码和标准是两个概念,任何一种标准都需要实现,可以简单的naive的直接用一个shorti nteger来实现UCS-2,也可以有很多其他编码比如UTF-8/16等等以及古老的其他编码方式。我觉得这个表有必要拷贝下来,因为它的信息量很大这个表里其实说的就很清楚了GB18030里把所有的英文字母数字标点符号又都重新定义了一遍,这个就是我天天打字最头疼的地方,有些时候看不清楚是汉字的标点还是ASCII的,在代码里有时候要费很大劲儿去除。

      Eight-bit environments

      Code range (hexadecimal) UTF-8 UTF-16 UTF-32 UTF-EBCDIC GB 18030
      000000 – 00007F 1 2 4 1 1
      000080 – 00009F 2 2 for characters inherited from
      GB 2312/GBK (e.g. most
      Chinese characters) 4 for
      everything else.
      0000A0 – 0003FF 2
      000400 – 0007FF 3
      000800 – 003FFF 3
      004000 – 00FFFF 4
      010000 – 03FFFF 4 4 4
      040000 – 10FFFF 5
      对于所谓的Seven-bit environments我不是很理解究竟指的是什么。 这里还有一个插曲,就是有人搞笑的提出所谓的

      UTF-9 and UTF-18 Efficient Transformation Formats of Unicode

      这个当然是“愚人节”笑话,但是从侧面来说编码有多么的无聊和机械,你完全可以似模似样的写出一份四平八稳的RFC不仔细看谁知道是搞笑?编码人人都会各有巧妙不同但是为人广泛接受的才是真正的好的编码。

      第一阶段就是编码的整理,可是怎么做我还是一头雾水,单单预处理的第一阶段就是这么的复杂啊。我的问题是我怎么知道是什么编码方式呢?应该说不可能也不需要知道不是我的责任去猜测或者尝试因为不可靠!,只有用户指定。

      怎么实现标准里说的很明白
      An implementation may use any internal encoding, so long as an actual extended character encountered in the source file, and the same extended character expressed in the source file as a universal-character-name (e.g., using the \uXXXX notation), are handled equivalently except where this replacement is reverted ([lex.pptoken]) in a raw string literal.
      这里重点是我之前感到疑惑的raw string的处理,算法是revert所作的mapping。这个是实现的细节,也许这个是说起来最容易的。即便是如此简单的任务也让人感到头疼!标准提出这个算法是有原因的,因为你不在全部处理完毕之前很有可能是无法准确定位raw string的吧?总之最简单的也是最可靠的。可是有时候所谓的是看上去如此而已,你难道要记录你怎么处理raw string的吗?有没有可靠的revert的方式?
    • 第二步似乎是一个简单的步骤,就是把认为分行的backslash去除掉。Only the last backslash on any physical source line shall be eligible for being part of such a splice.这里要求文件结尾必须是换行符,这一点让我想起了以前曾经为了把第三方的代码文件中去除window的换行符写的一个简单的shell命令结果导致编译器总是抱怨结尾没有换行符,我始终觉得很不解,原来症结在标准的要求。能不能这么理解就是经过第二步处理之后文件的结尾是使用这样子来标识的\n?但是这句话我始终不能理解Except for splices reverted in a raw string literal, if a splice results in a character sequence that matches the syntax of a universal-character-name, the behavior is undefined.这个是什么意思呢?我感觉英文还是难以理解,也许对于母语是英语的人群这个不言自明,可是对于我实在难以理解。什么是the behavior is undefined?是raw literal里反转之后符合\U hex-quad就造成了UB吗?还是相反?不明白。原来不止我一个人感到困惑,同样有人表示同感 这里的大牛指出了问题的原因是这篇论文。 应该是这个case:
      5.2 [lex.phases] paragraph 1, phase 2: a universal-character-name resulting from a line splice.
      我一开始看这个例子不明所以然,后来实际编译才看到玄机,比如这个字符串
      const char* p = "\\
      u0041";
      作者故意换行,那么根据以上定义必须把换行符前面的那个backslash\去掉可是因为是字符串内所以连同作为escape的backslash本身也去掉了,就是两个\\都去掉了,于是字符串成为了\u0041,就是说和universal-character-name一样了,打印出来的就是A,可是如果我们把换行符去掉,这个应该才是程序员的本意
      const char* p = "\\u0041";
      那么打印出来的就是原本的字符串\u0041换句话说这里的u在没有escape\的作用下是一个普通的字符。

      这中间的玄机我一时还是有些模糊不清。

      我为什么不理解呢?其实是因为没有看原来的文献,就是 the C99 Rationale大牛引用的C99 Rationale, section 5.2.1, in the part entitled “UCN models.”universal character name (UCN)这里c和c++委员会总结了三种模式
      • Convert everything to UCNs in basic source characters as soon as possible, that is, in translation phase 1.
      • Use native encodings where possible, UCNs otherwise.
      • Convert everything to wide characters as soon as possible using an internal encoding that encompasses the entire source character set and all UCNs.
      我即便不能深刻领会也能看出来三种方式都有隐患,没有完美的解决方案。这就是根源。
      Implementations, as well as the specification in a language standard, can employ any of the three, but it must be impossible for a well-defined program to determine which model was actually employed by implementation. The implication of this “equivalence principle” is that any construct that would give different results under the different models must be classified as undefined behavior.
      现在总算是crystal clear了吧?真的很不容易,我看了一早上才看到第二步,总共有步!
    • 第三步是preprocessing token的识别。其实不止是token,还包含众多的元素是我始料未及的,比如我没有想到这一步并不是处理preprocossor directive,也就是说仅仅识别并没有处理,所谓的tokenizer。但是复杂问题远远的超过我的预想,比如这个例子为什么是ill-formed raw string呢?
      #define R "x"
      const char* s = R"y"; // ill-formed raw string, not "x" "y"
      
      这个例子会编译错误:error: invalid character ' ' in raw string delimiter这个错误让人摸不着头脑。因为如果按照下面把R"看作raw string的开始,那么就不应该做宏替换那么为什么会有空格字符的错误呢?比如作为raw string里的。现在我使用这个g++ -E -fpreprocessed给出了一样的错误,这个-fpreprocessed
      Indicate to the preprocessor that the input file has already been preprocessed. This suppresses things like macro expansion,...
      所以结论是不存在宏替换了,但是为什么报错会提到这个空格字符呢?invalid character ' ' in raw string delimiter 比如这个d-char是不允许space的preprocessor应该在期待(作为休止符,所以我认为y会被当作d-char,但是...??? 值得注意的是我刚刚才意识到raw string的那个R和后面的"之间是不能有空格的,
      If the next character begins a sequence of characters that could be the prefix and initial double quote of a raw string literal, such as R", the next preprocessing token shall be a raw string literal.
      所以在preprocessing的时候whitespace是很重要的分隔符。
  2. 我看了看前言就知道这是一本武林秘籍,而且是那种缩减本的“精义”,可以速成的c语言内功心法,非有上乘功力不可修习。所以,我保存这个拷贝,因为重阳祖师曾经说过重阳派弟子皆不可修习这本秘籍。
    朋友,你知道C语言的精髓是什么吗?
    • Trust the programmer.
    • 不要像java一样把程序员当小孩子,没有保姆!
    • Don't prevent the programmer from doing what needs to be done.
    • 自由!自由!自由!重要的事情说三编!
    • Keep the language small and simple.
    • 举重若轻,大巧不工。
    • Provide only one way to do an operation.
    • 人无法做出正确的选择的原因是因为有选择。
    • Make it fast, even if it is not guaranteed to be portable.
    • C不是C++,不需要一致性与移植性。
  3. 这个视频是一个很有天分的少年的自述的添加很酷的编译器的feature的经历,让人感叹他的天分的同时也感到编译器的世界的复杂与残酷无奈。即便clang也是困难。

二月十一日 等待变化等待机会

  1. 我很羡慕这个工具网站,与是就下载cxxdraft-htmlgen看看能不能编译html版本,但是我对于haskell一点都不熟悉了,cabal有很多dependency不能解决,就尝试stack也遇到问题,后来stack upgrade了一个local版本,所以要使用最新的版本才行,ubuntu官方版本有问题。比如我的stack安装在这里,然后。。。我无法成功编译,算了直接使用编译好的版本吧!
  2. 昨天看了大半天我也不理解这个第三步,因为pp-token是怎么decompose呢?
  3. 这个古老的动态分配数组的方式让我想起了十年前被面试的问题。
  4. 孤陋寡闻啊,我虽然看到编译器自己编译的size executable却没有想到它居然是一个工具,难怪llvm要编译自己的size
  5. 再次审视这个疑惑,我是否应该比较一下这个先贤的c++11的语法呢?我记得我使用flex/bison好像是有问题就放弃了,究竟是什么问题呢?

二月十二日 等待变化等待机会

  1. 第三步不清楚的前提下看第四步。实际上这一步要复杂多了,各种各样的directive的操作实际上是非常的麻烦困难的,c++继承了所有c的丑陋与美丽的部分,这些宏的替换是在没有很强大的编译器与复杂语法的年代的程序员的创造,比如这个例子,我直接就要吐了。 看到这里我才开始明白第三步的意图,因为这里还没有任何的语法分析,也就是说在tokenize的一个早期步骤我们接下来的要做宏替换等等的复杂的步骤需要类似于“断句”的功能,否则我们怎么知道function-like-macro的replacement-list要怎么替换呢?换言之,第三步不是为了tokenize的最终结果至少是为了紧接着的第四步作准备。 看了这个例子就明白有什么必要定义preprocessing-token有这么多种了,总之我现在明白了为什么但是怎么还有很长的文档要读。
  2. The rational是这样子的,当你从不关心preprocessor的过程的时候,你完全意识不到这其中的繁琐与弊端,而这个时候就是了解机制实现的意义,所以,很自然的我现在来阅读module的革命性的进步不了解过去就不明白现在,不了解现在就不可能预测未来。 这里是module的语法的部分,我还完全不能理解。

二月十三日 等待变化等待机会

  1. 刚看了关于module的开头就犯困了。不过在这之前我已经知道这部分还没有完成,而且我需要把编译器升级到gcc-11.xxx,这个虽然不是问题,可是主要是eclipse可能又要重新indexing,而这个很可能再次导致问题。
  2. 在小憩之前我想再看一眼preprocessor的阶段,看来第五步重点是把字符串都统统转化为标准的universal-char-name这个形式。其中有意思的是char-literal和string-literal是两个概念。我以前怎么印象中char-literal是有长度限制的呢?是的,这个character-literal并不代表任意长度因为不应当把c-char-sequence看作是无限循环的。 我不妨把这个表收藏一下实际上我是手动重建了这个表,这个过程是一个很好的领悟学习的过程,因为之前实际上是有误解的。
    Table 9: Character literals [tab:lex.ccon.literal]
    Encoding prefix Kind Type Associated character encoding Example
    none ordinary character literal char encoding of execution character set 'v'
    non-encodable ordinary character literal int '\U0001F525'
    ordinary multicharacter literal int 'abcd'
    L wide character literal wchar_­t encoding of execution wide-character set L'w'
    non-encodable wide character literal wchar_­t L'\U0001F32A'
    wide multicharacter literal wchar_­t L'abcd'
    u8 UTF-8 character literal char8_­t UTF-8 u8'x'
    u UTF-16 character literal char16_­t UTF-16 u'y'
    U UTF-32 character literal char32_­t UTF-32 U'z'
  3. 读到第五步我才明白为什么之前的这个是UB,因为我们最终都是要把literal转化为universal-char-seq,那么究竟这个转化是已经发生了吗?嗯,也许我的理解还是不准确就是有三种实现模式但是结果是不同的,因为转化的时机点不同?我试图比较clang和gcc对于这个错误的raw-string的错误提示: const char* s = R"y";结果它们都报出了同样的错误但是clang要好很多因为它有高亮部分的错误提示,而g++没有。
    
    /usr/bin/clang-11 -E ill.cpp
    ill.cpp:1:22: error: invalid character ' ' character in raw string delimiter; use PREFIX( )PREFIX to delimit raw string
    const char* s = R"y"; // ill-formed raw string, not "x" "y"
                         ^
    
    但是g++的优点是那个^指的是很准确的,这个可以说是一个clang的不如人意的小bug吧?
  4. 第六步让我明白了原来null-terminated-string是preprocessor帮你加上了结尾的null字符。
  5. 现在知道要判断<到底是不是模板参数开始符到底有多难啊!
  6. 第七步还有可能包含模板的替换!这个该有多么困难啊!
  7. 这里看起来是一个很好的clang的介绍,也许这个是一个好的思路。这里有很多例子工程,看来这个方面clang是强项。

二月十四日 等待变化等待机会

  1. 我原来对于module的概念以为是简单的类似precompiled-header之类的简单的东西,现在粗粗一看就感觉这个是一个浩大的工程,因为它不仅仅是编译的问题更多的还有链接的问题,比如mangling的改变等等。这个部分太复杂了,暂时放一下吧。
  2. 首先从大图景来理解llvm toolchain
    • Preprocessor: This performs the actions of the C preprocessor: expanding #includes and #defines. The -E flag instructs Clang to stop after this step.
    • Parsing: This parses and semantically analyzes the source language and builds a source-level intermediate representation (“AST”), producing a precompiled header (PCH), preamble, or precompiled module file (PCM), depending on the input. The -precompile flag instructs Clang to stop after this step. This is the default when the input is a header file.
    • IR generation: This converts the source-level intermediate representation into an optimizer-specific intermediate representation (IR); for Clang, this is LLVM IR. The -emit-llvm flag instructs Clang to stop after this step. If combined with -S, Clang will produce textual LLVM IR; otherwise, it will produce LLVM IR bitcode.
    • Compiler backend: This converts the intermediate representation into target-specific assembly code. The -S flag instructs Clang to stop after this step.
    • Assembler: This converts target-specific assembly code into target-specific machine code object files. The -c flag instructs Clang to stop after this step.
    • Linker: This combines multiple object files into a single image (either a shared object or an executable).
    Clang provides all of these pieces other than the linker.
    clang可以使用的linker有这么些,让我惊讶的是,难道只有Linker才是体现操作系统的独特性吗?这一点仔细一想其实不言自明,不然为什么有wine来实现windows?所以linker实现了操作系统相关的部分。
  3. 编译器runtime究竟是什么概念呢?
    The compiler runtime library provides definitions of functions implicitly invoked by the compiler to support operations not natively supported by the underlying hardware (for instance, 128-bit integer multiplications), and where inline expansion of the operation is deemed unsuitable.
    能不能这么理解有一些和运行环境相关的操作比如浮点数大整数等等也许运行期可以使用特殊的cpu的指令集,我认为它们往往是需要优化的却不应该编译成中间代码的就是说每个平台必定有硬件支持的所以才不放在中间代码里。看到我理解正好相反,如果不是硬件支持为什么不可以放在中间代码里?嗯,也许我的理解有问题,这个是说编译器直接使用的部分,而这个应该是只有c++之类的语言才有的问题,就是说c++在c语言之上实现?所以有一些部分是特别实现的,比如我们使用regex忽略了这个实现部分是依赖于不同的平台的所以需要regex的动态库,所以,类似的在c++编译过程种也有一些被使用到的函数需要特别的实现。现实里它就是libgcc_s的代名词。
  4. 另一台笔记本的ubuntu-18.10想要升级到20可是不能正常升级,就尝试使用所谓的debian的方式把cosmic改为focal,而在这一刻我才真正明白LTS的意义:一定要stick with LTS否则升级维护会很麻烦,所以不值得从18.04升级到18.10因为以后会很啰嗦!
  5. 对于ABI我始终有模糊认识,难道这个仅仅是为了support functionality in the main Itanium C++ ABI吗?应该仅仅是文件和平台名字无关。实际上它是定义了c++实现的细节
    ...the object code interfaces between different user-provided C++ program fragments and between those fragments and the implementation-provided runtime and libraries. This includes the memory layout for C++ data objects, including both predefined and user-defined data types, as well as internal compiler generated objects such as virtual tables. It also includes function calling interfaces, exception handling interfaces, global naming, and various object code conventions.
    换句话说,在实现层面如果不同的c++模块要想“互联互通”必须要遵守一些基本的二进制码层面的规则,一个当然是对象的内存分布,这个直接影响到成员变量的偏移量;另一个就是异常处理和函数名字装饰,这个非常的重要,这个是一个约定成俗的规定,一旦确定要遵守才有可能不但做到向后兼容而且不同的c++库的兼容。因为这些部分和c语言也是无关的,纯粹c++的特性。这里我保存一个本地拷贝
  6. 我因为需要更新bios所以制作UEFI启动盘是一个很麻烦的事情,发现这个网站非常的棒,先收藏一个UEFI bootable USB的制作方法,另外,我需要下载一个Win10_1809Oct_English_x64.iso。这里对我最有用的部分是最后win10-UEFI启动文件的部分,至于说前面分区我使用fdisk手动也可以的,关键是sources/boot.wim要放在启动分区里,当然这里我没有意识到除了sources目录外的部分也要拷贝到启动分区,她使用7z的命令行include/exclude确实很巧妙,这个是比loop更好的办法吧?此外关于linux的UEFI启动我还是不很确定,我以前就是直接把ubuntu的iso直接dd到usb似乎它是已经配置好了支持UEFI/LEGACYl了?当然作者使用7z的明显的好处是不用sudo权限去loop或者dd设备。在没有查看UEFI文档的情况下我猜测efi启动的要点就是FAT32磁盘分区下一个EFI文件夹的EFI后缀文件名的支持efi启动的文件吧?

二月十五日 等待变化等待机会

  1. 这个帖子看样子有很多的insight,我知道HP下载的可执行文件spxxxx.exe是一个archive self-extractor文件,我以前只知道使用wine来模拟运行,现在知道可以直接使用7z来extract这些exe。而且解开的exe本身还可以使用7z再次解开。我怀疑这是windows下的把别人的可执行程序做到资源文件来运行的做法,我很早以前就这么做过,很明显的因为这些功能都不是HP的技术,是从第三方InsydeFlash的工具和文件,所以你可以使用7z直接解压缩出来,因为HP就是做了一个壳子包装资源文件而已。这个是我以前没有想到的。另外长按F2进入到管理界面不知道是否被禁止了,而这里提到的winkey+b恢复biso是否前提是我的HP的隐藏分区还存在呢?这里我们可以看到所有的bios的.bin文件还有很多很有趣的.efi文件,这些是dos下的可执行文件,连带的有一些对应的.s09/12/14文件,这些是所谓的数字签名文件,没有它们你应该无法突破bios内部的数字签名验证。这里有所谓的bios managemenet和update文件,你在一个特定目录 看样子挺好的,可是我胆怯了不敢真的去更新bios因为万一板砖了笔记本我就崩溃了。
  2. 昨天在llvm的网站看到一个最简单的编译gcc的脚本令人有了新的认识。这里在gcc自带的./contrib/download_prerequisites脚本自动下载所需要的那几个库很方便,但是它的不足之处在于环境变量没有清零容易出现很多不可预测的问题,此外我没有敢再编译因为make -j$(nproc)就是导致我的笔记本死机的一个原因,也是我想更新bios的根源,这个肯定是AMD CPU的bug,对于windows它肯定是尽心尽力支持的,所以,这个也是把windows笔记本跟改为ubuntu的一个潜在的危险。也许将来我应该购买官方支持linux的笔记本?
  3. 我刚刚看到这些帖子里提到可以boot with uefi file,我怎么就没有想到呢?这个也许根本就不需要制作uefi-bootable-usb,但是这个bios的功能是否存在呢?这个功能是存在的,我忘了是哪一个快捷键F6,7,8之类的,但是问题是insydeflash的那些.efi并不是bootable,所以,我还是需要一个uefi-bootable的usb,而且我怀疑efi本质是类似dos环境,所以,我制作的win10的usb安装盘不能运行这些efi文件的原因就是这个,最后我还是在这个win10的terminal下直接运行HP的那个资源包装的可执行文件来更新bios,但是这个过程实在是太惊险了我不想再尝试了,因为中间经历了很长时间的bios/pei更新过程看起来相当的复杂,最后重启之初我的ubuntu还是死机,后来高级选项启动才可以。这个到底能不能解决AMD的cpu的电源管理问题还是一个未知数。我检查了/proc/cmdline的启动参数发现我的grub不能再使用以前各种各样的奇怪参数,那些都是我google来的所谓workaround都是为了绕过电源管理和休眠的非解决方案。现在看来使用干净的无参数目前还能工作,也许说明了bios公司修正了问题,也许amd给了补丁,我之前工作就有过intel给补丁给bios厂商,集成厂商再把补丁放在bios里在启动之初的办法吧?
  4. 我发现这个表解释了很多关于llvm的编译流程,这个很重要因为它和gcc有着非常大的区别,它实际上是编译了中间代码,而这个给了它跨平台和优化的独一无二的能力,否则它怎么能够支持windows和各种虚拟机?这个和java/c#的思想很接近。
    Phase Inputs Outputs Options
    Preprocessing
    • Source Language File
    • Source Language File
    -E
    Stops the compilation after preprocessing
    Translation
    • Source Language File
    • LLVM Assembly
    • LLVM Bytecode
    • LLVM C++ IR
    -c
    Stops the compilation after translation so that optimization and linking are not done.
    -S
    Stops the compilation before object code is written so that only assembly code remains.
    Optimization
    • LLVM Assembly
    • LLVM Bytecode
    • LLVM Bytecode
    -Ox
    This group of options controls the amount of optimization performed.
    Linking
    • LLVM Bytecode
    • Native Object Code
    • LLVM Library
    • Native Library
    • LLVM Bytecode Executable
    • Native Executable
    -L
    Specifies a path for library search.
    -l
    Specifies a library to link in.
  5. 我很多年不用windows居然不知道微软现在允许你自行下载window ISO!难怪到处有网站公开下载,原来是合法的。
  6. 这个帖子间接证实了我的猜想,所谓的.efi的确是可以UEFI bootable,其实就是让bios里的uefi boot来执行就行了,只不过我不确定这个F2快捷键是否被HP关闭了。
  7. 最后我的bios更新看样子暂时是成功的,至少暂时没有死机。
  8. 这里是关于一个很好的问题的回答:what/why is about LLVM?然后是一个新的动机与能力:
    New Feature: Link Time Optimization
    • Optimize (e.g. inline, constant fold, etc) across files with -O4
    • Optimize across language boundaries too!
    这个ppt是十几年前的文档,是否反映今天的现实呢?原来这个是作者当年的宣言啊。我找到作者Chris Lattner也许是最早的ppt。 作者反复提到SSA:

    static single assignment form

    In compiler design, static single assignment form (often abbreviated as SSA form or simply SSA) is a property of an intermediate representation (IR), which requires that each variable be assigned exactly once, and every variable be defined before it is used. Existing variables in the original IR are split into versions, new variables typically indicated by the original name with a subscript in textbooks, so that every definition gets its own version. In SSA form, use-def chains are explicit and each contains a single element.
    这里each variable be assigned exactly once对于同一个变量的多次赋值重用要给它们赋予独特的别名吧?这个从逻辑上来看是最清晰与容易管理的,只有古早时代才有节约变量名的“人为”优化,那个年代程序员以最少的代码与变量名来“炫耀”自己的头脑。如今编译器代替了人脑来做这个优化的工作了?下面这个解释其实倒是很直观的,听上去也不难。
    Converting ordinary code into SSA form is primarily a matter of replacing the target of each assignment with a new variable, and replacing each use of a variable with the "version" of the variable reaching that point.
    不过已经是新变量了何必还要“version”呢?彷佛在回到我的疑问一样wiki举例在使用这个变量的时候你不知道到底是哪一个"version"被使用了,如果有多条路径的话。这里又引入了Φ (Phi) function和所谓的“dominance frontiers"。好了,打住吧,我不是来学习编译原理的优化部分的。
  9. 我的感觉是llvm最初想成为gcc的一部分,现实可能不允许所以就只好独立发展了吧?看来我只能将来去写一些anecdote
  10. 现在看来这篇论文似乎才是最早的吧?其实我就看了看前言的感谢和目录,我最想知道的是LLVM代表什么?low level virtual machine为什么说
    The LLVM project has grown beyond its initial scope as the project is no longer focused on traditional virtual machines.
    怎么理解啊?
    LLVM and the GNU Compiler Collection (GCC) are both compilers. The difference is that GCC supports a number of programming languages while LLVM isn't a compiler for any given language. LLVM is a framework to generate object code from any kind of source code.
    无意中闯入这个OmniSci Render,据说是使用了LLVM。

二月十六日 等待变化等待机会

  1. 大约在20年前的2002年这篇硕士论文就已经规划了llvm的蓝图,实际上就已经有了雏形。其中可能是主要的三个优化技术
    The three techniques are link-time interprocedural optimization, run-time dynamic optimization, and profile-driven optimization.
    然后我们就只看标题
    Traditional Approaches to Link-Time Interprocedural Optimization: ...gather as much of the program together into one place as possible, increasing the scope of analysis and transformation beyond a single translation unit.
    • Very Low Level- Machine Code
    • Very High Level- Abstract SyntaxTrees(AST)
    Traditional Approaches to Run-Time Optimization: Run-time optimization and Just-In-Time (JIT) compilation are very common among the class of high-level language Virtual Machines (VMs). These VMs often target very dynamic languages ...use a machine-independent byte-code input which encodes these languages at a very high-level (effectively at the AST level). ...able to provide platform portability and security services in addition to reasonable performance.
    • High Level Language Virtual Machines
    • Architecture Level Virtual Machines and Dynamic Translators
    Traditional Approaches to Compile-time Profile-Driven Optimization: ...uses the estimated run-time behavior of the application to improve its performance (often by optimizing common cases at the expense of uncommon cases)
    • The first stage of compilation compiles the program, but inserts profiling instrumentation into the program to cause it to gather some form of profile information at run-time.
    • The second stage links these instrumented object files into an instrumented executable.
    • The third stage of profile-driven optimization requires the developer of the application to run the generated executable through a series of test runs, which are used to generate the profile information for the application.
    • Finally, the fourth and fifth stages recompile the program (often from source) and relink it, using the collected profile information to optimize the program.
    • this approach has many suboptimal features:
    • First, profile information is only useful if it is accurate: realistic programs vs benchmarks.
    • The larger problem, however, is that developers are often not even willing to use profile guided optimization at all because it is too cumbersome.
    怎么解决这些传统的问题呢?以前的人不是不明白而是感觉根本做不到,而作者提出的方案我们今天依旧觉得是天方夜谭,我感觉就算知道它已经实现了依然是天方夜谭的夸夸其谈,天才就是这样子做到常人认为不可能的奇迹!
    the static compilers in the LLVM system compile source code down to a low-level representation that includes high-level type information...At link time, the program is combined into a single unit of LLVM virtual instruction set code...This executable is native machine code, but it also includes a copy of the program's LLVM bytecode for later stages of optimization...The LLVM run-time optimizer simply monitors the execution of the running program, gathering profile information... either direct modification of the already optimized machine code or new code generation from the attached LLVM bytecode.
    也许我对于JIT的无知让我认为这是天方夜谭,但是在运行期不停地优化代码是否可以认为程序不再是一成不变的了呢?听上去编译永远在路上,程序的整个生命周期都在不停的优化,因为profile-driven意味着用户的使用也会改变程序的优化与执行。
  2. 也许我应该保留这篇论文一个拷贝 我以前对于llvm使用原系统的linker的原因依然不是很理解,也许是没有必要,或者不现实,因为你部署自己的linker相当于改变了整个操作系统的完整性,这个和自己创建虚拟机有什么不同?但是部署自己的虚拟机如jvm那样是对于用户的大的负担吧?但是要去动态修改代码那肯定是要部署自己作为虚拟机了吧?
  3. 看来中间码是放在elf的一个section
    LLVM bytecode is contained in a special section of the executable, so it is only paged into memory when and if accessed by the runtime optimizer.
    这段话的理解也许是打开大门的钥匙
    Key to the design of the LLVM virtual instruction set is the ability to support arbitrary source languages through a common low-level type system. Unlike high-level virtual machines, the LLVM type system does not specify an object model, memory management system, or specific exception semantics that each language must use. Instead, LLVM only directly supports the lowest-level type constructors, such as pointers, structures, and arrays, relying on the source language to map the high-level type system to the low-level one. In this way, LLVM is language independent in the same way a microprocessor is: all high-level features are mapped down to simpler constructs.
    但是我还是不太能够理解什么是all high-level features are mapped down to simpler constructs.
  4. LLVM拥有自己的所谓的虚拟指令集,他说它是first class language我查了一下定义
    A programming language is said to have First-class functions when functions in that language are treated like any other variable. For example, in such a language, a function can be passed as an argument to other functions, can be returned by another function and can be assigned as a value to a variable.
    是不是可以理解就是这里的函数都是不可再细分的所谓的“原子”,比如当你把运行期动态库的函数当作是first-class function的时候,这个语言就是first-class languange了,因为这里的虚拟指令集就包含了这些“原子”函数。
    LLVM provides an infinite set of typed virtual registers which can hold values of primitive types (integral, floating point, or pointer values). The virtual registers are in Static Single Assignment (SSA) form
    就是说为了实现这个所谓的SSA,你不能限制这种寄存器的数量。
    LLVM programs transfer values between virtual registers and memory solely via load and store operations using typed pointers
    然后,就和普通的c语言很像的内存分global/stack/heap等等的细节,我想这个设计的原理就在于这句话:
    The LLVM virtual instruction set is designed as a low-level representation with high-level type information.
    最大的特点是保留了高级语言的类型,同时尽可能贴近c语言。这个和它优化的原理有直接关系,因为它是要依靠SSA来把每一个变量作为一个“原子”来操作,所以,这里虽然是一个寄存器的指针但是代表的是一个类的实例,必须要保留它的类型信息才行吧? 至于说采用三地址码的原因就是这个实际上是RISC架构的必然选择,而且从根本上说现今Intel在移动设备的失败的根本原因是三地址码导致译码器的设计简单从而降低功耗,很明显的
    Three-address code can be easily compressed
    从长远来看指令集的设计肯定也是走这条道路,因为CISC的选择是一个早期的机会主义,在指令集本身长度导致用户内存占用实在是一个非常非常小的因素,除了在计算机发展的早期阶段。我记得一个例子就是教授说当时为了节省指令里用到的literal要去动脑筋以便少打几个纸孔,这个都是极端的例子,再比如3G手机刚出世的时候高通还有所谓的“彩虹码”似乎是把编译好的机器码再压缩以便节省手机运行内存占用,可是很快的手机使用的内存甚至超过了一般的台式机,这些机会主义的做法都是很短暂的。
  5. classes in C++ with inheritance and virtual methods can be represented using structures for the data values and a typed function table with indirect function calls for inheritance. This permits many high-level language-independent optimizations (e.g., virtual function resolution) to be performed on the LLVM code.
    这个似乎是不言自明的吧?
  6. 我们应该问一下自己为什么LLVM反复强调要保留type的信息呢?优化有时候是可以盲目的不需要知道类型的具体结构信息吧,比如每个类型都是不可分的,也照样可以做到优化,只要不同类型都不兼容就足够多的信息了。这里的一个特点是所谓的getelementptr这种非常critical的指令说明了保留类型信息的用途,因为LLVM和gcc的不同之处在于它可以做静态分析检查指针越界的问题,而这个address-sanity在gcc里是依赖另一个库来做的,而且似乎是动态运行的不是静态的吧?这一点我不确定,我的意思是我通常看到的内存错误都是在运行期报出来的,是否这些错误能够在编译期发现呢?llvm似乎自称可以吧?这个也许就是保留类型信息的一个应用场景作为静态分析。
  7. ...an LLVM program to be type-safe if no cast instruction converts a non-pointer type to a pointer type or a pointer of one type to a pointer of another type
    既然是类型导向,那么程序员的cast就是破坏这个系统的潜在问题,所以,排除这个隐患就是必须的。什么是安全的呢?因为POD就那么几个基本类型大家对于它们之间的cast是有着深入认识的,用户定义的数据类型是不允许cast的,因为指针类型不能cast,所以,就堵住了这个隐患。llvm完全可以依赖变量的声明类型因为没有中途改变的cast。 这里看到一个例子说明细节有魔鬼就是我们不允许任何非指针cast成指针,那么合法的指针运算怎么办呢?就非法了吗?比如int*p=&i; p++;这里p作为指针在计算地址累加的时候要先转化为整数然后再转回指针类型那么这个不就违反了以上的法则了吗?所以,这里使用这个特别指令getelementptr才能避免这种违约。
  8. The LLVM virtual instruction set is also unique in the manner it handles memory.In LLVM, all addressable objects (stack allocated locals, global variables, functions, and dynamically allocated memory) are all explicitly allocated, giving a unified memory model. Stack allocated locals (“automatic” variables and source-level alloca() calls) are all explicitly allocated using the alloca instruction. Heap allocated memory is allocated with the malloc instruction.
    就是说保留了变量的内存模型这个对于分析是至关重要的,java都是一种内存模型所以没有必要,但是llvm

二月十八日 等待变化等待机会

  1. 另一个现实我的无知的地方是我对于gold竟然一无所知。了解为什么lld比ld/gold都快的原因的同时补课学习Linker & Loader 另外这个是讲演的文档,其实对于这些细节并不是对大多数人有用,大家只想知道一个结论。我觉得我要补课的是这个关于gold的系列
  2. linker&loader是一个老生常谈的话题,我以为很多都是不值一提的,但是如果不明白背后的原理有些博客上的描述是难以理解的,比如
    ...PLT entries are normally relocated lazily by the dynamic linker...by default, the dynamic linker will not actually apply a relocation to the PLT until some code actually calls the function in question... In order to make this work, the program linker initializes the PLT entries to load an index into some register or push it on the stack, and then to branch to common code. The common code calls back into the dynamic linker, which uses the index to find the appropriate PLT relocation, and uses that to find the function being called.
    我当然理解就是以前看到的所谓的stub,在每个entry填写一个假地址就是所谓的类似于interrupt fault一样的做法,只有真的被调用了采取“解决”到达lazy的效果。我觉得还是要去看那本书,这个博客作为原理高屋建瓴的理解是足够了,但是不及细节的披露吧,至少现在还没有看到。不过话说回来博客不大可能严格的用代码图表来说明细节的马上打脸,我看到后面的GOT的细节完全看不懂了。我只好翻出以前的笔记来对照着看。 现在才明白作者的讲述过于高看读者的水平,对于我这样对于汇编不熟悉的人是看不懂这个函数是怎么得到当前instruction pointer的 这个“伪”函数是干什么的呢? 作者说了它等价于但是作者不做讲解以为我能看懂,其实它是利用函数压栈出栈来获得当前instruction pointer这么做的根本原因是
    There's no instruction to obtain the value of the instruction pointer on x86
    。此外如果你没有读懂上面的就不理解前面说的这一段话的结论怎么来的
    The program linker will create a relocation for the PLT entry which tells the dynamic linker which symbol is associated with that entry. This process reduces the number of dynamic relocations in the shared library from one per function call to one per function called.
    英语就是这样子的,一字之差蕴含了很多的意思,一目十行是很难理解这个差别吧,至少对于非英语母语的人来说。
  3. 虽然是作者但是不代表就是一个好的老师,他的博客我看的很吃力对于GOT因为用不上所以很不容易理解,看明白了过后也忘了。还不如看自己以前的笔记来的明白。算了我知道原理就好了,具体的汇编看的很吃力,不看了。
  4. For an ordinary defined symbol, the section is some section in the file (specifically, the symbol table entry holds an index into the section table). For an object file the value is relative to the start of the section. For an executable the value is an absolute address. For a shared library the value is relative to the base address.
    这个常识值得注意。
  5. For an undefined reference symbol, the section index is the special value SHN_UNDEF which has the value 0. A section index of SHN_ABS (0xfff1) indicates that the value of the symbol is an absolute value, not relative to any section.
    我忘了section的数目上限是不是就是0xffff
    A section index of SHN_COMMON (0xfff2) indicates a common symbol...used for uninitialized global variables in C
  6. Weak symbols come in two flavors. A weak undefined reference is like an ordinary undefined reference, except that it is not an error if a relocation refers to a weak undefined reference symbol which has no defining symbol. Instead, the relocation is computed as though the symbol had the value zero.
    这里就体现了我不明白weak的问题,以前看见同事使用attribute_weak,但是不明其所以然,现在就更糊涂了。
    On Solaris, a weak defined symbol followed by a non-weak defined symbol is handled by causing all references to attach to the non-weak defined symbol, with no error. This difference in behaviour is due to an ambiguity in the ELF ABI which was read differently by different people. The GNU linker follows the Solaris behaviour.
    什么意思呢?难道说我同时定义了weak和non_weak,那么最后就都算是non_weak?这么做的意义是什么?是为了避免ODR的错误吗?我以前的理解是weak是为了不淹盖系统的定义,比如如果运行期系统库函数有定义就使用系统的,如果没有才使用我们自己的,这个对不对呢?这个例子可能能够说明,但是相当复杂,我还没有看懂。 这个结论值得记录,这个回答的帖子太长了。
    • When a weak reference to foo (i.e. a reference to weakly declared foo) is linked in a program, the linker need not find a definition of foo anywhere in the linkage: it may remain undefined. If a strong reference to foo is linked in a program, the linker needs to find a definition of foo.

    • A linkage may contain at most one strong definition of foo (i.e. a definition of foo that declares it strongly). Otherwise a multiple-definition error results. But it may contain multiple weak definitions of foo without error.

    • If a linkage contains one or more weak definitions of foo and also a strong definition, then the linker chooses the strong definition and ignores the weak ones.

    • If a linkage contains just one weak definition of foo and no strong definition, inevitably the linker uses the one weak definition.

    • If a linkage contains multiple weak definitions of foo and no strong definition, then the linker chooses one of the weak definitions arbitrarily.

    基本的原则还是要理解的,就是.o是无条件的linking,而.a是选择性的
    When an object file is input to a linkage, the linker unconditionally links it into the output file.

    When static library is input to a linkage, the linker examines the archive to find any object files within it that provide definitions it needs for unresolved symbol references that have accrued from input files already linked. If it finds any such object files in the archive, it extracts them and links them into the output file, exactly as if they were individually named input files and the static library was not mentioned at all.

    这里又引出了一个我长久以来的困惑,我究竟要怎么对付.a?我目前编译指令是把.a当作.o一样的编译而不是链接,这样子是否是正确的呢?对于.a是否不应该这样子呢?archive多个.o而成的.a究竟是否会失去什么?可能不会,但是是否会多出了很多不必要的代码呢?这个可能才是我担心的吧?不过从上文来看即使一个函数的reference也是要把整个obj都拷贝过去的。所以,我觉得把.a当作和.o一样的输入是正确的选择,而不是传统的想法里把.a和.so等量齐观的做法。因为本质上.a和.so的链接不是一回事儿。这里引出的结论是这么做的后果是.o/.a的顺序变得非常重要。 很重要的一点就是如果在编译的时候一个函数被定义为undefined weak reference的话,随后linker压根儿不屑于再去resolve它,尤其是你的.a里有定义也没有用了,因为.a并不是绝对必须链接的,linker压根不需要去解决。这么做的好处是什么呢?我一点也看不出。gnu-gcc的定义是这样子的
    The weak attribute causes the declaration to be emitted as a weak symbol rather than a global. This is primarily useful in defining library functions which can be overridden in user code, though it can also be used with non-function declarations.
    就是说用户定义自己的overridden的函数不会造成链接时候的重定义错误。这个应该在动态库里才有用吧?静态库里是无效的。或者在声明的时候不能使用,只能在定义的时候才使用,这个是基本原则。
  7. 我编译llvm的时候因为看了文档反而被误导,使用-DLLVM_ENABLE_PROJECTS="..."总是编译失败,最后使用默认编译所有的projects才行。结果11.x版本编译还是失败,我不知道为什么这个项目连编译都不过?现在看来编译不过的原因可能是不支持并行编译吧。
  8. 关于symbol resolution这些因素起作用:
    • The symbol name
    • The symbol version.
    • Whether the symbol is the default version or not.
    • Whether the symbol is a definition or a reference or a common symbol.
    • The symbol visibility.
    • Whether the symbol is weak or strong (i.e., non-weak).
    • Whether the symbol is defined in a regular object file being included in the output, or in a shared library.
    • Whether the symbol is thread local.
    • Whether the symbol refers to a function or a variable.
    作者写的这个解决的算法相当的好,我是不是应该收藏一下呢?
    1. If A has a version:
      • If B has a version different from A, they are actually different symbols.
      • If B has the same version as A, they are the same symbol; carry on.
      • If B does not have a version, and A is the default version of the symbol, they are the same symbol; carry on.
      • Otherwise B is probably a different symbol. But note that if A and B are both undefined references, then it is possible that A refers to the default version of the symbol but we don’t yet know that. In that case, if B does not have a version, A and B really are the same symbol. We can’t tell until we see the actual definition.
    2. If A does not have a version:
      • If B does not have a version, they are the same symbol; carry on.
      • If B has a version, and it is the default version, they are the same symbol; carry on.
      • Otherwise, B is probably a different symbol, as above.
    3. If A is thread local and B is not, or vice-versa, then we have an error.
    4. If A is an undefined reference:
      • If B is an undefined reference, then we can complete the resolution, and more or less ignore B.
      • If B is a definition or a common symbol, then we can resolve A to B.
    5. If A is a strong definition in an object file:
      • If B is an undefined reference, then we resolve B to A.
      • If B is a strong definition in an object file, then we have a multiple definition error.
      • If B is a weak definition in an object file, then A overrides B. In effect, B is ignored.
      • If B is a common symbol, then we treat B as an undefined reference.
      • If B is a definition in a shared library, then A overrides B. The dynamic linker will change all references to B in the shared library to refer to A instead.
    6. If A is a weak definition in an object file, we act just like the strong definition case, with one exception: if B is a strong definition in an object file. In the original SVR4 linker, this case was treated as a multiple definition error. In the Solaris and GNU linkers, this case is handled by letting B override A.
    7. If A is a common symbol in an object file:
      • If B is a common symbol, we set the size of A to be the maximum of the size of A and the size of B, and then treat B as an undefined reference.
      • If B is a definition in a shared library with function type, then A overrides B (this oddball case is required to correctly handle some Unix system libraries).
      • Otherwise, we treat A as an undefined reference.
    8. If A is a definition in a shared library, then if B is a definition in a regular object (strong or weak), it overrides A. Otherwise we act as though A were defined in an object file.
    9. If A is a common symbol in a shared library, we have a funny case. Symbols in shared libraries must have addresses, so they can’t be common in the same sense as symbols in an object file. But ELF does permit symbols in a shared library to have the type STT_COMMON (this is a relatively recent addition). For purposes of symbol resolution, if A is a common symbol in a shared library, we still treat it as a definition, unless B is also a common symbol. In the latter case, B overrides A, and the size of B is set to the maximum of the size of A and the size of B.
  9. 关于动态库的版本号我上一次就没有很清楚

    Versions can also be used in an object file (this is a GNU extension to the original Sun implementation). This is useful for specifying versions without requiring a version script. When a symbol name containts the @ character, the string before the @ is the name of the symbol, and the string after the @ is the version. If there are two consecutive @ characters, then this is the default version.


二月十九日 等待变化等待机会

  1. 我对于llvm有一点点的困惑,为什么一个简单的编译总是失败,我开始怀疑是不是因为我已经安装了clang-11和gcc-7.5并存干扰了编译过程呢?这个听上去太搞笑了,但是从cmake命令报出使用c++ --print-resource-dir这个错误能不让人如此怀疑吗?这个选项是clang的,但是gcc对应的是--print-search-dir,我怀疑这个作为搜索本系统c++的头文件等等的动作是首先使用clang,但是这个取决于c++这个symlink是否指向g++还是clang++,不管怎么说者都是一个问题。那么一个成熟的项目为什么简单的编译总是出问题呢?
  2. 又一次浏览这部分文档,可是我的白痴问题是我编译完了llvm居然找不到clang在哪里?难道默认配置并没有enable-clang吗?可是我在参数里enable又编译出错,真的是莫名其妙。总之我现在不敢再使用并行编译结果编译速度非常的慢,也许慢工出细活吧?
  3. 终于google到这个错误,也许是因为llvm使用太多内存的原因,现在我决定试一下。 ~/Downloads/cmake-3.18.4/bin/cmake -DLLVM_ENABLE_PROJECTS="clang;clang-tools-extra;compiler-rt;libc;libclc;libcxx;libcxxabi;libunwind;lld;lldb" -DLLVM_ENABLE_SPHINX=ON -DLLVM_PARALLEL_LINK_JOBS=1 -DLLVM_USE_LINKER=gold -G "Unix Makefiles" ../llvm 编译clang花了好几天才成功,看来是内存的问题,当使用默认配置linking的时候经常会失败就是因为并行需要大量内存,所以只能在并行编译成功链接阶段失败的时候再次是用单线编译才链接成功。

二月二十日 等待变化等待机会

  1. 我刚刚才发现就算是不完整的编译llvm及其附带的这些projects最后编译目录下的磁盘大小居然是94G我开始怀疑之前编译失败是不是我的磁盘耗尽了呢?这么庞大的二进制文件是否真的很实用呢?是不是要把很多用户都吓跑了呢?因为我看到很多的编译好的二进制可执行程序都是一个G以上的大小,整个bin目录就有41G,这个让人真的是无语了。
  2. 我觉得这里的描绘的图景很不错可是可能都不会是现实了吧?至少我现在就没有看到。比如我仅仅想仿照lex的unttest里的例子来创建一个lexer,结果发现我几乎都要把所有的静态库都加进去现在还无法解决链接的问题。中间还夹杂着很多的噪音,比如这个“typeinfo”的未定义的问题,根本不是库本身的链接问题,而是编译指令不能实用rtti的问题 -fno-rtti,这个简直要把人逼疯了。而静态库的链接是如此的麻烦因为顺序有很大的关系,变成我经常要尝试不同的顺序,而clang的静态库又依赖于llvm的静态库,有的时候但看名字还猜不出来是哪一个目录下的静态库,因为有时候一个目录下还会一分为二甚至三,这个时候我只能通过ar t *.a来查找.o文件来判断是哪一个,这个时候又有一个问题就是nm -C *.a里看到的函数symbol必须看仔细了有时候一个虚函数可能有destructor但是没有constructor,你以为它就定义在这个库里面了,实际上是在别的库里实现的。这个时候粗略看类的名字并不管用,还是要更仔细。此外还遇到这些系统函数的定义问题比如setupterm未定义这个库-ltinfo我还是很多年前遇到过一两次的链接。总之,我对于llvm的大图景越来越表示怀疑,它的代码结构固然有不错的架构,可是它的文档似乎停留在很多年前了。
  3. 也许我走的路线不对,看看这里别人的例子也许有帮助吧?感觉这个Libtooling也许更加的合适?

二月二十一日 等待变化等待机会

  1. 虽然我编译了古老版本的llvm但是我无法实用enable_projects的参数激活相应的项目,cmake的设置对于我来说摸不着头脑。顺便说一下我总是忘记怎样让local的git去track 至少7.x版本支持设置target:-DLLVM_TARGETS_TO_BUILD=X86对于工具链的三个关键字LLVM_HOST_TRIPLE的不熟悉让我感到头疼。 这里还有一个下下策就是编译成动态库:-DLLVM_BUILD_LLVM_DYLIB=true。与之相对的是LLVM_BUILD_STATIC
  2. 如果静态链接遵循这个原则的话,那么在静态库的顺序上应当把最基础的库放在最后面,也就是有依赖关系的库被依赖的在后面才对。
  3. 我觉得如果一个人自称可以按照目录把项目编译成若干个静态库就实现了所谓的library-based的架构,我是不是应该冒天下之大不韪说这个是欺世盗名?如果不能编译成动态库那么所有的undefined symbol的resolve问题压根没有解决仅仅把.o文件攒在一起说是.a的分块简直就是骗人,因为如果不从代码的调用上严格的按照动态库的合约机制那么有没有编译成静态库根本就是空话,因为任何的.o文件都可以无条件的编译成.a文件对于用户来说完全没有帮助!也许我真的是该打脸,使用编译好的动态库链接是没有问题了,可是运行期呢?那些动态库每一个的文件大小都超过了一个G,LLVM的实用看来真的是虚拟机一样,太大了! 折腾了这么多天证明了一点就是所谓的llvm实际上并不是很适合开发的工具,它有它独特的用途,但是要作为一个替代gcc的某种工具实在是不容易,这么大的动态库要怎么分发啊?为什么没有人这么抱怨过?难道我是那个看见皇帝新装的小女孩?

二月二十二日 等待变化等待机会

  1. LTO的一些常识,我刚刚才读过LLVM关于LTO的介绍就又忘了。
    Link-time optimization(LTO) typically works on intermediate representation (IR) ...there are no more high-level language constructs, so link-time-optimization is language-agnostic.
  2. 从来没有阅读过gcc的安装文档。其中关于bootstrap我只是听说而已,现在来阅读一下官方文档:

    The bootstrapping process will complete the following steps:

    • Build tools necessary to build the compiler.
    • Perform a 3-stage bootstrap of the compiler. This includes building three times the target tools for use by the compiler such as binutils (bfd, binutils, gas, gprof, ld, and opcodes) if they have been individually linked or moved into the top level GCC source tree before configuring.
    • Perform a comparison test of the stage2 and stage3 compilers.
    • Build runtime libraries using the stage3 compiler from the previous step.
  3. 我对于bootstrap的方式不太理解因为没有找到所谓的binutils的文件,后来才发现了类似nm之类的文件,在每个stage吧?但是我不确定怎么判断是三个stage,后来在stage_current里写的stage3,我可以实用--disable-bootstrap编译但是怎么知道是否正确呢?所以手动make -k check 但是这里说的很明确,默认不是toolchain的话都是要做这个测试的,对于交叉编译特殊情况下也是可能的,除非可以运行目标程序,我想这个情况少之又少。
    For a native build, the default configuration is to perform a 3-stage bootstrap of the compiler when ‘make’ is invoked ...In special cases, you may want to perform a 3-stage build even if the target and host triplets are different. This is possible when the host can run code compiled for the target (e.g. host is i686-linux, target is i486-linux).

二月二十三日 等待变化等待机会

  1. 关于配置语言--enable-languages=lang1,lang2,… ,LTO为什么要和语言相提并论呢?难道Link Time Optimazation是一种特殊的语言吗?仅仅因为这个优化和语言无关结果就成为默认?
    LTO is not a default language, but is built by default because --enable-lto is enabled by default.
  2. 我无意中看到这个gcc的配置-fPIE,
    --enable-default-pie

    Turn on -fPIE and -pie by default.

    猜不出是什么缩写,google了半天才看到这篇非常棒的帖子。其中提到所谓的ASLR(Address Space Layout Randomization没想到ubuntu似乎默认是打开的因为我的cat /proc/sys/kernel/randomize_va_space结果是2

    The following values are supported:

    • 0 – No randomization. Everything is static.
    • 1 – Conservative randomization. Shared libraries, stack, mmap(), VDSO and heap are randomized.
    • 2 – Full randomization. In addition to elements listed in the previous point, memory managed through brk() is also randomized.
    换句话说就是我能够实用gdb全依赖于开始的时候gdb自动的帮我做了很多工作,这其中的机制非常的复杂我一时还领悟不了,大概就是

    By turning ASLR off (with either randomize_va_space or set disable-randomization off), GDB always gives main the address: 0x5555555547a9, so we deduce that the -pie address is composed from:

    0x555555554000 + random offset + symbol offset (79a)
    
    作者又自己发现了一个问题How is the address of the text section of a PIE executable determined in Linux? 这个问题就彻底超出了我的能力了,我只能知道这个是内核的一个设定值吧?
  3. 这一个选项说明了为什么gcc的错误信息可以是多语言的
    --enable-nls
    --disable-nls

    The --enable-nls option enables Native Language Support (NLS), which lets GCC output diagnostics in languages other than American English. Native Language Support is enabled by default if not doing a canadian cross build. The --disable-nls option disables NLS.

    结果我对于反复出现的canadian cross build感到困惑,原来这个比我想像的交叉编译还要多一些,我以前只是印象中是两个平台之间的交叉编译就是目标和结果编译器是同一个平台。
    The Canadian Cross is a technique for building cross compilers for other machines. Given three machines A, B, and C... When using the Canadian Cross with GCC, there may be four compilers involved
    • The proprietary native Compiler for machine A (1) (e.g. compiler from Microsoft Visual Studio) is used to build the gcc native compiler for machine A (2).
    • The gcc native compiler for machine A (2) is used to build the gcc cross compiler from machine A to machine B (3)
    • The gcc cross compiler from machine A to machine B (3) is used to build the gcc cross compiler from machine B to machine C (4)
    这里联想到之前的--enable-bootstrap的意义就是要求binutils这些工具都编译好了在各个stage的目录里,这个和交叉编译是有紧密关系的
    GCC requires that a compiled copy of binutils be available for each targeted platform. Especially important is the GNU Assembler. Therefore, binutils first has to be compiled correctly with the switch --target=some-target sent to the configure script. GCC also has to be configured with the same --target option.
    这里的细节一直对我都是模糊的,比如
    Cross-compiling GCC requires that a portion of the target platform's C standard library be available on the host platform. The programmer may choose to compile the full C library, but this choice could be unreliable. The alternative is to use newlib, which is a small C library containing only the most essential components required to compile C source code.
    这个newlib对我来说就是陌生的,我以前听说过所谓的alternative c library,不知道那些embeded用的是否就是这个newlib? 另一个细节我始终模糊就是gnu的这三个term。也许阅读这里更清楚一些吧。
    --build=build

    The system on which the package is built.

    --host=host

    The system where built programs and libraries will run.

    --target=target

    When building compiler tools: the system for which the tools will create output.

    换句话说,build/host/target好想祖孙三代的意思。
    The names host and target are relative to the compiler being used and shifted like son and grand-son.
  4. 比较有趣的小细节是这个GCC_COLORS 我的系统里的值是
    error=01;31:warning=01;35:note=01;36:caret=01;32:locus=01:quote=01
    这个是不言自明的吧?
  5. 在很久以前我在十二月一日/十二月二日使用过这个小技巧,就是把资源放在代码里,今天又看到类似的文章复习一下这个.incbin。 那个时候我因为声明变量是char还是char*整整的折磨了两三天吧?现在总结一下做法,基本上还是重复我以前的做法: 但是作者有一个更好的办法就是把数据放在一个指定的“段”里,然后这个section的全部长度就是数据的大小了,这个可以直接实用嵌入式汇编实现就不用单独设定一个汇编文件了 可以说这篇博客非常的棒!
  6. 大师的这篇文章我还没有精力去看。一个主要原因我发现它的最大的厉害之处是作为编译器的插件,否则原始代码应该不会有多么的复杂吧?这种elf-reader之类的程序我记得有很多只是繁琐,要写的适合所有平台不容易。我还是肤浅,大师的程序有很多高深的检查比如说这个安全性的问题我是一无所知。
    The program must not have a stack in an executable region of memory.
    这个是怎么检查的?
  7. 原来cpp代表的是C Preprocessor这个明天再读吧?

二月二十四日 等待变化等待机会

  1. 对于cpp要从头开始学习,首先我才意识到平行于c++标准的c标准有这么一些 我以前模糊记得c++委员会的一个重要议题是尽量和c标准兼容,看来标准的年线也是相关的吧?
  2. 是否有人直接调用cpp呢?或者说可以吗?是否有这可能呢?
    You can invoke the preprocessor either with the cpp command, or via gcc -E.
    当然是可以的了,只不过cpp的运行依赖于cc1/cc1plus取决于所选的语言:-x c/c++它们的关系是怎样的我还不理解,我看到libcpp完全可以实现所有的功能。那么cc1是前端的driver吗?我的猜想是之所以有这么多层的嵌套是因为gcc支持多种语言,所以它的前端的可执行程序也需要这样子的多层吧?
    The cpp options listed here are also accepted by gcc and have the same meaning. Likewise the cpp command accepts all the usual gcc driver options...
    换言之就是gcc -E直接呼叫的是cc1或者cpp,现在它们之间的关系我还不清楚。 我对于这个过程还是不明白决定对比一下cpp的调用是怎样的strace cpp -x c++ -E /tmp/mytest.cpp -o /tmp/mytest.cc1 这个让我开始怀疑它们是不是完全一样的binary,果然如此,我怎么一开始没有想到呢!!!
    nick@nick-HP-Laptop:~/tmp/gcc-11-20210214_source/gcc-11-20210214/gcc$ ll /usr/bin/x86_64-linux-gnu-gcc-7
    -rwxr-xr-x 1 root root 1047488 12月  4  2019 /usr/bin/x86_64-linux-gnu-gcc-7*
    nick@nick-HP-Laptop:~/tmp/gcc-11-20210214_source/gcc-11-20210214/gcc$ ll /usr/bin/x86_64-linux-gnu-cpp-7
    -rwxr-xr-x 1 root root 1047488 12月  4  2019 /usr/bin/x86_64-linux-gnu-cpp-7*
    
  3. 忙活了一个早上知道了这么一个无聊的常识,实在是让人无语,我怎么就不知道呢? gcc/g++/cpp是三个同样的binary,并不是symlink指向,而是identical的binary。这个真的是无语啊。
  4. 再一次把这个c++语法网站标记一下以便以后容易搜索
  5. 我感觉目前preprocessor里我最最糊涂的地方就是module部分,因为从来没有用过而且是c++20的新玩意,所以始终不理解是怎么实现的。不过尽管看不懂代码libcpp支持module是确定无疑的。
  6. 找到了一个好办法在gcc/doc下看到了关于libcpp的内部文档,使用texi2html脚本把它从texi转为html,其中需要一个参数texi2html -I include cppinternals.texi,这里我保存一个版本。此外我简单看了一下gcc的git log看来这个库是在持续更新的,只不过它有内外接口,对外接口无人使用而已。知道了这一点我感到很高兴,因为有时候你抱着很大希望结果遇到一个僵尸项目让人很沮丧。

二月二十五日 等待变化等待机会

  1. 朋友,你听说过collect2吗?我时不时会看到这个名字却从来不知道它是干什么的。直到我看到这个内部文档实际上我是在gcc/doc下把texi转为html后才看到的。
    The program collect2 works by linking the program once and looking through the linker output file for symbols with particular names indicating they are constructor functions. If it finds any, it creates a new temporary ‘.c’ file containing a table of them, compiles it, and links the program a second time including that file.
    这个是一个什么样的骚操作啊?难道你的代码里没有constructor吗?难道这个constructor的代码不会被翻译成机器码吗?下面这个解释了__main-nostdlib
    The actual calls to the constructors are carried out by a subroutine called __main, which is called (automatically) at the beginning of the body of main (provided main was compiled with GNU CC). Calling __main is necessary, even when compiling C code, to allow linking C and C++ object code together. (If you use -nostdlib, you get an unresolved reference to __main, since it’s defined in the standard GCC library. Include -lgcc at the end of your compiler command line to resolve this reference.)
    为什么linking要这么大费周张呢?为什么一次做不完要再来一次呢?是否。。。且慢,这个不是产生constructor的机器码,而是要在main运行起来之前先把所有用到的constructor先调用安排妥当,相当于initialization,我记得很久以前我曾经痛苦的debug经历,当时的程序还没有启动就直接退出了,最后落实到一个类的constructor,但是gdb是无法设断点在那个constructor的,为什么我也忘了,总之在进入main之前程序就退出了,看来就是这个main之前的__main,我唯一有印象的是最后只有在preprocessor下我看到了代码的宏展开有问题,因为无法使用gdb所以只能从代码去debug,这个真的是很痛苦的过程。
    这里也呼应了另一个事情就是它是libgcc紧密相联系的,至少这个__main就是libgcc定义的,不过文档里一句话都没有提到,看来在大神的眼里只有浮点运算,数学计算以及奇奇怪怪的__builtin_xxx函数才值得它们提到,真是事毕拂衣去,深藏功与名啊。

    TL;DR

    以下似乎都是无关的探索发现。。。
    • 然而当我使用-nostdlib链接的时候遇到了/usr/bin/ld: 警告: 无法找到项目符号 _start; 缺省为 0000000000000350的警告,我很多年没有遇到这类问题了,google才想起来libc的入口是_start,正常编译后看到的这个__libc_start_main@@GLIBC_2.2.5大概就是c程序的入口吧?
    • libgcc的代码文件名就能够看出文档所言不虚,那么我们直接看crtstuff.c就很容易找到线索__do_global_ctors (void)
    • 然后在libgcc的主入口程序libgcc.c里看到 这样子够清楚了吧,在libgcc里定义了这么一个__main入口函数,而这个居然支持反复调用main的保护机制,令人震惊,难道是gdb之类的反复调用吗?那么__do_global_ctors做了些什么呢?首先,它调用了内部个__do_global_ctors_aux,具体原因注释里解释了什么什么crash之类的。
    • 其次,这个实际干活的__do_global_ctors_aux 唯一让人不确定的是__LIBGCC_TEXT_SECTION_ASM_OP__,但是也和我猜的差不多就是 #define __LIBGCC_TEXT_SECTION_ASM_OP__ " .section .text, "axU"",最后当然是在程序结尾atexit的handler注册了相应的dtor了。
    • 那么这个body做了什么呢?让人吃惊的在这里 你是不是无语了呢?就是你不知道哪里搜集的一堆的ctor然后一个一个初始化,这个就是应证了collect2干的好事。那么你想知道__CTOR_LIST__是什么吗?或者说怎么来的,那么看看collect2.c就知道了,它实际上就是直接写了一个临时的c程序里面是这么一个__CTOR_LIST__的数组。
    那么collect2在哪里呢?
    The program collect2 is installed as ld in the directory where the passes of the compiler are installed.
    什么叫做real-ld,我有吗?有的,我的ld是在这里/usr/bin/x86_64-linux-gnu-ld.bfd和它并排的是x86_64-linux-gnu-ld.gold,这个是"real-ld"。那么我的gcc在哪里?它的伴侣collect2和它在一起吗?我的gcc是一个symlink最终指向了/usr/bin/x86_64-linux-gnu-gcc-7而昨天我已经直到它最终是要呼叫cc1/cc1plus的,它们在这里/usr/lib/gcc/x86_64-linux-gnu/7/cc1, /usr/lib/gcc/x86_64-linux-gnu/7/cc1plus,和它们排排座的是/usr/lib/gcc/x86_64-linux-gnu/7/collect2
  2. 不过当我费力的找了一通发现文档里说的比我清楚多了,估计这些东西google更容易找到。只不过纸上得来终觉浅
    The compiled code for certain languages includes constructors (also called initialization routines)—functions to initialize data in the program when the program is started. These functions need to be called before the program is “started”—that is to say, before main is called.
    这里就解释的很清楚为什么了?我之前还是想了半天才理解到这一层。
    To make the initialization and termination functions work, the compiler must output something in the assembler code to cause those functions to be called at the appropriate time. When you port the compiler to a new system, you need to specify how to do this.
    这段话怎么理解呢?因为loader只知道我执行可执行程序就是入口__start,至少这个是assembler的做法,那么你现在要求它在执行之前再做什么呢?我现在还是不确定我直到谁做了这个调用__main的工作。阅读这个部分非常的困难,因为相当的复杂,共有四种变数,取决于不同操作系统和可执行文件动态库格式的支持情况。对于我的问题有一个最简单的办法就是在用户的main里插入第一个__main来执行我们的ctor/dctor,但是这个是多么的intrusive啊,你要修改用户的程序代码吗?当然不是,只不过汇编码自己插入一些而已,没人会注意的,哈哈。但是如果系统和动态库格式支持这些section那就容易了吧。总之不是做移植工作的人估计很难真正理解其中的奥妙。
  3. 这个粉丝世界的确很大,最近看了一些粉丝自己制作的电影以及动画感觉挺不错的。
    无意中我看到了关于terminator是否能够self-terminate的讨论,哪怕是任务目标要求机器人隐含的执行自我毁灭的动作这个是不是违反了阿西莫夫关于机器人的三原则呢?
    
        John: (holds up the robot arm) Will this melt in there?
        Terminator: Yes, throw it in.
        John: Adios! (John throws the arm into the molten steel)
        Terminator: And the chip.
        Sarah: (in relief) It's over.
        Terminator: No. There is one more chip... (the Terminator points to his head)... and it needs to be destroyed, also. Here. I cannot self-terminate. You must lower me into the steel.
        John: No! You can't go! You can't go! No, it'll be okay, stay with us!
        Terminator: It has to end here.
        John: I order you not to go. I order you not to go, (yelling) I order you not to go! (he starts to cry)
        Terminator: I know now why you cry... (the Terminator wipes away John's tear) ...but it is something I can never do. (gives elevator controls to Sarah)
        Terminator: Good-bye. (Sarah lowers him into the molten steel. He gives a thumbs up as he is lowered)
        Terminator 2: Judgment Day
    
        T-850: You cannot self-terminate.
        John Connor: No, you can't. I can do anything I want. I'm a human being, not some god-damn robot.
        T-850: (correcting him) Cybernetic organism.
        John Connor: Whatever!
        Terminator 3: Rise of the Machines
        
        
  4. 这个是有一个HOMM的网站。
  5. 我昨天还在为这些term发愁它们的准确叫法,今天看到这个官方的说法
    There are three system names that the build knows about: the machine you are building on (build), the machine that you are building for (host), and the machine that GCC will produce code for (target). When you configure GCC, you specify these with --build=, --host=, and --target=.
    这个让我想起一个“三民主义”的翻译短语: 没有很一一对称,但是有点异曲同工之妙。
  6. 这里是具体的详情:
    • If build, host, and target are all the same, this is called a native.
    • If build and host are the same but target is different, this is called a cross.
    • If build, host, and target are all different this is called a canadian (for obscure reasons dealing with Canada's political party and the background of the person working on the build at that time).
    • If host and target are the same, but build is different, you are using a cross-compiler to build a native for a different system. Some people call this a host-x-host, crossed native, or cross-built native.
    • If build and target are the same, but host is different, you are using a cross compiler to build a cross compiler that produces code for the machine you're building on. This is rare, so there is no common way of describing it. There is a proposal to call this a crossback.
    这些理解起来有些抽象,后面的部分我看不大懂。
  7. 关于gcc安装修正头文件对应的在这里的fixincludes/README有非常复杂的介绍。还是远离这些吧,这个是平台的专家需要看的。对于我来说太复杂了,这个让我想起了编译内核之后要怎样设定能够作为内核的编译开发环境,有些是内核代码产生的头文件,有些是内核空间有些是用户空间,十分的复杂难懂。在ginclude下有这些头文件是要安装的
    float.h stdalign.h stdatomic.h stddef.h stdint-gcc.h stdnoreturn.h unwind-arm-common.h iso646.h stdarg.h stdbool.h stdfix.h stdint-wrap.h tgmath.h varargs.h

二月二十六日 等待变化等待机会

  1. 看完了感觉一头雾水,到底要用哪一种呢?似乎还是#ifndef是唯一的选择,那么何必要说#import和#pragma once呢?实践中我好像也遇到过最后一种不灵的时候,忘了是不是微软的编译器。总之我一开始看到import以为是module的新功能,
    #import’ is not a well designed feature. It requires the users of a header file to know that it should only be included once. It is much better for the header file’s implementor to write the file so that users don’t need to know this. Using a wrapper ‘#ifndef’ accomplishes this goal.
    这个让我感到混淆因为google到不少的地方称这个是gcc的deprecated feature,而且似乎语法中也看不到这个directive,我手动测试的确是这样子警告:
    warning: #import is a deprecated GCC extension
    既然如此为什么文档不把它删除或者提醒呢?
  2. 使用libcpp真的有看上去那么容易吗?我遇到第一个看似简单的问题就是include path的问题,我没有想到这么一个看似容易的问题搞得这么复杂,我还没有搞明白quote,bracket的优先的问题,还没有理解default/standard,或者对于用户传入的-Iprefix,结果就看到一些没有用的回调函数,我看得头都大了。
  3. 我对于头文件quoted还是bracket的区别寻求标准的解释,结果让我大吃一惊:实现者自己定义!这怎么可能呢?让我引用bracket的定义

    A preprocessing directive of the form

    # include < h-char-sequence > new-line

    searches a sequence of implementation-defined places for a header identified uniquely by the specified sequence between the < and > delimiters, and causes the replacement of that directive by the entire contents of the header. How the places are specified or the header identified is implementation-defined.
  4. 那么gcc是怎么定义的呢?

二月二十七日 等待变化等待机会

  1. gcc的实现不是什么金科玉律,标准也给了实现者自由去实现,但是首先保证uniquely就不是一件容易的事情。其次,为了和gcc兼容几乎是所有人的共识,这一点是不言自谕的,因为gcc就是工业标准,就算是其他编译器有些什么特点似乎随着时间的推移统一似乎是必然的。无意义的评论是无谓地,关键是事实。
  2. 我首先看到的是至少昨天的输出结果来自于四个分组的融合: 这个顺序至少是输出打印的顺序。详情在这里incpath.c:merge_include_chains
  3. 另一个基本原则是用户定义的路径优先级高于系统的。这个是所有的路径设定的原则。
  4. 此外的一个基本话题是如何比较两个目录是冗余的呢?依靠名字吗?还是依赖于inode?似乎gcc是依赖于后者,我在想这个就去除了symlink的烦恼吧?这虽然是一个细节可是值得注意的好东西。当然为了避免不同设备下的inode的冲突还是要同时比较设备号的。以前我没有意识到不同文件系统内的inode会冲突,最近才有些理解,还是得益于同学的孩子学习ext2fs的一些常见问题,我去看了一些资料的结果。 很有趣的话题是gcc是支持windows的吗?此话从何说起没有可能elf和pe兼容的,但是不要忘了一大票人是用cgywin在windows下的吧,究竟是否能够模拟文件系统我不确定,但是gcc文档反复提到的VMS据说没有numeric inode,所以,这里的代码是使用一个宏来做inode的比较拷贝等等的动作。所以针对windows压根不支持inode的directory的比较就是是一个宏转化为所谓的目录名字的canonical版本的比较了。这个都是细节,可是很有意义。
  5. 对于incpath.c:merge_include_chains的算法逻辑相当的复杂,我目前看的不是很理解,给定四个链表:quote,bracket,system,after如何做到去重呢? 关键是看这个函数的运用register_include_chains
  6. 这个时候我才意识到如果我编译gcc的时候配置恰当我是可以用gdb来看这些信息的,可是我似乎编译的时候配置有些问题吧?这里的查看debug_info的帖子很有用。
  7. 顺便说一下我对于我之前的bios更新还是很自豪的,就是它解决了我实际的问题,如果没有这个更新我如今编译gcc开8个进程肯定就死机了。重要的是解决了实际问题。
  8. 可是我之前编译的时候似乎因为没有--enable-gcc-debug导致看不到代码,现在我打算重新编译看看如何。我查看我上一个no-bootstrap的成功配置是configure --enable-static --disable-dynamic --disable-bootstrap --enable-languages=c++ --disable-multilib现在我加上了--newlib因为报出了configure-target-libstdc++-v3' failed很多时候着急就是找不到,这里的编译gcc的configure的官方网页。
  9. 之前一直失败也许是代码被污染了吧?这个是成功的配置
    ../gcc-11-20210221/configure CFLAGS='-ggdb3 -O0' CXXFLAGS='-ggdb3 -O0' LDFLAGS=-ggdb3 --prefix=/home/nick/tmp/gcc-11-20210221_install --disable-bootstrap --disable-multilib --enable-languages=c++ --enable-gcc-debug
    使用这个编译配置gdb就完全没有问题了。
  10. 编译gcc虽然是最简单的工作,可是其中还是有以两个小的窍门,否则就是费时费力,这是我总结的最简单的做法。
  11. 使用gdb gcc遇到了一个大的困难:怎么debug vfork的子进程呢?应该是一个常见的问题吧?还没有google,我猜想是gdb attach pid之类的吗?但是另一个问题是cpp呼叫的子进程代码似乎不在cpp里,似乎是用piple传递结果。我从makefile里看到incpath.c被编入了一个libbackend.a的静态库,gcc的架构相当的复杂。明天再看吧。今天忙了半天就忙了一个编译,我注意到gcc/c++和cpp的binary不一致,这个现象和我系统的gcc-7.5不一样,不知道是什么原因。

二月二十八日 等待变化等待机会

  1. 早晨起来找到一本很好的关于gcc的架构的图刚看了一个开头就解决了我很多的疑问,值得收藏。以下是一个常识,但是我似乎一直没有建立起来。
    kindpreprocessorc compilerc++ compilerlinkerassembler
    binarycppcc1cc1pluscollect2as
  2. 这里还有一个gcc的特别的输出很有意思
    gcc -print-prog-name=cc1
    /usr/lib/gcc/x86_64-linux-gnu/7/cc1
  3. 应该把gcc最基本的faq读至少一次吧?关于rpath我从工作第一天就建立了印象是一个坏主意,但是也许有一天是有用的,因为我有看到一些程序不希望你拷贝它的情况,比如从头编译源码的平台自然没有拷贝binary的问题,但是对于现在很多时候大家都是使用NFS文件系统确实是一个头疼的问题。这里提到gcc -dumpspecs是一个让人透视很多信息的地方,以后要多注意。
  4. 关于linker的设置是一个我不熟悉的地方,也许这个是古老的方法吧?collect2和real-ld的关系如何呢?
  5. 这里关于各种平台的细节超过了我的实践,但是有个概念也好,关于linker的设置是一个比较实际的话题。我看到--with-tls=dialect的第一反应是tls难道是Thread Local Storage但是马上意识到是Target Support Language我难道是自己跟自己开玩笑?TLS (Thread Local Storage)只有一种可能性吧?
  6. 这是一个很高级的话题,至少我看得是云山雾罩,这个是一个常见话题吗?从应用者的角度来看似乎是的,但是它的操作实在是的不容易吧?我自己是从拉没有dynamic_cast的需求,也许这些在类似windows COM这一类的接口才有这种吧?否则对于cv-less,你用const_cast不行吗?而且对于这里使用dl还要事先export global symbols from the executable by linking it with the "-E" flag
    If you use dlopen to explicitly load code from a shared library, you must do several things. First, export global symbols from the executable by linking it with the "-E" flag (you will have to specify this as "-Wl,-E" if you are invoking the linker in the usual manner from the compiler driver, g++). You must also make the external symbols in the loaded library available for subsequent libraries by providing the RTLD_GLOBAL flag to dlopen. The symbol resolution can be immediate or lazy.
    我看得不是很明白但是如果我理解正确的话这个做法使用dl+dynamic_cast似乎脱裤子放屁一样的笨拙,肯定有别的办法避免吧?也许我是孤陋寡闻吧?
  7. 我对于gcc的plugin还是不抱很大希望的,也许是因为LLVM的拥趸者们的辛辣批判和LLVM项目存在的理由之一。这个看来是编译的时候设定的,那么我的编译指令可能没有吧?不,尽管我的编译选项没有提到plugin这两个还是编译了:libcc1plugin.solibcp1plugin.so。但是我粗略的搜索代码感觉这个plugin已经废弃了。 gcc -print-file-name=plugin
  8. 关于GIMPLE,我看到这个名字很多次,似乎LLVM也提到过,现在才明白就是三地址码
  9. 我卡在一些llbiberty的函数,现在才理解它的用途。
    It was originally intended to be a sort of standard cross-platform library, ...However, the development of standards for C and POSIX took away some of the impetus for this, and libiberty came to be used primarily as a support library for the GNU toolchain. ... The name is a pun or word play on the word "liberty". On Unix-like operating systems, library files are always named "lib" + "the name of the library." But when they are linked to with a C compiler command (cc, gcc, etc.), the command line flag specifying the library is -l followed by the part of the library name after "lib". In libiberty's case it therefore becomes -liberty.
    这最后一段pun是今天早上最好的幽默。
  10. 运行完了gcc testsuite,但是要怎么理解contrib/test_summary的运行报告呢?

三月一日 等待变化等待机会

  1. 对于两个月之前的笔记我几乎完全不能理解,甚至怀疑我的数组从1开始的实现有问题,直到我重新编译运行才信服我的制作是正确的,但是如何理解又花了我将近二十分钟,并不仅仅是所谓的familiar template lambda官方名称是familiar template syntax for generic lambdas的概念的阻碍,而是对于integer_sequence这个模板神器的运用的再次领悟,现在才更加深入的领会在meta-programming里如何把实参转化为型参的妙悟。型参本来是模板中对于实参的引领,可是在meta programming里有时候你需要把实参牵引到型参来操作,那么怎么做到呢?只有领会到这一层才能理解integer_sequence的妙用。然而仅仅领会到什么和为什么并不能解决怎么办的问题,世间最难的往往也停留在实现的最后一公里,尽管开天辟地的革命式创举在人们心目中的地位是无比的荣光,彷佛那些金字塔的设计者往往立碑书传一系列的宏伟蓝图,但是对于怎么把一块块巨石从尼罗河搬运到塔顶的过程却语焉不详,这其中无数的奴隶肩挑手抗出力之余具体设计出了堆土成斜坡的构思却被僧侣们无视而不被记载从而淹没在历史的长河里成为谜题。这里的实现的最后一公里就是如此的微妙有时候仿佛就是一层窗户纸,你甚至已经可以看到对面婆娑的倩影却始终如梦魇般可望而不可及。这里我想仅仅把制作从一开始的数组做一个小小的扩展使之能够随心所欲的产生从任意数字开始的数列,听上去似乎就是一个很简单的玩意儿,也许我的驽钝竟然花费了超过半小时还无法想出来怎么使用familiar template lambda,结果只好作罢,也许不太可能吧? 这里是验证程序及其结果 最后还是google解决了问题,我怎么可能想到一个我完全不知道的操作符template operator()呢?这个是改进的类似oneliner的familiar template lambda的解决方案 结果当然是一样的了。
  2. 这一切暴露了我是否真的了解lambda,为什么我不知道有这个template operator()的调用呢?这个在语法里有体现吗?首先看看"Familiar template syntax for generic lambdas"的起源,论文提到的定义generic_lambda
    A lambda is a generic lambda if the lambda-expression has any generic parameter type placeholders ([dcl.spec.auto]), or if the lambda has a template-parameter-list.
    这个仅仅是基本的定义,那么关键的关键是理解lambda closure type,这里提到的这句话The function call operator template for a generic lambda can be an abbreviated function template我看了半天也不明所以然,再追踪下去看到这个例子我就吐了 天哪,这个lambda返回的是怎样一个lambda呢?
    它的调用更加让人称奇,究竟返回的是lambda还是lambda的运行结果呢?
    吐完之后擦了擦嘴还是要继续看 我尝试打印q/p的demangled name,结果意外的发现返回null。这里又一次引发了地震,首先,我已经忘记了我为什么要使用llvm下面的libc++abi.a我只有模模糊糊的记得我又一个特殊的需求要单独编译libc++abi为静态库,原因也许是这个gcc自带的库是动态库而它又依赖于一些其他的glibc的高版本的运行库吧?所以我debug的时候不想设定LD_LIBRARY_PATH直接运行,这个也许就是编译Libc++abi.a的原因吧,可是直接编译gcc的这个静态库我当时可能是没有找到办法,就借用了llvm自带的这个库,我当时的检查应该是说llvm并没有做什么改动仅仅是wrap了一下吧,也许是调用不同版本号的glibc?我记不得了,但是它的makefile能够顺利编译这个是最大的改进因为gcc下无法单独编译。然而现在我看到我的错误是null returned from __cxa_demangle_llvm让我怀疑这个是llvm版本的问题,于是我回归本来的abi::__cxa_demangle接口而不是llvm的__cxxabiv1::__cxa_demangle,但是同样的返回null,然后我使用boost自带的
    boost::typeindex::stl_type_index::type_id_runtime(t).pretty_name();
    依旧是null,也就是说vglambda返回的类型对于demangle函数来说是一个不可解的谜团,它的typeid::name()是这样子的
    ZZ7test361vENKUlT_E_clIZ7test361vEUlS_T0_T1_E0_EEDaS_EUlDpOT_E_
    ,我翻阅到了c++demangle的网页: 对于这个例子我一开始感到困惑因为我记得通常exception.what()都是一些human-readable的字符串,那么它怎么可以作为demangle的输入参数呢?
    这个std::bad_exception不是定义在<stdexcept>里的而是在<exception>里的,注释里有这样的提示See comment in eh_exception.cc.,这样子仿佛在探案里的线索一样我来到了eh_exception.cc 这里赫然写着 原来如此,它返回了它的类型,而附近的注释里这样写着
    // NB: Another elegant option would be returning typeid(*this).name()
      // and not overriding what() in bad_exception, bad_alloc, etc.  In
      // that case, however, mangled names would be returned, PR 14493.
    作者指的是这个注释吗?
  3. 我跋涉了无数的丛林与沟壑一无所获,回过头来再次审视auto p = vglambda( [](auto v1, auto v2, auto v3) 难道vglambda的类型也没有了?显然不是的,否则我早就注意到了这个问题,vglambda的类型是test361()::{lambda(auto:1)#1},那么我们比较一下它们的typeid看看吧:
    
    typeid(vglambda).name() ==>Z7test361vEUlT_E_
    typeid(p).name() ==>ZZ7test361vENKUlT_E_clIZ7test361vEUlS_T0_T1_E0_EEDaS_EUlDpOT_E_
    
    很显然的对于p这样一个庞然大物demangle感到力不从心了,也许这个是一个新的bug,因为generic lambda的发展超过了人们的想像吧?
  4. 战线拉的太长了因为我从一个点追踪引出另一个甚至多个点,然后不断的引出层出不穷的疑惑,比如关于bad_exception的定义原因的注解里有如下的话语
    If an exception is thrown which is not listed in a function's exception specification, one of these may be thrown.
    这个就是bad_exception的设计的使用场景,而对于这样一个空空荡荡的空定义异常的ctor的注释是这么说的
    // This declaration is not useless:
    // http://gcc.gnu.org/onlinedocs/gcc-3.0.2/gcc_6.html#SEC118
    于是我跟随这个链接来到了古老的gcc的文档,这里又一次提到了我之前困惑的vague linkage这里讨论的是三类情况inline functions,vtables and type_info objects,前两天我就没有看懂类似的部分甚至我怀疑这个是古老的实现细节也许过时了?我不确定,但是至少关于type_info我的理解是只有用到才定义吧?
    type_info objects
    C++ requires information about types to be written out in order to implement `dynamic_cast', `typeid' and exception handling. For polymorphic classes (classes with virtual functions), the type_info object is written out along with the vtable so that `dynamic_cast' can determine the dynamic type of a class object at runtime. For all other types, we write out the type_info object when it is used: when applying `typeid' to an expression, throwing an object, or referring to a type in a catch clause or exception specification.
    注意到了吗?有多态的类的type_info才会无条件的和它的vtable一起被产生,对于其他类型都是有用到才产生。那么回过头来我们看看vtable是否会被写入机器码呢?
    VTables
    C++ virtual functions are implemented in most compilers using a lookup table, known as a vtable. The vtable contains pointers to the virtual functions provided by a class, and each object of the class contains a pointer to its vtable (or vtables, in some multiple-inheritance situations). If the class declares any non-inline, non-pure virtual functions, the first one is chosen as the "key method" for the class, and the vtable is only emitted in the translation unit where the key method is defined.

    Note: If the chosen key method is later defined as inline, the vtable will still be emitted in every translation unit which defines it. Make sure that any inline virtuals are declared inline in the class body, even if they are not defined there.

    我的理解就是如果你第一个非纯虚函数没有定义的话就会导致整个虚表都没有产生。这个是否还是如此我需要做实验才能确定,我很担心gcc的文档年久失修,而且这个是很古老的版本的文档,我在最新的文档已经找不到还是看到类似的文字了。我认为如果定义为inline就不符合上面key method的定义了吧?所以很重要的要在class body里让编译器知道这个是inline不要把它作为key method,是不是这样子呢?我想这些都要做实验才知道,我现在不但是要吐还很饿。
  5. 要记下这个关于libstdc++的网址,因为我老是忘记找不到。感觉这个是我绝对必须认真阅读的文档!!!
  6. 对于libc++libc++abi这两个库我是在llvm里看到才引起注意,因为之前gcc有很多库都不明其所以然甚至不知道是不是和我的运行环境有关,这个帖子说明了一些基本情况,以下就是我的理解: libc++是实现c++ standard library,但是必须依赖于libc++abi(LLVM的实现)/libsupc++(gnu/gcc的实现)等等的所谓实现ABI的库的,之前我们已经有了ABI的概念,就是一些基本的c++实现层的要求,它的要求是类似“互联互通”,但是需要有平台相关的细节来保证实现,这个就是libc++abi和libsupc++的存在的原因。这里是一个非官方的libsupc++的网页: libsupc++ 我想找一找官方的网页看看,这个是官方的libstdc++v3的网页,我在gcc/libstdc++-v3/doc/html下面看到的就是这个文件。
  7. 费了不少力气才找到在平台相关的编译目录下才有这个libstdc++.(a|so),比如在我的编译目录x86_64-pc-linux-gnu/libstdc++-v3/,很明显的它是平台相关的,而且我的configure选项--enable-static --disable-shared要求它编译为静态库,我可以找到我需要的symbol: 所以,我今天解决了一个如何编译gcc版本的c++ABI的静态库的简单问题,不过不幸的是不能像llvm的libc++abi.a可以单独编译,目前我只能在整个gcc编译过程中编译它。现在就实验一下链接看看如何。结果还是一样,就是说我至少证明了我使用gcc-10.2.0原生的libstdc++库的__cxa_demangle依然无法正确的解析之前的这个复杂的lambda的mangled name,有机会我实验一下gcc-11看看。

三月二日 等待变化等待机会

  1. libstdc++的文档是值得阅读的,这里的prerequest对我来说很意外居然是locale的问题。比如之前关于source character set我就没有意识,现在渐渐明白这个是preprocessor里面一个很重要的问题,牵涉到你怎么处理用户字符串的大大的头疼的事情,上一次看到的所谓的UB就涉及到三个不同的处理策略。这里有一个列表是c++的named locale

    If the 'gnu' locale model is being used, the following locales are used and tested in the libstdc++ testsuites. The first column is the name of the locale, the second is the character set it is expected to use.

    de_DE               ISO-8859-1
    de_DE@euro          ISO-8859-15
    en_GB               ISO-8859-1
    en_HK               ISO-8859-1
    en_PH               ISO-8859-1
    en_US               ISO-8859-1
    en_US.ISO-8859-1    ISO-8859-1
    en_US.ISO-8859-15   ISO-8859-15
    en_US.UTF-8         UTF-8
    es_ES               ISO-8859-1
    es_MX               ISO-8859-1
    fr_FR               ISO-8859-1
    fr_FR@euro          ISO-8859-15
    is_IS               UTF-8
    it_IT               ISO-8859-1
    ja_JP.eucjp         EUC-JP
    ru_RU.ISO-8859-5    ISO-8859-5
    ru_RU.UTF-8         UTF-8
    se_NO.UTF-8         UTF-8
    ta_IN               UTF-8
    zh_TW               BIG5
    
    值得注意的是stdc++只在繁体中文环境下有测试,甚至于香港的英文环境也有,这一点是让人遗憾的。此外这里提到的测试如果以上locale不满足就会失败也是一个头疼的问题,但是貌似如果安装了所有的locale也是可以的。
  2. 其次使用的选项,这里我不熟悉的是一些特别的版本比如我把原来的表一分为二,后半部分我连听说都没有过。
    Table 3.1. C++ Command Options
    Option FlagsDescription
    -std=c++98 or -std=c++03 Use the 1998 ISO C++ standard plus amendments.
    -std=gnu++98 or -std=gnu++03 As directly above, with GNU extensions.
    -std=c++11 Use the 2011 ISO C++ standard.
    -std=gnu++11 As directly above, with GNU extensions.
    -std=c++14 Use the 2014 ISO C++ standard.
    -std=gnu++14 As directly above, with GNU extensions.
    Option FlagsDescription
    -fexceptionsSee exception-free dialect
    -frttiAs above, but RTTI-free dialect.
    -pthreadFor ISO C++11 <thread>, <future>, <mutex>, or <condition_variable>.
    -latomicLinking to libatomic is required for some uses of ISO C++11 <atomic>.
    -lstdc++fsLinking to libstdc++fs is required for use of the Filesystem library extensions in <experimental/filesystem>.
    -fopenmpFor parallel mode.
    -ltbbLinking to tbb (Thread Building Blocks) is required for use of the Parallel Standard Algorithms and execution policies in <execution>.
    比如那个exception-free的版本,因为据说On recent hardware with GNU system software of the same age, the combined code and data size overhead for enabling exception handling is around 7%. 对于怎么抑制异常处理使用这样子的宏还是挺聪明的做法,当然对于宏专家是不值一提,只不过我很难想到if(true)这样子的语句,因为违反习惯
    #if __cpp_exceptions
    # define __try      try
    # define __catch(X) catch(X)
    # define __throw_exception_again throw
    #else
    # define __try      if (true)
    # define __catch(X) if (false)
    # define __throw_exception_again
    #endif
  3. c++里有一个hosted/stand-free的概念我始终不是很清楚,感觉是和平台无关吧?
    这里开始了c++最重要经常被忽视的的之一,就是头文件的问题,这里我决定摘抄作为加深记忆的办法:

    TL;DR

    • 这里都是上个世纪的老古董,仿佛猿人还没有触摸monolith之前的时代
      These are available in the C++98 compilation mode, i.e. -std=c++98 or -std=gnu++98. Unless specified otherwise below, they are also available in later modes (C++11, C++14 etc).

      Table 3.2. C++ 1998 Library Headers

      algorithm bitset complex deque exception
      fstream functional iomanip ios iosfwd
      iostream istream iterator limits list
      locale map memory new numeric
      ostream queue set sstream stack
      stdexcept streambuf string utility typeinfo
      valarray vector  
      Table 3.3. C++ 1998 Library Headers for C Library Facilities
      cassert cerrno cctype cfloat ciso646
      climits clocale cmath csetjmp csignal
      cstdarg cstddef cstdio cstdlib cstring
      ctime cwchar cwctype  
      The following header is deprecated and might be removed from a future C++ standard.

      Table 3.4. C++ 1998 Deprecated Library Heade

      strstream
    • 世纪曙光初现,c++十年磨一剑在沉寂十多年后的的飞跃迎来了c++11
      These are available in C++11 compilation mode, i.e. -std=c++11 or -std=gnu++11. Including these headers in C++98/03 mode may result in compilation errors. Unless specified otherwise below, they are also available in later modes (C++14 etc).

      Table 3.5. C++ 2011 Library Headers

      array atomic chrono codecvt condition_variable
      forward_list future initalizer_list mutex random
      ratio regex scoped_allocator system_error thread
      tuple typeindex type_traits unordered_map unordered_set

      Table 3.6. C++ 2011 Library Headers for C Library Facilities

      ccomplex cfenv cinttypes cstdalign cstdbool
      cstdint ctgmath cuchar  
    • 经过了c++11的大跨越,c++14仿佛是一个小小的停顿与休整,这样的休整是必要的不单单开发者需要一个整理维护的间隔期,用户也需要一个时期的咀嚼与吞咽,休息是为了更好的跨越。
      This is available in C++14 compilation mode, i.e. -std=c++14 or -std=gnu++14. Including this header in C++98/03 mode or C++11 will not result in compilation errors, but will not define anything. Unless specified otherwise below, it is also available in later modes (C++17 etc).

      Table 3.7. C++ 2014 Library Header

      shared_mutex
    • 在经历了c++14的短暂的休整之后c++17果然迎来了又一个大的跨越,注意这里没有c语言对应的,要知道c语言相映c++似乎成为一个很稳定的语言了,仿佛上个世纪初经典物理发展到了顶峰时候,巨匠们对于它的要求一样:把物理常数测的更加精准一些就可以了,所以c语言也是进入了一个半休眠期。
      These are available in C++17 compilation mode, i.e. -std=c++17 or -std=gnu++17. Including these headers in earlier modes will not result in compilation errors, but will not define anything. Unless specified otherwise below, they are also available in later modes (C++20 etc).
      Table 3.8. C++ 2017 Library Headers
      any charconv execution filesystem memory_resource
      optional string_view variant  
    • 照例说经过c++17的比较大的跨越c++20似乎应该也是做一个短暂的休整,然而不是的,c++20似乎比c++17还要大的突破,难道说c++语言近年来有一种加速发展的趋势?实际上c++20远不止这些因为到目前为止c++20还是一个现在进行式,仅仅是早期收获而已,这里的内容不一定准确。
      These are available in C++2a compilation mode, i.e. -std=c++2a or -std=gnu++2a. Including these headers in earlier modes will not result in compilation errors, but will not define anything.
      Table 3.9. C++ 2020 Library Headers
      bit version
      The following headers have been removed in the C++2a working draft. They are still available when using this implementation, but in future they might start to produce warnings or errors when included in C++2a mode. Programs that intend to be portable should not include them. Table 3.10. C++ 2020 Obsolete Headers
      ccomplex ciso646 cstdalign cstdbool ctgmath
    • 这里我自己起名叫做c++11外传因为它是自从c++98开始就一直徘徊着想要登入c++家族的大门,但是出于种种原因,它们成为了偏房始终不给名份最后在一个所谓的TR/TS的文字游戏中消磨了青春,直到近些年才转正
      Table 3.11, “File System TS Header”, shows the additional include file define by the File System Technical Specification, ISO/IEC TS 18822. This is available in C++11 and later compilation modes. Including this header in earlier modes will not result in compilation errors, but will not define anything.
      Table 3.11. File System TS Header
      experimental/filesystem
    • 如果说文件系统是一个通房丫鬟那么这些TS的主力就是小妾了,它们的待遇确实很不公平。
      Table 3.12, “Library Fundamentals TS Headers”, shows the additional include files define by the C++ Extensions for Library Fundamentals Technical Specification, ISO/IEC TS 19568. These are available in C++14 and later compilation modes. Including these headers in earlier modes will not result in compilation errors, but will not define anything.
      Table 3.12. Library Fundamentals TS Headers
      experimental/algorithm experimental/any experimental/array experimental/chrono
      experimental/deque experimental/forward_list experimental/functional experimental/iterator
      experimental/list experimental/map experimental/memory experimental/memory_resource
      experimental/numeric experimental/optional experimental/propagate_const experimental/random
      experimental/ratio experimental/regex experimental/set experimental/source_location
      experimental/string experimental/string_view experimental/system_error experimental/tuple
      experimental/type_traits experimental/unordered_map experimental/unordered_set experimental/utility
      experimental/vector
    • 如果说TS还算是修成了正果的偏房那么TR连小妾都不如的外室怎么也看不到有一天能够被收编的希望呢?至少也不可能全体,偶尔的零星半点的被暗渡陈仓而已。
      In addition, TR1 includes as:
      Table 3.13. C++ TR 1 Library Headers
      tr1/array tr1/complex tr1/memory tr1/functional tr1/random
      tr1/regex tr1/tuple tr1/type_traits tr1/unordered_map tr1/unordered_set
      tr1/utility  
      Table 3.14. C++ TR 1 Library Headers for C Library Facilities
      tr1/ccomplex tr1/cfenv tr1/cfloat tr1/cmath tr1/cinttypes
      tr1/climits tr1/cstdarg tr1/cstdbool tr1/cstdint tr1/cstdio
      tr1/cstdlib tr1/ctgmath tr1/ctime tr1/cwchar tr1/cwctype
      Decimal floating-point arithmetic is available if the C++ compiler supports scalar decimal floating-point types defined via __attribute__((mode(SD|DD|LD))).

      Table 3.15. C++ TR 24733 Decimal Floating-Point Header

      decimal/decimal
    • 说完了TS/TR这些怨妇就开始很少人问津的ABI了,这个实在是很偏门,我也是最近才听说是用来打印类型名字,对于c++更像是某种艳遇
      Also included are files for the C++ ABI interface:

      Table 3.16. C++ ABI Headers

      cxxabi.h cxxabi_forced.h
    • 那么对于extension能说什么呢?我常常想这些是不是都是一些秘而不宣偷情史呢?
      And a large variety of extensions.
      Table 3.17. Extension Headers
      ext/algorithm ext/atomicity.h ext/bitmap_allocator.h
      ext/cast.h ext/codecvt_specializations.h ext/concurrence.h
      ext/debug_allocator.h ext/enc_filebuf.h ext/extptr_allocator.h
      ext/functional ext/iterator ext/malloc_allocator.h
      ext/memory ext/mt_allocator.h ext/new_allocator.h
      ext/numeric ext/numeric_traits.h ext/pb_ds/assoc_container.h
      ext/pb_ds/priority_queue.h ext/pod_char_traits.h ext/pool_allocator.h
      ext/rb_tree ext/rope ext/slist
      ext/stdio_filebuf.h ext/stdio_sync_filebuf.h ext/throw_allocator.h
      ext/typelist.h ext/type_traits.h ext/vstring.h
      Table 3.18. Extension Debug Headers
      debug/array debug/bitset debug/deque debug/forward_list debug/list
      debug/map debug/set debug/string debug/unordered_map debug/unordered_set
      debug/vector  

      Table 3.19. Extension Parallel Headers

      parallel/algorithm parallel/numeric
  4. 混合头文件是一个多么复杂的问题啊!我简直难以想象这个实现的可能的样子,比如一个c++98的functional和c++11的array并列在一起会怎么样?文档里说混合是不可能的,所谓的泾渭分明,那么向后兼容的原则吗?不,新的取代旧的,就是说要使用array必须使用-std=c++11 文档里最后一点就是TR1版本和正式版混合,比如tr1/type_traitstype_traits混合,我粗略看了看两个文件差别非常大,似乎前者是靠诸如SFINAE来实现的,而后者更多的靠的是编译器的进步吧?总之两者应该说是很不同的,居然放在一起没有事情,可见c++的维护工作量有多少。
  5. c++风格的头文件名自然是一个基本的素养,我自信已经养成了习惯,只是背后的原因不甚了了。其实也很简单吧,就是保证了c++的所有函数都得到了std namespace的保护,而相似名称的c函数在global并不会冲突,而且使用很多std::xxx的c++版本的相应函数会收到更高的收益,比如模板化的适应全部类型参数的overloading。
  6. 关于precompiled header这个可能是对于我最大的帮助,因为我自己的precompiled header就包含了大部分的stdc++.h里的头文件,但是在我目前的gcc安装机制只能使用这个目录<bits/stdc++.h>,因为这个是平台相关的目录x86_64-unknown-linux-gnu/bits/stdc++.h 另外的两个头文件stdtr1c++.hextc++.h我估计用到的可能性不大吧。
  7. 其他关于namespace/macro以及linking都有些不得要领,似乎很简单没有什么值得记录的,但是总觉得有些不安。这里有提到freestanding似乎都是c语言的
  8. 这里谈到如何缩减string的内存占用尺寸,
    std::string(str.data(), str.size()).swap(str);
    理解这行代码的妙处有佛手捻花而笑的感觉。

三月三日 等待变化等待机会

  1. 在古老的文档里常常有一些很有趣的校注里面有些意外的收获,比如这里提到如何自己制作一个caseinsensitive的string呢?这个也算是著名的题目了,是论坛里出现频率很高的,我模糊印象中几个月前我花了不少精力去研究现在都忘得一干二净了,唯一记得最好的解决方案是boost的locale,因为当时要考虑多字节语言的upper/lower是无意义的,但是貌似也是可以的吧?因为无所谓大小写就也可以,但是牵扯到排序还是需要引入locale,所以,这个问题完整的解决方案还是使用locale的相关函数比较字节,但是这个作者的方案对于西方语言是足够了。简而言之就是实现自己的char_traits,它和操作符==直接相关的就是compare方法,所以如果仅仅是实现它也算是一个投机的做法。 我看到了正确的实现方式在这里。当时我还是不太能够领悟。 至于说其他成员函数eq,le因为用不到就不实现了,我偶然google到了TR,标准委员会里对于一个小函数都有详细的论证。
  2. 牵扯出的这个c++的网站www.gotw.ca貌似很不错的样子,以后再看吧。忍不住看了一个guru of week就被教训了 这个代码行吗?答案是行!
    This is a C++ feature… the code is valid and does exactly what it appears to do.
    Normally, a temporary object lasts only until the end of the full expression in which it appears. However, C++ deliberately specifies that binding a temporary object to a reference to const on the stack lengthens the lifetime of the temporary to the lifetime of the reference itself, and thus avoids what would otherwise be a common dangling-reference error. In the example above, the temporary returned by f() lives until the closing curly brace. (Note this only applies to stack-based references. It doesn’t work for references that are members of objects.)
    这里的const起到了至关重要的作用,没有它是不行的!
  3. 我才意识到libstdc++的篇幅是相当的大的因为它基本是一个迷你版的c++ standard library reference手册,不过这个不是废话吗?这个文档不是手册是什么呢?我是说很多平常很少遇到的问题这里是作为常见问答来回答的,比如这个关于iterator的问答
    问题:怎样打印iterator的地址?这样子是不行的:ostream::operator<<(iterator)
    问题:如何清除iterator的引用?这样子是不行的: iterator = 0
    iterator = iterator_type();
    问题:如何判断iterator还是指向有效数据?这样子是不行的:if (iterator)
    if (iterator != iterator_type())
  4. 我以前对于namespace std的敏感度不高,认为是天经地义的,现在才意识到很多的微妙的地方,尤其在c/c++混合的古早年代,这里global namespace和std namespace有着很多的陷阱,这里介绍的把std搀合进global的方法我还一时难以领会。唯一能看懂的是在用户的namespace里个别std namespace的函数变得好像是global namespace里的函数,其中的用途也许是某些c语言的老手可以自由的替换掉c语言的函数为c++的函数,如果不用加前缀std::的话?
  5. 这里提到的.gdbinit的设定我还从来没有试过,它提到的gdb文档看样子不错。 我做了一个简单的实验比如我定义个两个类来展示多态: Derived:public Base并且都实现一个虚方法int func(int);那么Base* base=new Derived();我们打印base变量如何呢?在打开set print object on
    (gdb) p /r *base
    $4 = (Derived) {
       = {
        _vptr.Base = 0x555555754d70 <vtable for Derived+16>
      }, <No data fields>}
    现在gdb可以认出base的真实类型是Derived然后ptype的输出证实了这一点
    (gdb) ptype
    type = /* real type = Derived */
    struct Base {
      public:
        virtual int func(int);
    }
    这里又一个很有意思的地方我故意在定义类不使用class而是使用struct就是为了节省一个public关键字,结果ptype打印的时候还是替我加上了。 所以,我觉得这个.gdbinit还是挺有用的,决定采纳。
       set print pretty on
       set print object on
       set print static-members on
       set print vtbl on
       set print demangle on
       set demangle-style gnu-v3
  6. 这里提到的设定gdb pretty-printer的方法已经过时了,就是说现代gdb都已经集成了这个Python了,怎么知道呢?这个却是花了我不少时间才明白,没有比较就没有伤害,你甚至不知道远古时代的痛苦怎么明白今天幸福生活的来之不易,所以,我终于发现可以直接info pretty-printer显示当前已经有安装的printer,同时如果你一定要load自己的python-pretty-printer的脚本可以直接 source your-python-pretty-printer-path,但是最最白痴的是我理解错误使用p /r vect结果以为pretty-printer不起作用,后来才意识到直接print vect才是使用的方式,print /r vect不使用pretty-printer的方式。真的是出洋相啊。

三月四日 等待变化等待机会

  1. 对于这个老问题始终理解不够,似乎明白,似乎不明白,因为其中的继承与模板函数有些不清不楚。 这个为什么编译不过呢?
    std::transform(p_str.begin(), p_str.end(), p_str.begin(), std::toupper);
    注意这里的toupper是这个std::locale里的
    template< class charT >
    charT toupper( charT ch, const locale& loc );
    可是我们明明印象中有一个参数是char的overloading啊,不过它是声明在抽象类__ctype_abstract_base,而且它根本就是一个内部实现类不对外暴露的 不应当被使用的,因为它不自己实现而是依赖于继承类的实现do_toupper
    char_type toupper(char_type __c) const { return this->do_toupper(__c); }
    这个事情我想我之前已经花时间去探寻这个原因了,可是进一步的问题是为什么这样子就可以呢?
    std::transform(p_str.begin(), p_str.end(), p_str.begin(), [](char c){return std::toupper(c);});
    注意到我们调用的是同样的std::toupper而其中的参数是char这里的解释是什么呢?
    Note that these calls all involve the global C locale through the use of the C functions toupper/tolower.
    就是说它们是global namespace里的c版本toupper其中隐含的把char转换为int的过程,
  2. 我似乎以前来过这个很不错的c++专家的网站,我还是第一次意识到可以用using来重新声明被继承类覆盖了的祖先类的成员函数,如果编译器版本不支持这个语法那么重新声明一下也就可以了。
  3. 我已经编译了libasan.a的静态库但是链接却有问题,因为这里说
    Note that using explicit -lasan option has been widely discouraged by ASan developers (e.g. here) as it misses some other important linker flags. The only recommended way to link is to use -fsanitize=address.
    也就是说静态链接-static -lasan是走不通的,至少我的尝试是这样的,它需要dl/pthread,最后还是卡在了一个宏DYNAMIC的未定义上,与是我就放弃了。阿三是一个很不简单的库,目前我还没有精力去折腾它。
  4. 再次强调一下这个https://isocpp.org/wiki/faq/超级问答网站。
  5. 为什么一个字符串的语法我总是记不住呢?这里写下一万遍!!!
    ${var#Pattern} Remove from $var the shortest part of $Pattern that matches the front end of $var.

    ${var##Pattern} Remove from $var the longest part of $Pattern that matches the front end of $var.

    ${var%Pattern} Remove from $var the shortest part of $Pattern that matches the back end of $var.

    ${var%%Pattern} Remove from $var the longest part of $Pattern that matches the back end of $var.

  6. 那么strtok相对应的函数是什么呢?我竟然一时想不到是什么了?难道现在我们还是要自己这么实现一下吗?虽然这段代码挺美的。似乎是证实了我的印象,c++在这个问题上是两难的,核心的问题是返回的容器是不是要成为一个模板参数?这个问题我还没有意识到。所以作者说c++是以iterator为核心而不是以容器为核心的,所以,通常都是不会把容器作为模板参数的吧?这个是c++标准不提供的原因吗?相比其他语言是一个让任何非专家感到懊恼与不安的。这里有一段很漂亮的使用regex来tokenize string的代码,我不禁仰慕而摘抄下来
    另一个很漂亮的做法是依赖于一个不能自定义delimiter的做法,我模糊印象中我曾经研究过这个机制但是已经记不清了,重新看代码发现这个是异常的复杂完全不可能用户自定义实现的。
    这一段代码的strtokenize的能力来源于哪里呢?究竟这个operator>>是哪一个类的呢?我找了很久我认为是basic_streambuf(文件istreambuf)的一个friend函数
    这个函数在文件istream.tcc,我目前对于c++ std lib的实现文件类型还不明白,常常看到这类模板文件,它们是模板产生的源文件吗?

三月五日 等待变化等待机会

  1. 一个人看不懂代码那么至少能看懂文档;一个人看不懂文档,至少看得懂FAQ;一个人看不懂FAQ,至少看得懂视频;一个人看不懂视频,至少看得懂头条。
    我现在处于看FAQ的阶段。What's libsupc++?,我的印象和libgcc的部分有些混淆了,后者应该是一些gcc-builtin的函数和数学计算相关的东西,这个是特别关于c++的ABI的部分,这个问题我记得我已经讨论过了。

    TL;DR

    If the only functions from libstdc++.a which you need are language support functions (those listed in clause 18 of the standard, e.g., new and delete), then try linking against libsupc++.a, which is a subset of libstdc++.a. (Using gcc instead of g++ and explicitly linking in libsupc++.a via -lsupc++ for the final link step will do it). This library contains only those support routines, one per object file. But if you are using anything from the rest of the library, such as IOStreams or vectors, then you'll still need pieces from libstdc++.a.
    它是c++语言的基本支持部分的函数:
    • Fundamental Types和c语言一样的十四个基本基本类型POD:
      These fundamental types are always available, without having to include a header file. These types are exactly the same in either C++ or in C.
      C++ has the following builtin types:
      • char
      • signed char
      • unsigned char
      • signed short
      • signed int
      • signed long
      • unsigned short
      • unsigned int
      • unsigned long
      • bool
      • wchar_t
      • float
      • double
      • long double
      这个是c++标准委员会制定的标准,那么查看limits文件里的注释有这么些说明
      // The numeric_limits<> traits document implementation-defined aspects
      // of fundamental arithmetic data types (integers and floating points).
      // From Standard C++ point of view, there are 14 such types:
      //   * integers
      //         bool                                                 (1)
      //         char, signed char, unsigned char, wchar_t            (4)
      //         short, unsigned short                                (2)
      //         int, unsigned                                        (2)
      //         long, unsigned long                                  (2)
      //
      //   * floating points
      //         float                                                (1)
      //         double                                               (1)
      //         long double                                          (1)
      //
      // GNU C++ understands (where supported by the host C-library)
      //   * integer
      //         long long, unsigned long long                        (2)	
      	
      所以long long, unsigned long long是GNU c++的对于基本类型的扩展支持。 但是实际上这个是不对的,因为最新的c++标准实际上还支持新的char8_t, char16_t, char32_t
    • Numeric Properties:
      The header <limits> defines traits classes to give access to various implementation defined-aspects of the fundamental types. The traits classes -- fourteen in total -- are all specializations of the class template numeric_limits
      比如struct numeric_limits<char>等等
    • NULL 这个真的是不值一提的trivial的课题吗?一个库函数值得去定义这个简单的不能再简单的常数吗?可是
      • 它必须是一个宏在哪里规定的?c++标准?应该不是。
      • 它有类型吗?应该是和void*类型一样吧?那么宏定义为0L长度可就不一定和void*一样了。
      • c++2011引入了nullptr这个是新的关键字,
        The C++ 2011 standard added the nullptr keyword, which is a null pointer constant of a special type, std::nullptr_t. Values of this type can be implicitly converted to any pointer type, and cannot convert to integer types or be deduced as an integer type. Unless you need to be compatible with C++98/C++03 or C you should prefer to use nullptr instead of NULL.
    • Dynamic Memory 这个似乎也c/c++是程序员几乎第二天就要掌握的基本常识,然而你知道new/delete有几种形式吗?
      void* operator new(std::size_t);
      Single object form. Throws std::bad_alloc on error. This is what most people are used to using.
      void* operator new(std::size_t, std::nothrow_t) noexcept;
      Single object nothrow form. Calls operator new(std::size_t) but if that throws, returns a null pointer instead.可以直接使用它的ctor比如std::nothrow_t{}
      void* operator new[](std::size_t);
      Array new. Calls operator new(std::size_t) and so throws std::bad_alloc on error.
      void* operator new[](std::size_t, std::nothrow_t) noexcept;
      Array nothrownew. Calls operator new[](std::size_t) but if that throws, returns a null pointer instead.
      void* operator new(std::size_t, void*) noexcept;
      Non-allocating, placement single-object new, which does nothing except return its argument. 什么都不干?这个叫什么函数呢?这个和指针重新赋值有什么区别呢?难道这个是为了帮助自己实现在栈里模拟堆? This function cannot be replaced.什么叫做不能replaced?谁要镇么做?为什么要镇么做呢?
      void* operator new[](std::size_t, void*) noexcept;
      Non-allocating, placement array new, which also does nothing except return its argument. noexcept是什么时候才引入的关键字呢?这个是c++98就有的吗?我看到的是c++11才有的specifier,至于operator noexcept是另一码事。 This function cannot be replaced.
      对于像我这样子粗枝大叶的人一眼看过去还以为这个就是我们第一天学c++的所谓new ctor的语法,可是且住!你看到它返回值是和malloc一样的void*了吗?你看到它的参数是无类型的一个size_t了吗?你意识到它说的是这个global namespace里的operator new了吗?你是这么分配内存的吗?
      void* p1= operator new(sizeof(int));//你是不是现在才意识到它说的是这个?
      delete (int*)p1; // 实际上我应该调用这个: operator delete(p1); // see below discussion!
      int* p2 = new int;//你是不是以为是这个?
      delete p2;
      你是否和我一样的困惑operator new(size_t)和operator new[](size_t)都是一样的需要一个参数怎么就能体现出array和普通的单个对象的区别呢?难道它们都是一个连续内存的分配为什么要两个不同的函数,相应的delete函数我需要两个形式吗?这个仔细一想应该就能猜到array形式的肯定不是连续内存,这个不是必要也不可能有那么大的内存,我记得实现层里有个技巧就是把array的个数写在地址开头处并且用链表来实现内存的多个块分配。我根本不能证明它是连续内存但是很有可能是别的问题,比如内存分配开头部分写了额外的信息倒是可能的,只是我看不到代码。很简单的证明如下
      void*p3= operator new[](sizeof(int)*8);
      //operator delete(p3);//这个是会导致内存泄漏的调用
      //delete (int*)p3; //这个会导致内存泄漏是asan报告的
      //delete [](int*)p3; //你不需要告诉有几个元素,这些信息是保存在地址开头的部分的,这个不一定等价于下面的,但是下面的肯定是对的
      opeerator delete[](p3); // 这个才是正确的调用形式!!!
      
      到这里我还没有看到明确列出的delete的相对应的形式,但是我觉得这个已经是相当的复杂了,似乎超过了FAQ的期望了吧?实际上我很不自信我调用的是期待中的delete因为c++的类型是它的参数,而无法delete void*。所以再次祭起祖师的法宝RTFC在del_op.cc
      _GLIBCXX_WEAK_DEFINITION void
      operator delete(void* ptr) noexcept
      {
        std::free(ptr);
      }
      首先它是被定义为了weak symbol深层次的linking考虑我还无法想像,其次下面调用是完全不一样的delete,作为operator,它可以不用()来传递参数,可是如果使用了()来传递参数那么它对应的函数是上面那个类型为void*的原生delete,而不是期待传递一个类型的c++的operator delete。最后我们知道所谓的delete在最底层的实现和c语言的free没有什么区别。
      void*p3= operator new[](sizeof(int)*8);
      operator delete(p1);
      
    • 对于Termination我始终没有很明确的概念,这个大概是没有比较就没有概念的缘故,为什么要调用abort呢?据说是正常要调用atexit的注册函数?这个机制我始终不太清楚。那么这个std::set_terminate也是调用abort?而libsupc++的Termination Handlers
      • the abort() function does not call the destructors of automatic nor static objects,... The functions registered with atexit() don't get called either
      • The good old exit() function ... 1) Static objects are destroyed in reverse order of their creation. 2) Functions registered with atexit() are called in reverse order of registration, once per registration call. 3) The previous two actions are “interleaved,”
      • atexit() is only required to store 32 functions...recommend using the xatexit/xexit combination from libiberty, which has no such limit.
      abort就是直接退出不像exit会呼叫atexit注册的函数以及释放static allocated object in reversed oder。不过你可以注册自己的
    • 至于说Verbose Terminate Handler你可以理解成它是一个默认被注册的除非你编译时候强制禁止--disable-libstdcxx-verbose
      If you are having difficulty with uncaught exceptions and want a little bit of help debugging the causes of the core dumps, you can make use of a GNU extension, the verbose terminate handler.
      它的过程是
      The __verbose_terminate_handler function obtains the name of the current exception, attempts to demangle it, and prints it to stderr. If the exception is derived from std::exception then the output from what() will be included.
      它的代码在vterminate.cc的确是如此,值得注意的是
      Any replacement termination function is required to kill the program without returning; this one calls std::abort.
      看到这里我才豁然开朗原来abort实际上就是简单raise一个SIGABRT的signal而已。这里就是操作系统一个简单的常识性的概念,一个进程通常并不会自然而然的结束,因为并不像是一个函数结束运行就结束一样吧?正像这个进程不会自然而然开始运行一样的,是操作系统的机制使用fork/exec开始当然也需要类似的机制来kill它,
  2. 无意中下载了TR1,估计我是坚决不去浏览这个文档的。但是情不自禁的保存了一份。我也对于gcc的官方文档长期没有更新感到悲凉,明明这个regex已经实现了啊!可是status里还是说NO

三月六日 等待变化等待机会

  1. 对于libcc1我始终不理解它的作用与定位,因为找不到文档!这里有一些文章似乎是说它是一个plugin,但是看起来不是很确定。而且我亲自实验gdb的时候compile code命令报出了错误说No compiler support for language c++.我一开始怀疑是路径的问题,但是显然的2016年是一个遥远的时代,我的gcc/gdb的版本应该早就要支持了。这里有人建议重新编译新版gdb,这个也许是一个好主意。但是具体编译的时候我有些小犹豫,因为官方的repo给人的印象是和binutils相关的这一点和gcc类似,难道我要下载编译整个binutils吗?我也不想这么安装啊?最后我还是去官网下载发行包编译,这里似乎更加的让人好理解,就是和gcc类似的多个stage编译但是每个stage对于binutils的依赖其实是有限的只是需要一些很少的几个命令而已,而且这个是编译过程需要而已并不是运行期的依赖,所以这个是令人放心的。
  2. 那么另一个问题就是gdb编译是否和我的编译器gcc版本相关呢?这个问题我觉得也是也不是吧?就是gdb不见得本身需要用到最新的语言的特性来编译,但是难保不会使用最新的编译器的一些特性吧?但是debuginfo这个东西我很怀疑有什么大的改动,这个可能也像abi一样的可靠。但是ldd gdb也能看到它也是需要libgcc_s.so,libstdc++.so.6,但是它是否需要c++20才能debug c++20的新特性呢?我以为不需要吧。我连安装都不想,直接运行binary吧。但是运行binary和安装好的情况居然不一样。
  3. 没有精力去探索gdb的种种奇怪现象因为那是另一个海洋。看了看gdb的代码集中在compile的目录下是所有这个在gdb里编译代码的逻辑,这些代码确实很头疼,首先,第一步我要先熟悉什么是gcc的triplet,我结合代码找了我自己ubuntu里各式各样的gcc/g++目录大概就是之前的所谓的build/host/target的相关联的toolchain里的那个三段论,我不知道要怎么称呼它们,比如我自己编译的x86_64-pc-linux-gnu,我模模糊糊记得gcc编译准备脚本configure里有相关的猜测探测的代码,但是这个实在是很烦人,有时候是PC,有时候是unknown,而gnu似乎也不是很重要,这个真的是很让人抓狂。其次,在gdb里实际上是呼叫libcc1.so的接口gcc_c_fe_context_function/gcc_cp_fe_context_function前者是c编译器,后者是c++编译器,它们是libcc1.so我一直还以为有一个对应的libcp1.so因为源代码有这样的文件,但是动态库二者合而为一了。
    $ objdump -CT /usr/lib/gcc/x86_64-linux-gnu/7/libcc1.so |grep gcc_c
    000000000000a260 g    DF .text	0000000000000054  Base        gcc_cp_fe_context
    0000000000006890 g    DF .text	0000000000000054  Base        gcc_c_fe_context
    我之所以看gcc下面的libcc1的代码会糊涂是因为没有想到它的设计是这样子的。通常你得到了这个libcc1.so的入口获得了所谓的context你以为就可以直接操作gcc得到你所需要的运行结果,我就是这样天真的想的结果把自己搞糊涂了,因为这个所谓的接口根本就没有能力做你想像的事情。这个和人间的现象很像似,民间常常有所谓自称手眼通天能够接近大人物的神棍招摇撞骗打着大旗当作虎皮,我们这些凡人以为它们是显贵身边的红人自然就是能够帮我们办事,于是对他们顶礼膜拜孝敬有加,归根结底问题就出在它们那个名号上,凭着libcc1.so的名头想想看cc1是什么来历,那不就是编译器了吗?那么它身边的护卫难道不能给我们办事吗?其实不能,因为gcc/g++确实是一个很庞大的系统确实没有办法如通常的软件那样轻易做到我们想像中的功能,这一点上LLVM拥趸们并没有错。我看了不少时间才明白libcc1只不过是一个门房,就是一个注册回调函数的接口,它本身能够帮你的就是传话,比如第一步他要做一个很搞笑的事情,现在想来才明白我对于名头的错判,比如作为皇上身边的带刀侍卫,它却不确定皇上在哪里!不要以为一个libcc1.so就对应一个版本的gcc/g++,实际上它是一个通用的接口基本上固定不变的,所以它需要在PATH环境变量里的所有路径下按照用户给定的triplet来查找gcc/g++,找到之后他要做的是开一个socketpair,我还是第一次听说这个函数,但是它的作用应该很容易猜到就是一个pipe的应用吧很久很久以前我用过的unix socket应该就是这个东西吧?,让gcc/g++在那一头执行把结果从这个pipe传回来而已。这里关键的是它的参数让我恍然大悟-fplugin=libcp1plugin以及参数-fplugin-arg-libcp1plugin-fd=%d这里的%d是unix socket的file descriptor。原来这个libcc1.so依赖的是libcc1plugin/libcp1plugin,这对兄弟才是真正的内卫,只有它们才能通天,那个门房只是帮我们传话罢了。到这里我们才明白是怎么回事:如果你要使用特定的编译器就直接调用libcp1plugin好了,gdb之所以使用libcc1.so的原因是它不确定用户要使用那个编译器,给了用户指定任意编译器的灵活度。

    山居无日月,世事越千年。

    看到这里我觉得今天的收获足以让我安心休息一下了,虽然几乎一无所获,但是至少看明白了一条路的方向。
  4. 找到了triplet的定义
    Target Triplets describe a platform on which code runs and are a core concept in the GNU build system. They contain three fields: the name of the CPU family/model, the vendor, and the operating system name. You can view the unambiguous target triplet for your current system by running:

    gcc -dumpmachine

    所以我的结果是x86_64-linux-gnu所以,三件套:CPU-ARCH/OS/vendor,这个对吗?太糟了我没有看完就下结论 为什么我总是觉得难以理解呢?一个人不明白什么事情感到理解困难的根本原因就是不肯学习

三月七日 等待变化等待机会

  1. 所以从昨天的结果就直接指向了libcp1plugin,但是我记得之前我已经有一个结论就是gcc的plugin已经被放弃了,这个是怎么回事呢?看来我还是理解肤浅我之前没有找到调用plugin_init的代码就妄下结论是可耻的。实验gcc的testsuite的例子是最好的入门学习。
  2. 我意外的看到gdb跟踪fork的命令,下次实验一下:
    set follow-fork-mode child
  3. 我长久以来一直不明白ubuntu里的clipboard拷贝粘贴为什么不像windows那样跨越terminal,尤其是怎么从less里拷贝是一个头疼的事情,终于看到一个简单的解决办法就是安装xclip然后cat source.cpp|xclip -selection clipboard或者这样子xclip -selection clipboard -in source.cpp
  4. 我对于gcc的plugin的理解有这么大的偏差的原因是什么?我的感觉是以后下结论需要一些证据,做好笔记是必要的。现在先从文档来理解再结合代码分析一下。
  5. 这个plugin loading说了什么呢?

三月八日 等待变化等待机会

  1. 关于plugin API是这样子的。实际上这个部分几乎很多年都没有变化了,所以gcc文档有一个统一的部分,但是我使用当前最新版10.2.0也是无所谓的。

三月九日 等待变化等待机会

  1. 我觉得我之所以对于Plugin initialization 产生困惑的一个原因来自于它的参数传递方式,比如plugin被load后第一个工作就是plugin_init的被调用,可是传递进来的参数plugin_name_args
    struct plugin_name_args
    {
      char *base_name;              /* Short name of the plugin 
                                       (filename without .so suffix). */
      const char *full_name;        /* Path to the plugin as specified with
                                       -fplugin=. */
      int argc;                     /* Number of arguments specified with
                                       -fplugin-arg-.... */
      struct plugin_argument *argv; /* Array of ARGC key-value pairs. */
      const char *version;          /* Version string provided by plugin. */
      const char *help;             /* Help string provided by plugin. */
    };
    
    到底是谁设定的呢?其中的base_name, full_name,argc,plugin_argument是通过命令行传递的参数,但是对于后面的version, help呢?这个也是昨天困扰我的一个原因,因为这个根本就是要plugin的制作者来提供,可是我这个plugin已经被调用了我不通过命令行怎么提供呢?答案当然是通过register_callback来提供了。但是这个实在是有些令人费解,作为plugin的调用者我是使用者还要自己给自己提供帮助吗?难道我要写一些备忘录在我使用gcc的plugin吗?这个gcc难道是运行一个service吗?我就run一次结果却要做这些版本检查和填写注释?这个设计看来是比较泛泛的,当初没有料想到gcc的plugin如此的命运不济被人冷落。的确使用plugin非常的困难,稍不小心就crash而且gcc几乎无法跟踪,我使用gdb代码路径还没有想明白怎么设置,更不要说遇到fork这个拦路虎。所以,我猜想很少有人使用,而且我预感到也许event定义的也很不全颇有鸡肋的意味。 顺便提到一个小细节就是我的plugin是由默认gcc-7.5编译,但是我的plugin的include路径指向了gcc-10.2.0的安装路径,运行的目标的gcc也是gcc-10.2是没有问题的,可是如果我使用gcc-7.5来挂载我的plugin会报错undefined symbol
    c++filt _ZN17gcc_rich_location8add_exprEP9tree_nodeP11range_label
    gcc_rich_location::add_expr(tree_node*, range_label*)
    
    这个看来说明了gcc-plugin对于版本敏感的一个原因吧?看来我还是错的。这个c++filt看来是很cute的东西!
  2. 所以直到现在才进入了深水区:开始了解gcc plugin event。从基本的testsuite里的简单的例子现在还不明白为什么不打印AST tree。这个才开始接触真正的问题。
  3. Plugin callback是plugin的核心, callback的原型也很简单
    /* The prototype for a plugin callback function.
         gcc_data  - event-specific data provided by GCC
         user_data - plugin-specific data provided by the plug-in.  */
    typedef void (*plugin_callback_func)(void *gcc_data, void *user_data);
    
    其中的user_data是用户注册plugin的时候register_callback传入的参数,这里的一个小细节是针对不同的plugin event实际上并不是user_data都是用户自定义的,对于PLUGIN_PASS_MANAGER_SETUP来说这个一定是register_pass_info,也正因为这个你才可以传递一个栈里的临时变量,因为register_pass知道怎么使用这个user_data,自然的对于其他event,这个void*的指针肯定是要保证生命周期的了。仿佛在应证我的发现这里是文档的标准答案
    For the PLUGIN_PASS_MANAGER_SETUP, PLUGIN_INFO, and PLUGIN_REGISTER_GGC_ROOTS pseudo-events the callback should be null, and the user_data is specific.

三月十日 等待变化等待机会

  1. plugin event分几种呢?除了昨天看到的三个pseudo-event之外,我现在回答不了,大体上分好几个模块,比如ggc,pass等等。
  2. 尝试运行testsuites会有一些收获吧?可是我发现这个命令似乎不行。
    make  RUNTESTFLAGS="dg.exp=diagnostic_plugin_show_trees.c" check-gcc
    也许测试例不能在子目录下面? 经过几次摸索这个命令可以make check-gcc RUNTESTFLAGS="plugin.exp",就是说check-gcc指明了目标目录,然后参数plugin.exp指明了运行的driver
  3. 查看测试能够学习到很多的东西,比如怎样使用libcpp呢?使用cpp_callback,这个是异常强大的工具,而这个callback像一把瑞士军刀一样有众多的callback接口,你可以定义自己关于comment的callback来处理comment的事件。
    
    cpp_callbacks *cb = cpp_get_callbacks (parse_in);
    cb->comment = my_comment_cb;
     
  4. 有一点很讨厌的就是使用gcc的无论是哪一个warning/inform/emit_diagnostic都会被当作错误计数。
  5. 从testsuite里学到了如果要压制编译过程的错误信息这个是很好的命令:-fno-diagnostics-show-caret -fno-diagnostics-show-line-numbers -fdiagnostics-color=never -fdiagnostics-urls=never测试例有很多值得学习的东西,查看运行log是我下一步的工作了。

三月十一日 等待变化等待机会

  1. 在gcc的测试例里看到大量使用-Bprefix的选项,我从来没有用过这个,原来它是一个可以替代-L,-I,-isystem的大综合。
  2. 看前辈的代码很多时候是读他们的注释,这个实在是涨学问,比如一个简单的目录处理的问题,如果用户传递的是symlink怎么办?有一个通用的处理办法,就是统统添加一个/.的后缀,这样哪怕是symlink/.也变成了目录。比如mylink是一个指向dir的symlink
    stat mylink 文件:mylink -> dir 大小:3 块:0 IO 块:4096 符号链接 ...
    那么
    stat mylink/. 文件:mylink/. 大小:4096 块:8 IO 块:4096 目录 ...
    这就是为什么gcc在处理目录前全部都加上/.后缀的原因。
  3. 《墨子杯》2020第四届全国兵棋推演大赛,一种全民皆兵的好现象,那么人工智能是需要人类来调教的?而且居然有linux版本的安装,肯定有很多不足,但是应该是有一个很好的起步了。这个是下载中心

三月十二日 等待变化等待机会

  1. gcc代码的变量命名实在是有些随意,比如我花了不少时间去确认用一个字母来命名的全局变量不是笔误,结果还真的不是
    /* The global singleton context aka "g". (the name is chosen to be easy to type in a debugger). */ extern gcc::context *g;
    看到这样的代码是不是如我一样要崩溃了?这样子的理由对于gcc里成千上万的变量来说哪一个不可以有呢?
  2. gcc的代码非常难读的一个重要原因就是命名的随意性,完全没有规范,比如你看到一行代码如果test_diagnostic_starter是一个函数那么diagnostic_starter (global_dc) = test_diagnostic_starter;肯定是设定回调函数,可是天知道这个diagnostic_starter是何方神圣?如果我告诉你global_dc可以猜出来是global variable的diagnostic_context的话,你能猜出来diagnostic_starter是什么东西呢?居然是一个宏!
    #define diagnostic_starter(DC) (DC)->begin_diagnostic
    简直是让人无语了。更让人无语的是同样的回调函数的定义里,既然定义了宏给begin_diagnostic为什么不给start_span定义呢?为什么会有如下的代码存在呢?global_dc->start_span = test_diagnostic_start_span_fn; 我唯一的解释就是这个工程是众多程序员长年累月的积累,中间某些程序员的个人习惯或者是突发奇想的结果吧?每个人其实都会做这样的事情,不过就是因为团体的强制约束防止发生才防止出现,没有了团体的约束这个是必然的结果。
  3. 对于理解diagnostic大有帮助的是这个diagnostic.def比如究竟这些个DK_XXX是什么意思呢?
    /* DK_UNSPECIFIED must be first so it has a value of zero.  We never
       assign this kind to an actual diagnostic, we only use this in
       variables that can hold a kind, to mean they have yet to have a
       kind specified.  I.e. they're uninitialized.  Within the diagnostic
       machinery, this kind also means "don't change the existing kind",
       meaning "no change is specified".  */
    DEFINE_DIAGNOSTIC_KIND (DK_UNSPECIFIED, "", NULL)
    
    /* If a diagnostic is set to DK_IGNORED, it won't get reported at all.
       This is used by the diagnostic machinery when it wants to disable a
       diagnostic without disabling the option which causes it.  */
    DEFINE_DIAGNOSTIC_KIND (DK_IGNORED, "", NULL)
    
    /* The remainder are real diagnostic types.  */
    DEFINE_DIAGNOSTIC_KIND (DK_FATAL, "fatal error: ", "error")
    DEFINE_DIAGNOSTIC_KIND (DK_ICE, "internal compiler error: ", "error")
    DEFINE_DIAGNOSTIC_KIND (DK_ERROR, "error: ", "error")
    DEFINE_DIAGNOSTIC_KIND (DK_SORRY, "sorry, unimplemented: ", "error")
    DEFINE_DIAGNOSTIC_KIND (DK_WARNING, "warning: ", "warning")
    DEFINE_DIAGNOSTIC_KIND (DK_ANACHRONISM, "anachronism: ", "warning")
    DEFINE_DIAGNOSTIC_KIND (DK_NOTE, "note: ", "note")
    DEFINE_DIAGNOSTIC_KIND (DK_DEBUG, "debug: ", "note")
    
    /* For use when using the diagnostic_show_locus machinery to show
       a range of events within a path.  */
    DEFINE_DIAGNOSTIC_KIND (DK_DIAGNOSTIC_PATH, "path: ", "path")
    
    /* These two would be re-classified as DK_WARNING or DK_ERROR, so the
    prefix does not matter.  */
    DEFINE_DIAGNOSTIC_KIND (DK_PEDWARN, "pedwarn: ", NULL)
    DEFINE_DIAGNOSTIC_KIND (DK_PERMERROR, "permerror: ", NULL)
    /* This one is just for counting DK_WARNING promoted to DK_ERROR
       due to -Werror and -Werror=warning.  */
    DEFINE_DIAGNOSTIC_KIND (DK_WERROR, "error: ", NULL)
    /* This is like DK_ICE, but backtrace is not printed.  Used in the driver
       when reporting fatal signal in the compiler.  */
    DEFINE_DIAGNOSTIC_KIND (DK_ICE_NOBT, "internal compiler error: ", "error")
    
    这里你可以学习到一些常用的缩写的意思(我觉得我越来越像English Programmer了,只能看得懂程序里的注释部分。)比如ICE=Internal Compiler Error, NOBT=no BackTrace, SORRY=unimplemented
  4. 我现在找到了一个很好的学习方法就是阅读.def文件,这个往往有着开发者最准确的文档,可能也是gcc唯一的真正的内部文档了吧?真的是完全没有必要,我能想到这个是很好的文档,难道设计者不是这么设计的吗?文档在这里这里有一个特别的重要而我目前又没有能力学习的是tree.def,暂时留一个拷贝
  5. 经常看到GENERIC现在才理解了 The purpose of GENERIC is simply to provide a language-independent way of representing an entire function in trees....If you can express it with the codes in gcc/tree.def, it's GENERIC.
  6. RTL是Register Transfer Languange的缩写。
  7. 有一个重要的部分就是编译产生的文件,这个之前困扰了我不少时间。比如我的plugin就需要它们,所以,它们安装在plugin/include目录下 其中configargs.h是很有意思的,很有可能你可以使用它来比较当前的目标gcc吧?
    /* Generated automatically. */
    static const char configuration_arguments[] = "../gcc-10.2.0/configure CFLAGS='-ggdb3 -O0' CXXFLAGS='-ggdb3 -O0' LDFLAGS=-ggdb3 --prefix=/home/nick/Downloads/gcc-10.2.0/gcc-10.2.0-install --disable-bootstrap --disable-multilib --enable-languages=c++ --enable-gcc-debug";
    static const char thread_model[] = "posix";
    static const struct {
      const char *name, *value;
    } configure_default_options[] = { { "cpu", "generic" }, { "arch", "x86-64" } };
    

三月十三日 等待变化等待机会

  1. 内在的逻辑是如果要理解plugin的代码是必须要理解编译的内部过程,而内部的过程的核心是语义分析,而语义分析的基础很重要的是AST树的设计等等。如果说syntax是形,那么semantec就是神,只有形神具备才是真正的有灵魂的整体。而灵魂是神不是形,语义部分的复杂度远远超过语法部分,我觉得还是放一放吧?
  2. 我的直觉是plugin是非常有用的工具,因为gcc的内部过程过于复杂,没有这样子的窥探的工具普通人是极其难以了解其中的奥秘,哪怕管中窥豹都近乎不可能。但是这个工具似乎不是那么完善吧?或者我的理解还有偏差,比如我想使用PLUGIN_INCLUDE_FILE事件来查看到底有多少include文件被打开结果似乎很让人费解。我本身已经使用了precompiled-header然后再使用stdc++.h这样子的标准库的所谓的precompiled-header,这样子会怎样呢?首先,似乎这个事件对于precompiled-header是无效的,众多的包含的头文件并没有引发事件,其次,stdc++.h似乎是引发了一个built-in的头文件名字,这个很奇怪啊。还有就是gcc -v揭示了很多我没有意识到的问题
    • gcc报出了的第一个搜索路径是/usr/local/include/x86_64-linux-gnu,这个当然是不存在的,我没有安装在系统目录而是个人目录/home/nick/opt/gcc-10.2.0,但是让我吃惊的是这个triplet的问题,为什么是x86_64-linux-gnu,因为我的配置的target是x86_64-unknown-linux-gnu,这个很可能是configure自动侦测无法判断vendor的通常结果吧?这个是所有的gcc的固定的搜索路径吗?因为我自己编译的tripet不像官方版本而是x86_64-unknown-linux-gnu而作为对比的官方版本的tripet才是 x86_64-linux-gnu 找到了可能的原因了因为官方版本的配置是强制使用前缀的--program-prefix=x86_64-linux-gnu-,这个也许和tripet无关吧?
    • 我在makefile里面特意把我的编译器安装目录的include作为参数-I加入搜索路径实际上是浪费因为这个安装路径的include路径是必然要被走索的
      忽略重复的目录“/home/nick/opt/gcc-10.2.0/include/c++/10.2.0”
        因为它是一个重复了系统目录的非系统目录
    • gcc的搜索路径很复杂分为用户定义和系统的,就是通常的引号和尖挂号的,我之前有过这个方面的探索。
      #include "..." search starts here:
      #include <...> search starts here:
      
      这个只能说明""是用户定义的高于系统的<>定义的路径,其中的细节还是很多的不清楚。我印象中看过这部分的相关代码,结果还是不清楚。我现在的感觉是用户定义的-I也是放在了这个系统的搜索路径的最前面,不过这部分c++标准不具体规定是各个编译器自己实现的所以没有什么普遍意义。
    • 我通过我的plugin观察到了传入新的include文件名字是<built-in><命令行>这个中文的命令行当然是我使用中文locale导致gcc从commandline之类的翻译的吧?这个是在我使用bits/stdc++.h出现的。
    • 这里说的是怎么编译标准库的标准的precompiled-header,可是我编译的是我自己的,仅仅是使用了这个stdc++.h似乎仅仅是重新书写了一下头文件列表,但是原理是一样的。但是不管怎样在编译precompiled-header过程中plugin的事件不会出现。
    • 查看代码PLUGIN_INCLUDE_FILE是在parse_in也就是cpp_reader的全局变量的file_change这个callback里来呼叫的。那么就全都取决于cpp/libcpp的调用自己的这个回调函数的机制了。然后作为English Programmer我找到了如下的注释解释了我的疑惑
      /* Called when switching to/from a new file.
       The line_map is for the new file.  It is NULL if there is no new file.
       (In C this happens when done with <built-in>+<command line> and also
       when done with a main file.)  This can be used for resource cleanup.  */
      
      看来问题解决了。
  3. 在我#include "cpplib.h"后总是出现这个编译错误# error "Cannot find a least-32-bit signed integer type",一开始我误以为是我需要包含特别的头文件,最后经过了很多盲目的尝试才意识到这个问题的根本原因是这个头文件本身定义的。它的核心是这个directive的问题:
    #if CHAR_BIT * SIZEOF_INT >= 32
    # define CPPCHAR_SIGNED_T int
    #elif CHAR_BIT * SIZEOF_LONG >= 32
    # define CPPCHAR_SIGNED_T long
    #else
    # error "Cannot find a least-32-bit signed integer type"
    #endif
    这个directive要求我们定义CHAR_BIT所以必须包含glimits.hSIZEOF_INT这个要求包含auto-host.h。其次就是对于头文件line-map.h的一些定义的问题,它要求我们包含ansidecl.hlibiberty.h。至此编译过程可以了,下面是链接的问题了,libcpp.a是肯定的,其次就是libiberty.a。此外还需要。。。libcommon.a和libbacktrace.a,当然了链接的时候必须使用linking flag-static来强制静态链接。 这过程中阅读这个帖子有很多的帮助,当然我的问题不是那么简单。
  4. 我成功的编译libcpp的cpp_reader作为一个静态库来使用仅仅是我已经推迟了差不多一个月的计划而已,因为这个路径是明显的,困难的地方是可以预期的,只是工作量的问题,而怎样模仿gcc driver来传递产生参数才是大问题,但是不管怎么样总是有一种成就感吧?顺便说一下,cpp_reader* parse_in=cpp_create_reader(CLK_CXX2A, nullptr, nullptr);并不代表初始化的成功,因为没有设定正确的参数之前呼叫const char* ptr=cpp_read_main_file(parse_in, "/home/nick/eclipse-2021/cppTest/src/cppTest.cpp");是不会成功的,而这个正是我现在在探索的问题,为了方便debug我决定编译debug版本的libcpp:--enable-cpp-debug不知道这个开关是否是正确的?我是从--enable-gcc-debug推理出来的。

三月十五日 等待变化等待机会

  1. 昨天卡壳的原因在于给cpp配置参数太过于复杂了,因为没有办法跟踪gcc所以,最终参数怎么传递给libcpp的过程太不清楚了,而且另一个是我意识到一些本来以为无关的部分实际上变成不可或缺了,比如diagnostic_context之类的看名字以为并不是运行所必须的,实际上任何错误都会有一个处理机制就必然引发错误信息的输出结果pretty_printer没有设置就会出错了,除非你的参数永远不引发编译过程的错误否则这些看似好像debug帮助的机制实际上是运行必须的设置。
  2. 一个有意思的观察是gcc有一部分的代码实际上还是很规整的,比如incpath这么一个小的功能在头文件里定义的extern函数就是对外的接口,表示这一类的函数实际上是提供给外界调用的,这就是很好的c语言风格的实现隐藏包装,我在考虑能不能利用这些小模块在它们所处的静态库里链接它们,比如这个是处于libbackend.a,如果这条路能行的话可以方便的使用driver遇到了困难因为libbackend.a是一个think archive,我隐约记得这个东西表示它的很多的symbol本身就像一个软链接一样的吧?搜索了一个不明觉厉的帖子。这个可能是死路一条?
    这里是一个急救章的解决过程,表面上看似乎部分工作,但是深层次的问题也许还是存在的。
    • 首先,我决定把thin archive重新编译,这个例子很好的解决了这个问题。因为所谓的thin archive就是incremental的编译的过程吧?所以,这个足够了。ar -t libbackend.a | xargs ar -rvs libbackend_real.a,这么做的一个直接坏处就是我的这个静态库爆增到超过一个G!
    • 紧接着就遇到了在我的这个新的静态库依旧依赖外部的函数定义等的问题,发现我还要增加libcommon-target.a
    • 于是链接问题解决了,这样子我就可以模仿gcc调用cpp的参数传递过程了register_include_chains(parse_in, nullptr, nullptr, nullptr, 1, 1, 1); 这里我采用verbose结果打印出类似的编译器的内置路径
      ignoring nonexistent directory "/home/nick/Downloads/gcc-10.2.0/gcc-10.2.0-debug-libcpp-install/lib/gcc/x86_64-pc-linux-gnu/10.2.0/../../../../x86_64-pc-linux-gnu/include"
      #include "..." search starts here:
      #include <...> search starts here:
       /home/nick/Downloads/gcc-10.2.0/gcc-10.2.0-debug-libcpp-install/lib/gcc/x86_64-pc-linux-gnu/10.2.0/../../../../include/c++/10.2.0
       /home/nick/Downloads/gcc-10.2.0/gcc-10.2.0-debug-libcpp-install/lib/gcc/x86_64-pc-linux-gnu/10.2.0/../../../../include/c++/10.2.0/x86_64-pc-linux-gnu
       /home/nick/Downloads/gcc-10.2.0/gcc-10.2.0-debug-libcpp-install/lib/gcc/x86_64-pc-linux-gnu/10.2.0/../../../../include/c++/10.2.0/backward
       /home/nick/Downloads/gcc-10.2.0/gcc-10.2.0-debug-libcpp-install/lib/gcc/x86_64-pc-linux-gnu/10.2.0/include
       /usr/local/include
       /home/nick/Downloads/gcc-10.2.0/gcc-10.2.0-debug-libcpp-install/include
       /home/nick/Downloads/gcc-10.2.0/gcc-10.2.0-debug-libcpp-install/lib/gcc/x86_64-pc-linux-gnu/10.2.0/include-fixed
       /usr/include
      End of search list.
      注意到了问题吗?我使用的是我自己编译的gcc-10.2本地安装版本,结果它和我使用当前官方gcc-7.5的路径完全驴唇不对马嘴了,所以,这个静态库一定要和运行期的编译器保持一致才行。
    • 我使用这个测试方法可是输出的symbol似乎不太对啊
      const cpp_token*token=NULL;
      token=cpp_get_token(parse_in);
      while (token){	
      	cout<<cpp_token_as_text(parse_in, token)<<endl;
      	token=cpp_get_token(parse_in);
      }
    调用的方式来生成正确的配置参数?前提是这样子的静态链接不会引发新的额外的symbols,否则这个链接就是无意义的。LLVM在这方面就是开玩笑一样的,不是说它们的思路不好,我的感觉是这个项目缺乏专业的人才,不是编译器优化方面而是很多基本的工作需要大量的人力物力时间,这个项目显然没有gcc有优势,那种简单的把一个目录下的代码文件编译成一个所谓的静态库完全是自欺欺人啊,代码的关联性没有隔绝,简单的把代码文件放在不同的目录下有用吗?
  3. 我以为预处理器虽然是编译各个环节中最简单的第一步,但是并不代表它就简单到任何程序员都可以轻易实现的地步,c/c++语言中的很多元素让[f]lex之类的工具无能为力,比如宏的函数型替换恐怕就做不到,此外character-set也是一个很头疼的问题吧?尤其要在预处理阶段解决用户自定义的raw string我就感到很迷茫,而对于我至今还不理解的新的module部分是否在预处理阶段就要开始处理呢?这个更加的复杂,module并不是简单的链接的问题或者编译的部分吧?预处理要不要参与呢?所有这一切我以为都是未知数,也许libcpp并不能解决完成吧?说不定接下去发现有些问题是留待在编译链接的时候才去处理的?那就是一个悲剧了。
  4. 第一百零一次的发问O Romeo, Romeo, wherefore art thou Romeo? 我的coredump在哪里?我决定摘录如下: 此外我的coredump原因已经找到了就是pretty_printer没有设定罢了,这个我已经知道了,只是不知道怎么办而已。

三月十六日 等待变化等待机会

  1. 遇到一个编译错误比较饶舌,比如你用typedef定义个一个回调函数
    typedef bool (*printer_fn) (pretty_printer *, text_info *, const char *, int, bool, bool, bool, bool *, const char **);
    ,但是编译错误说
    error: typedef 'printer_fn' is initialized (use decltype instead)
    这个挺让人困惑的吧,其实关键的问题是这里的类型pretty_printer没有定义结果编译器不把它当作是typedef而是猜测你是在初始化什么东西,或者是定义一个返回类型之类的,总之,这个是非常的误导人的。一个简单的解决办法是forward declare: class pretty_printer;
  2. 而要初始化diagnostic_context的全局变量需要这么做 diagnostic_initialize (global_dc, 0);这里的global_dc是一个global_diagnostic_context的指针变量,类似于一个singlton的做法,只不过没有保护而已,它声明在diagnostic.h,但是包含这个头文件会引发很多的问题,首先,必须包含system.hcoretypes.h而且它们的顺序很重要的。而且system.h能出现在任何c++的通常的头文件比如iostream等等之前
  3. 另一个前两天遇到的小问题是如何判断一个静态库是否是debug build
    nm --debug-syms libexample.a
    通过查看For debugger symbols the second column indicates N.
  4. 我的路径设置依然有些问题,可是设定自己的cpp_callback。仅仅是要输出简单的头文件的信息还不如我已经头昏脑胀了,这个根本就是不同的东西,编译选项是编译期的,我设置cpp_callback是在我自己的调用cpp的时候,根本就是风马牛不相及啊!使用内部定义的编译选项-fdump-internal-locations,但是这个说明文档libcpp/location-example.txt是非常的难懂。我对于简单的location_t还不明了就更不用提rich_location_t了。
  5. 一个下午才搞明白register_include_chains里的第一个参数sysroot的意义实际上就是在默认搜索路径/usr/include所增加的前缀使之成为sysroot/usr/include这个是对应于编译gcc的安装目录configure里的prefix吧?这么简单我却不理解以为是文件名的前缀。而第二个参数iprefix才是我理解的前缀,比如gcc默认会有两个目录includeinclude-fixed它们会被添加前缀iprefixinclude和iprefixinclude-fiexed,而第三个参数imultipath,实际上是gcc默认的第三个搜索路径的后缀,比如我们总是看到这个看似无用的路径lib/gcc/x86_64-pc-linux-gnu/10.2.0/../../../../x86_64-pc-linux-gnu/include,而这个imultipath就是加在这个路径的后缀。
  6. 那么正确的添加搜索路径的办法是什么呢?使用add_path比如
    char*path=strdup("/tmp");
    add_path(path, INC_QUOTE, 1, 1);
    这里为什么要strdup,因为add_path很lazy直接往链表上加指针的,所以不能是const char*
  7. 另一个基本的基本的常识害得我debug一个下午居然是这么一个概念的问题,就是cpp_get_token什么时候结束,我幼稚的以为返回的token是nullptr,真是可笑,看过lex的定义都知道它返回最后一个CPP_EOF的token,所以要判断token->type!=CPP_EOF这么简单我费了一个下午!
  8. 这个设定路径是必要的
    register_include_chains(parse_in, nullptr, nullptr, nullptr, 1, 1, 1);
    否则
    const char* ptr=cpp_read_main_file(parse_in, "/tmp/test.c");
    会返回NULL指针,同样的之后需要
    cpp_init_builtins(parse_in, 1);
    才算是完全初始化,而注册回调函数应该保留之前的。所以,
    cpp_callbacks*pCallback=cpp_get_callbacks(parse_in);
    然后设定
    pCallback->diagnostic=cb_diagnostic;
    再注册
    cpp_set_callbacks(parse_in, pCallback);
    这里的diagnostic回调函数如果没有设定似乎出现任何错误就会fancy_abort?
  9. 这些东西其实我感觉都很无聊,也许根本没有什么逻辑,就是gcc实现的细节,标准不管的。
  10. 保存一个版本。编译指令并不复杂,主要是-I/[gcc-installed-path]/lib/gcc/[gcc-triplet i.e. x86_64-pc-linux-gnu]/[gcc-version i.e. 10.2.0]/plugin/include需要链接这些静态库 libcpp.a libincpath.a libcommon-target.a libcommon.a libbacktrace.a libiberty.a,这里的顺序很重要,而且libincpath.a是一个我自己造出来的静态库:
    ar -t gcc/libincpath.a 
    incpath.o
    cppdefault.o
    options.o
    prefix.o
    
    主要原因是libbackend.a是一个所谓的"thin archive",我无法正常去链接所以只能自己制作ar -rvs libincpath.a incpath.o cppdefault.o options.o prefix.o 看来如果我不留下笔记过一个星期我肯定就忘记怎么重新制作这个project了。

三月十七日 等待变化等待机会

  1. location_t可能是最复杂的一个元素之一吧?我看着那个无比复杂的定义发憷,哪怕程序员的注释解释的图文并茂我也没有心情看下去,主要是一个需求问题,我仅仅想输出源代码的字符串并不想准确的定义所谓的caret/start/finish等等的复杂的给用户的提示。总之,我注意到LLVM的这个caret指示不对,这是让我感到欣慰的:他们也未必搞得非常清楚吧?
  2. 使用cpp_warning_with_line如果传递的cpp_warning_reasonCPP_W_NONE的话,是不会当作错误来计算的。此外我终于意识到了在location小于所谓的BUILTINS_LOCATION的时候输出的头文件是被赋予这个特殊的头衔<built-in>这个就是我之前观察到的原生头文件的输出。而这个所谓的BUILTINS_LOCATION是被定义为1的,并不是我一开始以为的0.
  3. 我相信我绝不是第一个也绝对不是最后一个抱怨gcc命名的问题的凡人,比如对于类似于singleton的全局变量就折磨我了好一会儿,这个cpp_create_reader需要一个line_maps的传入参数,我想当然的就自己创建了一个,后来才发现这个必须使用line_table这个全局指针变量,但是初始化却是要你自己完成,这个做法是相当古老的,我一开始看到所谓的genmatch里可以自己创建以为就是可以的,现在回过头来看这个名字很有可能是一些内部测试使用的东西吧?总之解决了这个问题之后我就可以在回调函数里把location_tlinemap_client_expand_location_to_spelling_point转为expanded_location了。
  4. 中美博弈的本质就是文明之间的你死我活的问题,最好的例证就是三体人和地球人之间争夺生存空间的问题《The Three-Body Problem》。明天在遥远寒冷的阿拉斯加的美军空军基地里要举行的谈判的核心就是判断当战争冲突爆发时候双方需要判断对手的持剑人是否有承担毁灭地球70%以上人口的历史责任的决心问题。如果三体人不是依赖智子从长期对圣母婊程心的长期观察判断得出她绝无可能按下核按钮的话,怎么可能敢于在罗辑仅仅把控制器交给程心才十分钟就发起攻击?对于领导人性格意志的观察分析是决定中美未来冲突策率的根本依据,而爆发于未来的战争起始于今天,就是现在!在武力统一台湾或者武装占领钓鱼岛的过程中,美国核潜艇偷袭中国海军的六艘航母中的任何一艘,那么中国是否敢于使用东风导弹打击美国的航母?也许有,也许没有。但是假如并没有任何美国航母在东风导弹的射程范围内的话,打击美国某个军事基地是否能够得上对等报复?这个行为是否可以被美国认定为如伊朗使用导弹打击美国驻伊拉克的空军基地一样的仅有象征性意义呢?是否这一冲突可以在随后的外交谈判中化解并最终导致台湾和钓鱼岛的历史地位被固定化?这个可以说是美国计划中的最好的剧本。兵法有云:弱不能守,强不能久。这里指的就是作为对弈中的较弱的中方必须要采取进攻的战略才能达到防守的目的;而作为较强的美方时时刻刻都应该明白作为实力暂时居优的一方的时间已经不多了,如果不今早发起战略决战迟早会被中方逆转大局。那么如果一定要爆发冲突最好在美军还占有较大优势的时候爆发,既然要打,那不如大打,那不如打核战争。
  5. 古今中外的战略家最纠结的就在于掌握不胜不败的分寸。在对弈双方中的强势一方几乎没有人敢于主动追求不胜;而与之相对应的是弱势的一方几乎没有人不愿意把追求不败作为最高目标的。有实力获得全胜而予以放弃转而求其次满足于不胜,不但国君不能答应,己方的将士也视之为无能与怯懦。而相对应的看似不可能取得胜利,能够以自保就是最好的运气的弱势一方,如果最高目标超过了不败没有人不会认为是不自量力的自杀送死的想法的。所以,强势的一方不敢追求不胜,弱势的一方不得不追求不败。而若不能守要求的是弱势一方不能满足于不败,要在战略相持阶段到来之前就打出外线去反攻,这个就是在1947年毛泽东要求刘邓挺进中原时候走的险棋。而强不能久的一方如果制定的战略是以不胜为目标的话,他的优势迟早会在完全达到目标前就消失,所以,从来不胜即为失败。所以,蒋介石如果在1947年底就把关外精锐的新一军新六军调入关内以绝对优势兵力解决华东的话,国民政府绝对可以保有长江以南全部和长江以北的大部版土,但是东北的全部丢失给共产党之后。即便倾全国之力也不可能剿灭东北的共产党政权。此为不胜之策,断然没有决策者愿意采纳。是故强有所不甘,弱有所不敢。不胜之策留有胜之余味,为强者所不甘愿也。而不败之策有一厢情愿的意味,为弱者所不敢希望也。
  6. 中美之间冷战已经爆发的一个重要理由是在美苏之间的冷战的一个重要特征就是bluffing的博弈游戏。美苏双方都拥有足以毁灭对方好几次的核武器,而双方的博弈焦点就在于让对方决策者相信自己有决心使用互相摧毁的手段来达到自己的战略目的,而彼此之间的反复试探和努力一方面是做出准备使用核武器的架势,另一方面加强领导人坚毅决心的外在形像的演示,其目的都是不过营造一个让对方相信己方虚张声势的氛围。称其为讹诈的战争一点也不为过。彼此对于对方手中的牌一清二楚却又将信将疑,自欺欺人的揣摩对方领导人揣摩自己心理的各种剧本,也就是《三体》中的那个猜疑链 如果用中文来转述的话,也许是这样子的:
    中方认为美方认为中方在虚张声势,而美方认为中方认为美方在虚张声势。中方的虚张声势的目的是要让美方明白在中方没有虚张声势的时候的的确确是没有虚张声势,或者中方确实在虚张声势的时候要让美方觉得中方并没有虚张声势。而在中方看来美方的虚张声势一定就是虚张声势,因为美国如果没有虚张声势的话,那么美方的的确确是没有虚张声势,所以说美方的确是没有虚张声势。
    这里的逻辑非常的复杂,而中文并没有一个与英文bluffing完全一致内涵的对等的词,而其中的假命题的否定之否定让人头脑发昏了。总之,中美双方究竟谁在虚张声势呢?也许都没有,也许都有,也许只有一方有,无外乎就是这几种组合罢了。
  7. convert命令非常的强大,它背后是强大的ImageMagick比如我要把gif旋转一下convert input.gif -rotate 90 out.gif
  8. 我在debug过程中开始明白我所作的并非毫无意义,至少我现在看到这里的cpp的命令行参数可以和cpp_options做一一对应。而返回的这个cpp_options是一个可以直接修改的指针,也就是说你可以完全控制cpp的所有选项。
  9. 我现在在比较同样的编译器编译我的源代码的搜索路径和同样的代码我使用libcpp来搜索的异同,这是一个了解搜索路径的深入的方法,为了揭示为什么正常gcc编译成功而我的程序爆出来一些看似multilib的头文件没有找到的错误,我决定把之前的plugin加入到编译器然我的plugin输出每次include文件改变的事件,结果我才意识到我的libcpp的错误出在system的header也许是builtin吧,总之gcc有一个机制去ignore这类错误,因为任何系统头文件也许包含了一些找不到的头文件,但是这个能够怪用户吗?这个是系统配置的问题,也许用户从来就不需要这些,比如编译内核才需要用到的头文件在某些nested的include里用到了可是用户的程序根本用不到它们,作为编译器你要报错停下来吗?要知道如果是用户程序里的include包含了找不到头文件是有选项或者报警或者报错的,这个很有可能是用户很在意的人为错误,而系统头文件不一定要报错,报错的决定权在compiler身上,作为preprocessor主要的任务是解决那些定义的宏的展开等等,也许根本就用不到那些找不到的系统头文件。这就是我debug一天的心得,如此的简单的道理我却刚刚才明白过来。
  10. 另一个我慢慢意识到的因素是关于target这个之前我一直以为是和multilib有关联,实际根本是两回事。我现在才明白搜索路径里包含的/usr/include/x86_64-pc-linux-gnu这个x86_64-pc-linux-gnu不是multilib,而是target,multilib是所谓的后缀吧?就是在文件名之后加上的?我找到了gcc的权威文档,现在看起来才有些理解,似乎我只有实践过了才能理解一点点,所以,我对于主席的《实践论》是比较推崇的,一切的真知都来源于实践。
    #include <file> in:
    /usr/local/include
    libdir/gcc/target/version/include
    /usr/target/include
    /usr/include
    For C++ programs, it will also look in libdir/../include/c++/version In the above, target is the canonical name of the system GCC was configured to compile code for; often but not always the same as the canonical name of the system it runs on. version is the version of GCC in use.
    在我的情况targetx86_64-pc-linux-gnu,而version10.2.0,而这里的libdir并不是lib64,而是lib虽然我的系统是64位,但是lib作为兼容32位,或者因为multilib的因素吧,总而言之,lib才是正确的libdir,我模糊记得redhat/fedora似乎不是这样子吧?我不确定。总之,现在对于搜索路径有了更清晰的认识了。
    You can add to this list with the -Idir command line option. All the directories named by -I are searched, in left-to-right order, before the default directories. The only exception is when dir is already searched by default. In this case, the option is ignored and the search order for system directories remains unchanged.
    这里告诉我们什么呢?-I拥有最高的优先级!但是cpp究竟怎么运行的没有明显的输出方式,我的plugin只能输出用户程序的include,对于系统的头文件并不能被调用,所以,依旧只能去看代码才知道。而这个正是我的libcpp需要知道的逻辑。所以,今天的全部就卡在这里了。
    GCC looks for headers requested with #include "file" first in the directory containing the current file, then in the directories as specified by -iquote options, then in the same places it would have looked for a header requested with angle brackets. For example, if /usr/include/sys/stat.h contains #include "types.h", GCC looks for types.h first in /usr/include/sys, then in its usual search path.
    所以顺序本地,-iquote,尖挂号天哪!我以为-iquote意思就是-I""!不是的!它是替代已经过时的-I-。我要崩溃了!
  11. gcc的众多函数都来自于libiberty这里是函数表,查询起来方便多了。

三月十八日 等待变化等待机会

  1. 我已经在昨天笃信像gnu/stubs-32.h这类的系统头文件不会触发missing header之类的错误了,但是其中的逻辑代码的寻找就不那么明显。一开始我是在搜索其中的错误回调函数,我找到了这个专门处理的回调missing_header,它给了我一条线索,但是它的调用的场景非常有限,libcpp的逻辑是只有当正常搜索不到的时候才调用这个用户可以自定义的回调,逻辑是所谓的exhaust搜索才返回。所以这个函数是所有逻辑的集合点_cpp_find_file
  2. cpp反复提到commandline我现在才意识到它可能指的是所谓的-include命令行包含的头文件,这个用法我曾经在强制使用PCH的时候用过,原因似乎是我的代码文件并没有直接包含这个PCH的头文件导致编译器不懂得要使用PCH。 在internal.h里定义的这些头文件类型相当的复杂,因为其中的import我还非常的陌生,而include_next是gnu/gcc的扩展,非常的tricky,很不寻常。其他我也不大明白。HWM代表什么呢?好像是High Weight Module吧?
    enum include_type
    {
    /* Directive-based including mechanisms.  */
    IT_INCLUDE,  /* #include */
    IT_INCLUDE_NEXT,  /* #include_next */
    IT_IMPORT,   /* #import  */
    
    /* Non-directive including mechanisms.  */
    IT_CMDLINE,  /* -include */
    IT_DEFAULT,  /* forced header  */
    IT_MAIN,     /* main  */
    
    IT_DIRECTIVE_HWM = IT_IMPORT + 1,  /* Directives below this.  */
    IT_HEADER_HWM = IT_DEFAULT + 1     /* Header files below this.  */
    };
    这里最重要的是建立一个概念,头文件分两类,一个是directive-based,一个是non-directive-based,后者是我不是很确定。但是我的case应该是前者。这里的逻辑有很多,首先-include,-imacro在所谓的cpp的cwd路径,而#include_next顾名思义要跳到下一个,这个是GNU的扩展,非常的独门。
  3. 在山穷水尽后还是求助于gdb,发现的确是我需要设定set follow-fork-mode child才能设断点,默认的是parent,这个可以使用show follow-fork-mode来查看。
  4. 终于找到了线索,并不是gcc处理逻辑的特殊化,而是应该定义了宏才导致gnu/stubs.h不会包含错误的stubs-32.h而只会是stubs-64.h,这个宏显然是__x86_64__问题是这个宏的翻译可能是来自于target吗?既然我已经知道这个宏起了关键作用,那么宏的机制是什么呢?或者说传递给gcc的宏是没有这个的,这个__x86_64__是gcc内建的宏。所以,这个命令可以输出所有的内建的宏。gcc -x c++ /dev/null -dM -E注意这里-x是强制指定语言的开关,所以这里并不需要直接使用g++就能够指定输出c++相关的宏。
  5. gcc里面很多有关系统设定的宏来自于编译的makefile代入的常量,这些是在代码里不会定义的,都不是hardcoded,只能在编译的makefile里看到,比如DEFAULT_TARGET_MACHINE是来自于makefile里定义的一个宏target_noncanonical='x86_64-pc-linux-gnu',所以,gcc的前端设置运行环境是一个相当大的复杂的过程,这个让单独运行一个模块变得非常的困难。同时debug follow-fork-mode设置断点后continue似乎不能继续,我也是头昏脑胀了。看到代码设置众多的spec就已经糊涂了。出去走走吧。
  6. 阅读gcc的代码显然是很头疼的,但是我总算明白了一点,恐怕是白痴都已经能够意识到的,就是所谓的宏在gcc/cpp眼里就是一个switch,也就是存储在一个非常原始的数组里的数据结构的仿佛变量对子吧?在编译器眼里根本没有什么宏的概念,这些预定义的宏来源不一,也许来自spec,也许是命令行或者环境变量等等,总之这个数据结构是一个通用的所谓的obstack我一开始还以为这个原始的结构会直接从gcc前端转移给cpp,后来一想是不可能的吧,因为很多是给cc1/cp1/collect2等等,结果最后才看到让我今天唯一比较高兴的东西,cpp_define是完成这个定义过程的钥匙,这个函数builtin_define_std是这个工作的入口。而且这里是那些开头有两个__的宏的出处,这里就是那段注释我现在会成为comment programmer了!
    Pass an object-like macro.  If it doesn't lie in the user's
    namespace, defines it unconditionally.  Otherwise define a version
    with two leading underscores, and another version with two leading
    and trailing underscores, and define the original only if an ISO
    standard was not nominated.
    
    e.g. passing "unix" defines "__unix", "__unix__" and possibly
    "unix".  Passing "_mips" defines "__mips", "__mips__" and possibly
    "_mips". 
    这里提到ISO不提倡定义这些头尾没有__的宏,这个我还真的没有概念,总之现在知道一个内建的宏很可能一个变三个在cpp里。
  7. 其实这里说的很清楚,我其实压根不需要依赖于gcc传递参数给cpp,我可以直接看cpp的定义的宏,只有这个时候我才会再次认真的去看手册

三月十九日 等待变化等待机会

  1. 我验证了昨天的想法,结果是失败:我把cpp -dM /dev/null输出的宏依样画葫芦传递给我的libcpp结果依然出错在tbb/tbb.h。怎么办呢?只好再用gdb跟踪吧?
  2. gdb的结果让我有些困惑也有些欣慰,使用官方gcc的gdb跟踪让我明白了同样遇到missing header的情况,这个tbb/tbb.h被忽略了,其中的注释我看不懂,可能是和import有关吧?
    /* Although this file must not go in the cache, because the file found might depend on things (like the current file) that aren't represented in the cache, it still has to go in the list of all files so that #import works. */
    所以,我并不需要特别处理就应该可以,那么我出了什么错呢?我再gdb我自己的libcpp发现回调函数missing_header里我使用cout输出就导致gdb跟踪无法继续,也许这个就是根源?去掉之后似乎正确跳过了missing_header的错误,但是最后还是coredump然后我gdb core之后看到bt里是所谓的一个convert_escape的空指针crash了,这是怎么回事呢? 经过艰苦的gdb过程发现需要先初始化cpp_init_iconv否则charset conversion的函数指针没有初始化,这个的确是一个让人感到有些意外的地方,我始终感觉对于charset这个概念是模糊的,其中既包含代码和可执行码的这两个方面。其中代码的charset是比较复杂的,这个牵涉到各种literal的翻译问题,比如我遇到的crash就是在翻译escape的时候无所适从而崩溃的,虽然我的编码是utf-8已经设定过了,可是相关的转换函数的初始化就是这个函数的作用。我不由得再次感叹gcc整个系统里最简单的预处理就是这么的复杂,那么后面部分呢?以前编译器课程的教授说过编译器实现的95%的难读在于代码产生部分因为所有的优化都发生在那里,那么这剩下的不到5%的部分里预处理又是少之又少的部分,那么。。。我只能仰天长叹。
  3. 再次温习孙子对于谋攻的教诲:
    故知胜有五:知可以战与不可以战者胜;识众寡之用者胜;上下同欲者胜;以虞待不虞者胜;将能而君不御者胜。此五者,知胜之道也。
    知易行难,作为统帅怎么能够判断出知可以战与不可以战呢?你怎么知道你依赖的幕僚确实知道呢?我以为就是要依赖算法与实验来验证,运用兵棋推演并把兵棋推演结果做近似实战化进行验证。决策者不需要也不可能知道实现的细节,但是要知道验证的逻辑与原理并亲眼验证验证过程本身的可靠性。陪审员不需要法官的法律专业知识技能,但是必须具有凭常识判断证据可信度以及因果逻辑关系正确性的能力。从孙子的这五个要素来看,中美较量中方并不可能在任何四个领域占优,在我看来中方唯一的优势就只有一条:上下同欲。这恐怕是中方唯一的优势,因为在其他四个领域里美方都不见得比中方弱,甚至有先发的优势而占优。
  4. 似乎奋力解决了几个星期终于似乎把libcpp走通了,终于可以松口气回过头来看看它能做什么了。
  5. 你们没有资格在中国的面前说,你们从实力的地位出发同中国谈话。多么的霸气侧漏啊!这句中文要怎么翻译啊?什么叫做从实力的地位出发?中国不承认美国的一超地位了吗?这次聚会是否相当于渑池之会呢?对于秦王侮辱赵王鼓瑟时候蔺相如五步之内,相如可以刎颈以血溅大王!
  6. 让我们回顾一下要怎么走到这一步吧?
  7. 我觉得这个是值得记录一下,因为我还从未听说过这个POSIX的标准函数open_memstream,这一点让人汗颜,但是让人欣慰的是cppreference.com里居然也是没有内容,也许大家觉得作为c++程序员这个是不值得,或者相对于强大的stringstream没有任何优越性可言的?而且什么叫做POSIX.1-2008. These functions are not specified in POSIX.1-2001, and are not widely available on other systems.?我不明白这个到底是什么时候问世的,怎么没有人通知我呢? 这里我为了适应cpp_output_token需要的file stream,我想把结果输出在内存里处理,所以,这个是我能够想到的办法,本来想用stringstream,可是要怎么模拟FILE*呢?似乎没有显而易见的办法吧?不过这个方法有些不可靠,因为如果要保留一个个的token,你要拷贝一个个的string,保存buffer的指针偏移肯定是不行的,天知道有没有在内部使用了realloc,所以,这个方法实在是很笨拙,我还想不出来什么好办法。或者牺牲效率,就每次都调用新的open_memstream,这个节省一些内存吧?实际上使用cpp_spell_token就解决了这个问题。

三月二十日 等待变化等待机会

  1. 听说过被人提到pumping lemma,才确定确实以前学过但是基本就忘记了内容。
  2. 开始尝试一个全新的领域之前我想把昨天的最后一个补充点记录下来,就是说这个是浅而易见的,虽然我们证实但是应该是正确的,就是关于大量的gcc的builtin macro的来源,那一定是来自于spec。我很担心我过两天就忘记了这个粗浅的道理。这个结论是正确的我对此深信不疑,但是有很多可以利用的就是比如define_language_independent_builtin_macros,又比如c_stddef_cpp_builtins,可是大问题是很难利用,因为如何获得最原始的资料呢?所以,我觉得最根本的还是去解析spec,我对此深信不疑。但是面对如此戴良的宏尤其是和c++各个不同版本相关的宏的定义的话,不使用这个c_cpp_builtins是难以想像的。
  3. 正像我之前就意识到的libcc1是徒有其名的因为它仅仅是一个帮助你联系libcc1plugin的傀儡,而那个plugin本身是很有限的,虽然我还没有完全尝试过。
  4. 注意到大量的libcpp的回调被应用在cc1/cp1里也许这个是基本的模式吧?在不同的回调里体现语言的差异处理?gcc的很好的设计成为你的障碍,因为尽可能的代码重用导致多个语言的多个模块里分散了代码逻辑导致非常的难以集中理解吧?有一利就有一弊。
  5. 我几乎立刻就意识到c/c++编译器和预处理器的紧密关系超过了我的想像,两者可以说是深度绑定,所以回过头来重新审视cpp_token的应用很有必要。
  6. 我依稀记得c++标准里说操作符的优先级和关联性在BNF语法里没有明确的表达是需要隐含的推理出来的,而在实现层里这个是明确的表达的:在expr.c里的这个表就是
    
    static const struct cpp_operator
    {
      uchar prio;
      uchar flags;
    } optab[] =
    {
      /* EQ */		{0, 0},	/* Shouldn't happen.  */
      /* NOT */		{16, NO_L_OPERAND},
      /* GREATER */		{12, LEFT_ASSOC | CHECK_PROMOTION},
      /* LESS */		{12, LEFT_ASSOC | CHECK_PROMOTION},
      /* PLUS */		{14, LEFT_ASSOC | CHECK_PROMOTION},
      /* MINUS */		{14, LEFT_ASSOC | CHECK_PROMOTION},
      /* MULT */		{15, LEFT_ASSOC | CHECK_PROMOTION},
      /* DIV */		{15, LEFT_ASSOC | CHECK_PROMOTION},
      /* MOD */		{15, LEFT_ASSOC | CHECK_PROMOTION},
      /* AND */		{9, LEFT_ASSOC | CHECK_PROMOTION},
      /* OR */		{7, LEFT_ASSOC | CHECK_PROMOTION},
      /* XOR */		{8, LEFT_ASSOC | CHECK_PROMOTION},
      /* RSHIFT */		{13, LEFT_ASSOC},
      /* LSHIFT */		{13, LEFT_ASSOC},
      /* COMPL */		{16, NO_L_OPERAND},
      /* AND_AND */		{6, LEFT_ASSOC},
      /* OR_OR */		{5, LEFT_ASSOC},
      /* Note that QUERY, COLON, and COMMA must have the same precedence.
         However, there are some special cases for these in reduce().  */
      /* QUERY */		{4, 0},
      /* COLON */		{4, LEFT_ASSOC | CHECK_PROMOTION},
      /* COMMA */		{4, LEFT_ASSOC},
      /* OPEN_PAREN */	{1, NO_L_OPERAND},
      /* CLOSE_PAREN */	{0, 0},
      /* EOF */		{0, 0},
      /* EQ_EQ */		{11, LEFT_ASSOC},
      /* NOT_EQ */		{11, LEFT_ASSOC},
      /* GREATER_EQ */	{12, LEFT_ASSOC | CHECK_PROMOTION},
      /* LESS_EQ */		{12, LEFT_ASSOC | CHECK_PROMOTION},
      /* UPLUS */		{16, NO_L_OPERAND},
      /* UMINUS */		{16, NO_L_OPERAND}
    };
    但是它们似乎和c++网站的这个表对不上?
    Precedence Operator Description Associativity
    1 :: Scope resolution Left-to-right
    2 a++   a-- Suffix/postfix increment and decrement
    type()   type{} Functional cast
    a() Function call
    a[] Subscript
    .   -> Member access
    3 ++a   --a Prefix increment and decrement Right-to-left
    +a   -a Unary plus and minus
    !   ~ Logical NOT and bitwise NOT
    (type) C-style cast
    *a Indirection (dereference)
    &a Address-of
    sizeof Size-of[note 1]
    co_await await-expression (C++20)
    new   new[] Dynamic memory allocation
    delete   delete[] Dynamic memory deallocation
    4 .*   ->* Pointer-to-member Left-to-right
    5 a*b   a/b   a%b Multiplication, division, and remainder
    6 a+b   a-b Addition and subtraction
    7 <<   >> Bitwise left shift and right shift
    8 <=> Three-way comparison operator (since C++20)
    9 <   <= For relational operators < and ≤ respectively
    >   >= For relational operators > and ≥ respectively
    10 ==   != For equality operators = and ≠ respectively
    11 & Bitwise AND
    12 ^ Bitwise XOR (exclusive or)
    13 | Bitwise OR (inclusive or)
    14 && Logical AND
    15 || Logical OR
    16 a?b:c Ternary conditional[note 2] Right-to-left
    throw throw operator
    co_yield yield-expression (C++20)
    = Direct assignment (provided by default for C++ classes)
    +=   -= Compound assignment by sum and difference
    *=   /=   %= Compound assignment by product, quotient, and remainder
    <<=   >>= Compound assignment by bitwise left shift and right shift
    &=   ^=   |= Compound assignment by bitwise AND, XOR, and OR
    17 , Comma Left-to-right
  7. gcc的编译器和预处理器深度耦合的另一个例证是在预处理器的cpp_token的基础上搭建编译器层面上的所谓的token,这个其实也没有什么不对,你在flex/bison上也看到这种紧密的关联,比如你在bison里面定义了token的类型,让flex返回你设定的类型。这个不是顺理成章的吗?这个能看作是深度耦合吗?只不过是我感到不快的是预处理器的一个类型CPP_KEYWORD原来就是预定给编译器用的,因为我还在纳闷什么是预处理器级别的keyword呢?define?不是的,这个宏的逻辑已经被消化了,根本不会暴露给编译器了,所以,我才感到有点蒙什么算是keyword呢?
  8. 有一件事我始终很模糊就是关于import好像在预处理器曾经有过一个过时的#import这个似乎微软还有吧?所以,我在预处理器的表里看到它就以为是新时代的module集成在预处理器层面,很快就证明这是误解。另一个典型的例子就是预处理的#assert被我混淆了,我一时以为是c语言的assert,而且我这时候才意识到原来pragma是预处理指示,这个听上去让人感到可笑,难道不是吗?我脑子里有很多很奇怪的概念不知道哪里来的。总而言之,cpp_reader的cpp_token已经把这些预处理器的directive都处理过了,所以不存在什么keyword了,想想也对,预处理器的directive都是要你处理过后产生给下一级的输入的没有理由把所谓的keyword留下来给下一级的编译器看啊。所以,这就是为什么CPP_KEYWORD是挖的坑让我钻的缘故,它完全就是留着给编译器来定义的。或者另一个可能这个是完全为了给objectiveC准备的吧?苹果有一些特别的东西比如居然有一个#操作符。
  9. 总之我终于开始意识到llvm抱怨gcc的一个显而易见的问题,因为兼容多种语言编译部分的逻辑分散在很多层面。。。
  10. 我刚刚发现我用错了函数。这个cpp_output_token有一个孪生兄弟cpp_spell_token就是我要找的函数,所以完全没有必要使用什么open_memstream。值得注意的是cpp_spell_token是不能处理CPP_PADDING的,这个当然是可以理解的,你要打印空格你自己去打印吧。而且查看这个函数你就能理解很多的原理,比如怎样从hashnode转换,不过我不想了解这些细节。 其中的TOKEN_NAME有些精心动魄因为我很少干这种宏的复杂的替换,尤其是重定义的宏,不过我是现学现卖把原本的TOKEN_NAME改造了一下拷贝过来的:
    
    struct Token_Name
    {
    	cpp_ttype t;
    	unsigned char* name;
    };
    #define OP(e, s) { CPP_##e,    (unsigned char*) #s },
    #define TK(e, s) { CPP_##e,    (unsigned char*) #e },
    static const struct Token_Name token_names[N_TTYPES] = { TTYPE_TABLE };
    #undef OP
    #undef TK
    #define TOKEN_NAME(token) (token_names[(token)->type].name)
    
    这里的关键是TTYPE_TABLE是被利用的带有注解的,设计者当初就计划好了你灵活使用注解作为打印enum的办法,这个在c++的reflective能力有突破之前是唯一的办法。
    
    #define TTYPE_TABLE							\
      OP(EQ,		"=")						\
      OP(NOT,		"!")						\
      OP(GREATER,		">")	/* compare */				\
      OP(LESS,		"<")						\
      OP(PLUS,		"+")	/* math */				\
      OP(MINUS,		"-")						\
      OP(MULT,		"*")						\
      OP(DIV,		"/")						\
      OP(MOD,		"%")						\
      OP(AND,		"&")	/* bit ops */				\
      OP(OR,		"|")						\
      OP(XOR,		"^")						\
      OP(RSHIFT,		">>")						\
      OP(LSHIFT,		"<<")						\
    									\
      OP(COMPL,		"~")						\
      OP(AND_AND,		"&&")	/* logical */				\
      OP(OR_OR,		"||")						\
      OP(QUERY,		"?")						\
      OP(COLON,		":")						\
      OP(COMMA,		",")	/* grouping */				\
      OP(OPEN_PAREN,	"(")						\
      OP(CLOSE_PAREN,	")")						\
      TK(EOF,		NONE)						\
      OP(EQ_EQ,		"==")	/* compare */				\
      OP(NOT_EQ,		"!=")						\
      OP(GREATER_EQ,	">=")						\
      OP(LESS_EQ,		"<=")						\
      OP(SPACESHIP,		"<=>")						\
    									\
      /* These two are unary + / - in preprocessor expressions.  */		\
      OP(PLUS_EQ,		"+=")	/* math */				\
      OP(MINUS_EQ,		"-=")						\
    									\
      OP(MULT_EQ,		"*=")						\
      OP(DIV_EQ,		"/=")						\
      OP(MOD_EQ,		"%=")						\
      OP(AND_EQ,		"&=")	/* bit ops */				\
      OP(OR_EQ,		"|=")						\
      OP(XOR_EQ,		"^=")						\
      OP(RSHIFT_EQ,		">>=")						\
      OP(LSHIFT_EQ,		"<<=")						\
      /* Digraphs together, beginning with CPP_FIRST_DIGRAPH.  */		\
      OP(HASH,		"#")	/* digraphs */				\
      OP(PASTE,		"##")						\
      OP(OPEN_SQUARE,	"[")						\
      OP(CLOSE_SQUARE,	"]")						\
      OP(OPEN_BRACE,	"{")						\
      OP(CLOSE_BRACE,	"}")						\
      /* The remainder of the punctuation.	Order is not significant.  */	\
      OP(SEMICOLON,		";")	/* structure */				\
      OP(ELLIPSIS,		"...")						\
      OP(PLUS_PLUS,		"++")	/* increment */				\
      OP(MINUS_MINUS,	"--")						\
      OP(DEREF,		"->")	/* accessors */				\
      OP(DOT,		".")						\
      OP(SCOPE,		"::")						\
      OP(DEREF_STAR,	"->*")						\
      OP(DOT_STAR,		".*")						\
      OP(ATSIGN,		"@")  /* used in Objective-C */			\
    									\
      TK(NAME,		IDENT)	 /* word */				\
      TK(AT_NAME,		IDENT)	 /* @word - Objective-C */		\
      TK(NUMBER,		LITERAL) /* 34_be+ta  */			\
    									\
      TK(CHAR,		LITERAL) /* 'char' */				\
      TK(WCHAR,		LITERAL) /* L'char' */				\
      TK(CHAR16,		LITERAL) /* u'char' */				\
      TK(CHAR32,		LITERAL) /* U'char' */				\
      TK(UTF8CHAR,		LITERAL) /* u8'char' */				\
      TK(OTHER,		LITERAL) /* stray punctuation */		\
    									\
      TK(STRING,		LITERAL) /* "string" */				\
      TK(WSTRING,		LITERAL) /* L"string" */			\
      TK(STRING16,		LITERAL) /* u"string" */			\
      TK(STRING32,		LITERAL) /* U"string" */			\
      TK(UTF8STRING,	LITERAL) /* u8"string" */			\
      TK(OBJC_STRING,	LITERAL) /* @"string" - Objective-C */		\
      TK(HEADER_NAME,	LITERAL) /* <stdio.h> in #include */		\
    									\
      TK(CHAR_USERDEF,	LITERAL) /* 'char'_suffix - C++-0x */		\
      TK(WCHAR_USERDEF,	LITERAL) /* L'char'_suffix - C++-0x */		\
      TK(CHAR16_USERDEF,	LITERAL) /* u'char'_suffix - C++-0x */		\
      TK(CHAR32_USERDEF,	LITERAL) /* U'char'_suffix - C++-0x */		\
      TK(UTF8CHAR_USERDEF,	LITERAL) /* u8'char'_suffix - C++-0x */		\
      TK(STRING_USERDEF,	LITERAL) /* "string"_suffix - C++-0x */		\
      TK(WSTRING_USERDEF,	LITERAL) /* L"string"_suffix - C++-0x */	\
      TK(STRING16_USERDEF,	LITERAL) /* u"string"_suffix - C++-0x */	\
      TK(STRING32_USERDEF,	LITERAL) /* U"string"_suffix - C++-0x */	\
      TK(UTF8STRING_USERDEF,LITERAL) /* u8"string"_suffix - C++-0x */	\
    									\
      TK(COMMENT,		LITERAL) /* Only if output comments.  */	\
    				 /* SPELL_LITERAL happens to DTRT.  */	\
      TK(MACRO_ARG,		NONE)	 /* Macro argument.  */			\
      TK(PRAGMA,		NONE)	 /* Only for deferred pragmas.  */	\
      TK(PRAGMA_EOL,	NONE)	 /* End-of-line for deferred pragmas.  */ \
      TK(PADDING,		NONE)	 /* Whitespace for -E.	*/
    

三月二十一日 等待变化等待机会

  1. 我始终认为c++里最困难的一个地方是模板,因为我自始至终不理解要怎么实现它,一方面的原因是编译原理没有这方面的内容,我以前只能理解就是一个复杂的宏,可是后来觉得预处理器没有这个能力也许是在连接器处理的,总之很迷茫。这一段问题以前也读到过根本一个字都不理解。现在只能再反复读看看,或者抄录下来来理解?

三月二十二日 等待变化等待机会

  1. 关于宏和模板的讨论如果是从使用者的角度来看,这些贤者的议论无疑是正确的。但是为什么宏是预处理器处理的对象而模板却是编译器处理的对象呢?这其中有一个非常浅显易懂的道理却没有人点出,并不是大家不知道而是大家都认为是理所当然的常识而不值一提,所以,我的看法是从处理难度的角度来解释两者的异同就是:宏因为是oneliner的一个简单的替换可以轻而易举的使用简单的字符串替换而需要语法语义分析的预处理器做到。而相对应的模板是可以看作高级的宏并不是依靠一个特殊的换行符来标识它的结尾,模板可以跟随一个函数,可以跟随一个类,现在也可以跟随一个变量,这导致模板必须要做语法语义分析才能解析它的定义,所以,它必然是编译器才能处理的对象。我以为虽然我们内心可以把模板想像成一种高级的宏,但是两者的本质差别却是巨大的原因就在于此吧?
  2. 又看了一遍GCC Template Instantiation我依然是不得要领,能不能这么理解就是gcc采用的是Borland模式?我越看就越糊涂,我感觉我根本不明白它在说什么也许它根本就是讲的两回事。比如你要防止一个模板实例的多次出现真的很困难吗?困难在哪里?函数不是有签名吗?难道ODR的原则不是很容易吗?所以,我觉得我完全不能理解,也许它讲的不是我理解的吧?
    这里我做了一个实验意外的发现模板函数都被当作了weak symbol,具体的说就是它首先被放在了.textsection的自己的section

    TL;DR

    头文件template.h
    
    template <typename T>
    bool compare(T t1, T t2){
            return t1<t2;;
    }
    
    代码文件template3.cpp
    
    #include "template.h"
    void test3(){
            compare(23, 34);
            compare<char>(5,7);
    }
    void test4(){
            test3();
    }
    
    现在我们看看section吧。
    
    objdump -Ct  template3.o
    
    template3.o:     file format elf64-x86-64
    
    SYMBOL TABLE:
    0000000000000000 l    df *ABS*	0000000000000000 template3.cpp
    0000000000000000 l    d  .text	0000000000000000 .text
    0000000000000000 l    d  .data	0000000000000000 .data
    0000000000000000 l    d  .bss	0000000000000000 .bss
    0000000000000000 l    d  .text._Z7compareIiEbT_S0_	0000000000000000 .text._Z7compareIiEbT_S0_
    0000000000000000 l    d  .text._Z7compareIcEbT_S0_	0000000000000000 .text._Z7compareIcEbT_S0_
    0000000000000000 l    d  .note.GNU-stack	0000000000000000 .note.GNU-stack
    0000000000000000 l    d  .eh_frame	0000000000000000 .eh_frame
    0000000000000000 l    d  .comment	0000000000000000 .comment
    0000000000000000 l    d  .group	0000000000000000 .group
    0000000000000000 l    d  .group	0000000000000000 .group
    0000000000000000 g     F .text	0000000000000025 test3()
    0000000000000000  w    F .text._Z7compareIiEbT_S0_	0000000000000015 bool compare<int>(int, int)
    0000000000000000  w    F .text._Z7compareIcEbT_S0_	000000000000001a bool compare<char>(char, char)
    0000000000000025 g     F .text	000000000000000c test4()
    
    所以,模板函数compare<int>处于自己独特的一个section .text._Z7compareIiEbT_S0_,这个可以很简单的就证实它和.text无关。另一个模板函数也是类似的就是它们都被标签weak symbol.
    
    objdump -Ct --section=.text._Z7compareIiEbT_S0_  template3.o
    
    template3.o:     file format elf64-x86-64
    
    SYMBOL TABLE:
    0000000000000000 l    d  .text._Z7compareIiEbT_S0_	0000000000000000 .text._Z7compareIiEbT_S0_
    0000000000000000  w    F .text._Z7compareIiEbT_S0_	0000000000000015 bool compare<int>(int, int)
    
    objdump -Ct --section=.text._Z7compareIcEbT_S0_ template3.o
    
    template3.o:     file format elf64-x86-64
    
    SYMBOL TABLE:
    0000000000000000 l    d  .text._Z7compareIcEbT_S0_	0000000000000000 .text._Z7compareIcEbT_S0_
    0000000000000000  w    F .text._Z7compareIcEbT_S0_	000000000000001a bool compare<char>(char, char)
    
    
    这里我们再次复习一下weak symbol的定义:
    A weak symbol denotes a specially annotated symbol during linking of Executable and Linkable Format (ELF) object files. By default, without any annotation, a symbol in an object file is strong. During linking, a strong symbol can override a weak symbol of the same name. In contrast, in the presence of two strong symbols by the same name, the linker resolves the symbol in favor of the first one found. This behavior allows an executable to override standard library functions, such as malloc(3). When linking a binary executable, a weakly declared symbol does not need a definition. In comparison, (by default) a declared strong symbol without a definition triggers an undefined symbol link error.
    怎么理解呢?
    • strong和weak symbol这个概念都是相对于symbol declaration而言才有意义,我以为对于definition来说是没有什么strong or weak的,因为definition就是defintion无所谓strong or weak的。当然你也可以没有declaration而直接在函数definition上直接声明它为weak,但是这个不改变我的观点:那就是weak/strong是symbol的属性和怎么实现无关的概念。而声明weak在函数definition的时候,编译器要求你把__attribute__((weak))放在函数的返回值之前才行,我找到了支持我的观点的帖子,就是按照标准应该是只有声明才有意义吧?
      The weak function attribute is supposed to be used on function declarations. Using it on a function definition may yield unexpected results, depending on the compiler and optimization level.
      
      __attribute__((weak)) void myfunc() 
      {}
      
    • 如果同时对一个symbol既声明为weak也声明为strong(没有声明为weak就是strong)那么最后这个symbol它就是weak,不管顺序如何。比如我在
      nick@nick-HP-Laptop:/tmp/temp$ cat weak.h 
      void myfunc() __attribute__((weak)) ;
      nick@nick-HP-Laptop:/tmp/temp$ cat strong.h 
      void myfunc();
      nick@nick-HP-Laptop:/tmp/temp$ cat myfunc.c 
      #include "weak.h"
      #include "strong.h"// it doesn't matter even we change the order of include here 
      void myfunc()
      {}
      nick@nick-HP-Laptop:/tmp/temp$ gcc -c myfunc.c -o myfunc.o
      nick@nick-HP-Laptop:/tmp/temp$ nm myfunc.o
      0000000000000000 W myfunc
      
      诚所谓一日weak,则终生weak。
    • 对于同一个symbol你可以把它声明为正常的(strong)也可以把它声明为weak的,取决于你编译这个实现的时候编译器看到的声明,所以这个时候头文件非常的重要我之前遇到过莫名其妙的coredump也许是我的typo导致我认为不能在definition里声明函数为weak symbol,才导致我以为一定要有头文件来声明,其实编译器对于__attribute__((weak))放在什么地方似乎是很随意的,在返回值前或者后,或者在函数体之前都可以。
    • 这个问题在静态库里是很要命的
      Using weak symbols in static libraries has other semantics than in shared ones, i.e. with a static library the symbol lookup stops at the first symbol – even if it is just weak and an object file with a strong symbol is also included in the library archive. On Linux, the linker option --whole-archive changes that behavior.
      就是说一个symbol的声明在一个obj文件里是ODR,但是对于不同的obj文件就是链接的问题了,如果把不同的声明放在同一个静态库里就是灾难了!
    • 这位大神说weak symbol在动态库里不起作用,它仅仅是静态链接才有用。这个不是颠倒乾坤吗?也就是说先到先得,平起平坐。这里是全文的解读
      On UNIX System V descendent systems, during program runtime the dynamic linker resolves weak symbols definitions like strong ones. For example, a binary is dynamically linked against libraries libfoo.so and libbar.so. libfoo defines symbol f and declares it as weak. libbar also defines f and declares it as strong. Depending on the library ordering on the link command line (i.e. -lfoo -lbar) the dynamic linker uses the weak f from libfoo.so although a strong version is available at runtime. The GNU ld provides the environment variable LD_DYNAMIC_WEAK to provide weak semantics for the dynamic linker.
      所以动态链接必须有环境变量LD_DYNAMIC_WEAK才能强制要求寻找strong symbol,但是lazy之类的就复杂了吧?
    • 这个静态链接的例子说明什么呢?一个被声明为weak symbol可以同时有多个定义。23.03.2021以下是我在不知不觉中重复了wiki的静态链接的例子,现在算是理解了静态的粗浅的道理,就是同一个symbol取决于每个编译的obj文件的声明可以编译成可能strong或者weak的symbol,那么在链接的时候就有了优先级的差别
      
      nick@nick-HP-Laptop:/tmp/temp$ cat first.h
      int myfunc() __attribute__((weak));
      nick@nick-HP-Laptop:/tmp/temp$ cat first.c
      #include "first.h"
      int myfunc(){
      	return 1;
      }
      nick@nick-HP-Laptop:/tmp/temp$ cat call.c
      #include "first.h"
      int main(){
      	return myfunc();	
      }
      
      这里因为我们在头文件里声明了myfunc是一个weak symbol,导致它们编译后就是weak symbol。而second.c里我们没有声明myfunc为weak symbol
      
      nick@nick-HP-Laptop:/tmp/temp$ cat second.c
      int myfunc(){
      	return 2;
      }
      
      然后我们编译所有的代码文件
      
      nick@nick-HP-Laptop:/tmp/temp$ gcc -g -c first.c -o first.o
      nick@nick-HP-Laptop:/tmp/temp$ gcc -g -c second.c -o second.o
      nick@nick-HP-Laptop:/tmp/temp$ gcc -g -c call.c -o call.o
      
      让我们看看每个obj里的weak symbol是怎么样的。
      
      nick@nick-HP-Laptop:/tmp/temp$ nm first.o
      0000000000000000 W myfunc
      nick@nick-HP-Laptop:/tmp/temp$ nm second.o
      0000000000000000 T myfunc
      nick@nick-HP-Laptop:/tmp/temp$ nm call.o
                       U _GLOBAL_OFFSET_TABLE_
      0000000000000000 T main
                       w myfunc
      
      这里我们先执行看看first.exe是否返回1而second.exe是否返回2,原因很简单first.exe毫无悬念的就是first.c里的定义,可是当first.o和second.o并列的时候,就是一个weak symbol和一个strong symbol并列的话,call.o只能调用strong symbol的实现,
      
      nick@nick-HP-Laptop:/tmp/temp$ gcc -g call.o first.o -o first.exe
      nick@nick-HP-Laptop:/tmp/temp$ ./first.exe
      nick@nick-HP-Laptop:/tmp/temp$ echo $?
      1
      nick@nick-HP-Laptop:/tmp/temp$ gcc -g call.o first.o second.o -o second.exe
      nick@nick-HP-Laptop:/tmp/temp$ ./second.exe
      nick@nick-HP-Laptop:/tmp/temp$ echo $?
      2
      
      nm返回W是有实现的weak symbol,而w是没有实现的weak symbol。

三月二十三日 等待变化等待机会

  1. 完全被带偏了,我本想继续探寻weak symbol,结果为了能够不使用任何头文件和任何的库的最简单的小程序被扯到很远很远回不来了。查了以前的笔记,但是我又不想增加一个汇编程序,纠结在如何使用-nostdlib和-nostartfiles上,但是我发现我对于这一块的认识很贫乏,首先直接调用syscall这个就是stdlib,而那个所谓的直接调用的宏_syscall已经被拿掉了,我找到一个用汇编实现这个宏的做法,这个可能是我今天找到的最接近的做法了。
  2. 我估计我的crash问题就在于weak symbol在链接的时候是不会检查是否有实现的,完全依赖于假设在动态运行库里存在,于是在动态库里并不报告找不到的问题。这是因为并没有强制要求解决所以根本不知道需要什么动态库。以下就是实例,你声明的这个weak symbol的函数myfunc,甚至都不需要extern,而链接的时候没有任何的抱怨,而我们看到可执行程序里这个myfunc是一个没有实现的weak symbol的标志w。这就是为什么crash的原因。
    
    $ cat call_no_definition.c
    __attribute__((weak))int myfunc();
    int main(){
    	return myfunc();	
    }
    $ gcc call_no_definition.c -o call_no_definition.exe
    $ nm call_no_definition.exe | grep myfunc
                     w myfunc
    $ ./call_no_definition.exe
    Segmentation fault (core dumped)
    
  3. 这个是我一直记不住的东西,就是怎么查看boot log: journalctl -b。我现在遇到的是启动会死机,那么在recovery模式可以启动,我比较了两个启动菜单差别主要就是显示模式。比如正常的grub菜单设定gfxmode $linux_gfx_mode 这里我找到了很好的帖子解释这个设置
    If this variable is set, it controls the video mode in which the Linux kernel starts up, replacing the ‘vga=’ boot option (see linux). It may be set to ‘text’ to force the Linux kernel to boot in normal text mode, ‘keep’ to preserve the graphics mode set using ‘gfxmode’, or any of the permitted values for ‘gfxmode’ to set a particular graphics mode (see gfxmode).
    者就对了,它实际上是影响kernel的参数vga= 但是继续看这里我又有些困惑,我应该不是32位的boot程序那么应该不需要直接设定set gfxpayload=1024x768之类的吧?
    另一个差别在linux [kernel-file] quiet splashrecovery nomodeset 这个linux的启动命令是kernel的loader吧?所以linux的确是grub的命令实际上就是load内核并且把所有参数传递给内核,所以,这个recovery的参数是内核参数
  4. 使用grub但是始终没有真的阅读与理解,我想一方面是很多基本相关的内容不理解,另一个方面就是没有时间没有意愿。这里是关于grub本身安装位置的选择
    The partition table format traditionally used on PC BIOS platforms is called the Master Boot Record (MBR) format; this is the format that allows up to four primary partitions and additional logical partitions. With this partition table format, there are two ways to install GRUB: it can be embedded in the area between the MBR and the first partition (called by various names, such as the "boot track", "MBR gap", or "embedding area", and which is usually at least 31 KiB), or the core image can be installed in a file system and a list of the blocks that make it up can be stored in the first sector of that partition.
    所以第一种就明白了在512byte之后到32k之间,通常这个部分是不放东西的因为on modern disks, it is often a performance advantage to align partitions on larger boundaries anyway, so the first partition might start 1 MiB from the start of the disk. 这就对了,我曾经无数次的看到所谓的虚拟机,BMC等等的img都是这样子的分成前面32k大部分是空,然后真正的文件系统block有时候是的确从1M开始的。这个在fdisk里很容易验证。不过这一点我很快打脸因为我自己的ubuntu应该是GPT而不是MBR,只不过为了兼容MBR在第一个512byte是放了假的兼容的数据而已吧?这个是《打脸的打脸!我明明就是MBR我却想当然的以为我当初配置的是GPT,我是怎么安装系统的现在一点也想不起来了! 所以判断系统的设置是: 回到当初的断言,我看到fdisk的输出:
    sudo fdisk -l /dev/sda
    ...
    Units: sectors of 1 * 512 = 512 bytes
    ...
    Device     Boot      Start        End   Sectors   Size Id Type
    /dev/sda1  *          2048  504104959 504102912 240.4G 83 Linux
    
    所以文件系统的起始位置是在2048*512/1024=1024Kib=1Mib。所以,这句话the first partition might start 1 MiB from the start of the disk是对的。
  5. 在GPT系统下当然是既可以使用BIOS启动也可以使用[U]EFI,但是核心的问题是grub要安装在一个安全的地方,那就是专门的分区
    Some newer systems use the GUID Partition Table (GPT) format. This was specified as part of the Extensible Firmware Interface (EFI), but it can also be used on BIOS platforms if system software supports it; for example, GRUB and GNU/Linux can be used in this configuration. With this format, it is possible to reserve a whole partition for GRUB, called the BIOS Boot Partition. GRUB can then be embedded into that partition without the risk of being overwritten by other software and without being contained in a filesystem which might move its blocks around.
    所以,以后安传如果使用GPT就会遇到了。
  6. 这里是gearny的快捷键,应该是很有用的。
  7. 我又一次的完全迷失了方向,本来是想做实验检验启动死机的问题,结果不知道跑到哪里去了。
  8. 有时候一个如此简单的问题我却想了好久不知道怎么做,比如你要得到之前的执行结果是$?,可是我想打印出来难道一定要使用pipe吗?直接用;不就行了吗?
  9. 不知道能不能先安装amd/ati的新驱动来实验一下?24.03.2021结果不理想,升级内核之后再次编译amdgpu驱动我只能说就一个字:烂!依然是花屏,以后买笔记本坚决不要amd的cpu了!cpu本身电源管理有问题会死机,结果自家的显卡同样是垃圾一样会导致死机,amd你为什么不去死?我决定放弃每次开机都是用拯救模式才行。不折腾了。

三月二十四日 等待变化等待机会

  1. gcc的代码真的是有妖娆吧,不过这个是对于后现代程序员而言吧?也许在老古董的眼里是正常的,比如你要定义一个函数类型,通常我们是定义函数指针类型而不是函数变量类型,比如typedef ReturnType(*FuncType)(ArgType...);这样子的话作为一个指针类型可以声明一个函数指针变量就能够随意使用了。但是假如你声明一个函数类型,它和指针类型也差不多,不过就是lvalue/rvalue之类的区别吧?但是有什么优势呢?以下代码是gcc的真实代码只不过为了保护隐私把结构和函数的参数省略了而已。Are you kidding me?其实这个在编译器不支持c++的年代人们幻想着有一种神奇的函数叫constructor或者有一种模式叫做factory。毕竟c++编译器是用c语言实现的,出现这个情况不值得大惊小怪的。
    
    struct gcc_cp_context{};  //a struct. ok.
    typedef struct gcc_cp_context *gcc_cp_fe_context_function();//a function type?!not pointer.
    extern "C" gcc_cp_fe_context_function gcc_cp_fe_context;//a function variable?
    extern "C" struct gcc_cp_context *gcc_cp_fe_context (){ //THE function.
    	return new gcc_cp_context;
    }
    #define GCC_CP_FE_CONTEXT gcc_cp_fe_context //why do you want this?
    
    这里说的很好:

    ...you can declare a function to take a function as a parameter ..., and the effect is to make the parameter a function pointer. This is similar to the way you can declare a function parameter that looks like an array, but is actually a pointer.

    The function argument can be the name of the function, with or without a & to explicitly take its address. If you omit the &, then there's an implicit function-to-pointer conversion. Again, this is similar to passing a (pointer to) an array, where the implicit array-to-pointer conversion means you only need to write the array's name, rather than &array[0].

  2. 表面上你明白了,一遇到实际问题立刻就暴露了无知与迷茫。什么是translation unit呢? 这个是我看到这个函数cp_parser_translation_unit的感受,当时风清扬传授独孤九剑给令狐冲的时候就只有九招,杨过心中暗喜以为很快就能学会,可是一接触才明白这九招里任何一招都是无比的绵密复杂,比如一招破剑式可能就有360式。这个函数也是如此,单单看语法就看得我头疼因为你有多少种声明的形式啊?
  3. 我以前总是心底里抱怨为什么gcc里c语言的parser和c++的parser不彻底分开,在我看来两种语言完全可以包装成两个独立的parser,这个想法也对也不对,gcc/g++已经是这么做了,可是对于还没有深入理解就妄加议论的人是不值一驳的,而当我看到实际的代码才意识到我有多么的大错特错,c++语言里经常会嵌套着extern "C"这个要求你不断的在c/c++语言之间切换,那么你怎么能够把c的parser分开呢?这是一个如此简单的道理可是如果不是真正接触的人可能是没有想到的吧?
  4. 我越看越觉得不可思议,因为第一很多我认为的代码函数根本没有被调用让我百思不得其解,没有办法只有求助于gdb,然而其中的奥妙更多。第一我还是不能不明白怎么在follow-fork-mode里的child/parent之间转换,总是卡在不知名的地方只好退出。其次,我感觉似乎info sources看到的代码文件还是parent的模式下。为了跳过这些麻烦事情我决定不用g++这个driver而是使用背后的cc1plus来执行,但是这样子参数是要改变的,从g++里拷贝下来运行,但是我发现其中的代码文件似乎都是编译时候产生的,我在源代码里根本就找不到比如很多的gt-cp-xxx.c这样的代码是编译期动态产生的吗?gcc的水很深啊。难怪llvm的大把大把的抱怨。
  5. g++和cc1plus的参数差别是这样子的,前者使用-c输出是obj文件,后者则是汇编文件.s。后者增加的参数有-imultiarch x86_64-linux-gnu -quiet -dumpbase -dD -D_GNU_SOURCE ../src/cppTest.cpp -mtune=generic -march=x86-64 -auxbase-strip
  6. 我在gcc目录下的Makefile找到一个target s-header-vars是产生这些代码文件的名字,但是它们的来源呢?就是说所有这一个目标gt的文件名都是前缀gt然后是目录名然后是源文件名,比如gt-cp-class.c,这个做法是为了能够把众多的代码文件拷贝到同一个目录下编译吗?这个简直要把人逼疯了,这个是故意这么做的吗?

三月二十五日 等待变化等待机会

  1. 在头条上看到一篇极好的文章,不过可惜是抄袭的。不过呢, 千里马常有,而伯乐不常有。即便是抄袭也是有功劳的,何况原作者前几天才发表就能被传播到中国而且还要改动代码翻译成中文,没有功劳也有苦劳啊!姑且原谅了。 这篇文章极其深奥,我几乎要反复理解一些最基本的概念,比如你有一个万能的类来对应一个类的成员变量 那么使用structure construction来生成一个任意类要怎么做呢? 比如:
    struct MyPoint{int x, y, z; string name};
    对于这个类我想使用这样的ctor来生成
    Point p{Anything{},Anything{},Anything{},Anything{}};
    这个编译是通不过的因为抱怨
    undefined reference to `Anything::operator int<int>() const'
    当然对于最后一个成员变量string就是`Anything::operator int<string>() const',你当然可以在Anything里加上一个简单的实现就如被我注释的那样{return T();},可是对于复杂类型这个就不一定行了,而实际上我们不需要真的创建这么一个变量,因为我们仅仅需要这个Point的一个decltype比如 以下这个是可以的 尽管我们只给了两个参数给Point的ctor依然是成立的因为对于没有explicit的ctor这个类似于数组的Brace-enclosed initializers给几个参数都是可以的只要不超过。而且由于是decltype编译器根本不去实例化参数只是检查参数个数,所以以至于连这个非法的void(0)也可以蒙混过关立刻被打脸,只有实践是检验真理的唯一标准,在c++只有编译器是检验理论的唯一标准,这里的void(0)不是用来给Point作参数的而是在(void(0),Anthing{})这个()操作符里的所谓初始化部分,因为逗号,前面的是初始化,只有逗号,后面的Anthing{}才是唱主角的返回值。所以它等价于Point{Anthing{}};,何况名门正派Antything呢?这就是为什么这个类的模板函数operator T() const根本没有模板参数也可以因为编译器压根不关心!这里的核心是编译器只关心参数的个数比如 MyStruct只能接受一个参数因为它的成员变量就是一个,你给两个编译器就发现了,这里编译器是只做类似语法的检查,decltype/static_assert并不需要产生目标代码。 总而言之,作者就是利用了这个来检查类的成员变量的最大个数
  2. 对于代码 里的T{(void(Is), detail::Anything{})...};要怎么理解呢?这里的(,)...是什么神操作?我又一次想了好久才回忆起来以前就反复遇到过,...Brace-enclosed initializers代表使用函数参数里的parameter pack来拓展(expand),至于说(,)这个是惯常的比如摘录这个例子在初始化数组的时候顺便打印每个值0
  3. 在搜索关于void(0)是什么的时候发现的,我已经忘怀了(void)0
    (void)0 (+;) is a valid, but 'does-nothing' C++ expression, that's everything. It doesn't translate to the no-op instruction of the target architecture, it's just an empty statement as placeholder whenever the language expects a complete statement... the (void) prevents it from being accidentally used as a value
  4. 有时候我以为我理解了怎么使用index_sequence,但是一到使用就立刻晕了,为什么我一定要specialization一个模板才可以使用呢?index_sequence到底是实参还是型参?我花了好久最后也得到了类似的结果,可是我还是不明白,这里的解释也不是很透彻。 我首先一定要声明一个万能的模板类 然后再specialization的时候把index_sequence作为模板参数传递。我觉得这个和用模板函数传递它作为实参的效果似乎是异曲同工,总之index_sequence实际上是相当于实参而不是型参吧? 这里的using虽然是alias实际上和声明实例没有区别。 这里就是结果能够打印出来数组的成员0,1,2,3,4,5,6,7,8,

三月二十六日 等待变化等待机会

  1. 昨天被带偏了,那个reflection是非常棒的东西,可是我还是要专心致志推进cc1plus的探索。
  2. 据说每一个部件都有所谓前端后端的相对概念,比如g++也许所是cc1plus的前端或者说driver吧,它依靠所谓的lang_hook来驱动parsing work。而这些针对语言的language hook,类似于抽象类接口的实现一样把各个语言的差异性隐藏起来做到同质化的一个调用,比如一个通用方法parse_file不带参数。而这些有差异的实现的初始化依靠大量的宏,这个是没有办法的办法,因为第一个c++的parser是依靠c实现的,这里都是c的代码,尽管后来有了新版的编译器可以实现这些也没有人愿意改动因为怕出错,而且我始终有一个担心就是使用最新的c++的feature去编译c++编译器是否是一个螺旋的错误,如果有某种很深藏的bug导致编译器有某种bug,而导致编译出来的编译器根本被隐藏了就永远发现不了了,因为很多时候我们测试是基于编译器的行为,有些测试是独立的第三方的是基于输入输出的,可是有些测试是基于新版编译器是否符合当前编译器的期待输出的,这个时候如果编译器本身就是错的那么结果怎么样呢?错上加错?这些接口定义在langhooks-def.h居然是一个头文件,真的是进步啊,昨天看到头条的文章有人惊叹居然c/c++的include可以引用任何类型的文件,这个明显是windows程序员的固疾因为被操作系统固化了思维的人会把文件类型和后缀名等同化的结果,一个人在某种体制下束缚太久就会形成惯性思维和路径依赖,这就是所谓的天经地义理所当然的感想的由来,事物文件不以它们的外表或者说文件名来决定,而是由文件的内容决定的,这一点linux的工具file就是做这个工作的,可惜不是所有的应用都能够遵循这个原则,毕竟集成这个工具需要不少的力气而且他本身也没有办法做到那么完美。
  3. 我对于之前gcc编译修改代码文件名字的猜测有些动摇了,因为虽然我在info sources里看到的代码文件名是被修改的gt-cp-xxx.c但是在具体设定断点后gdb却依旧显示了正确的文件名并找到了代码,这真的是奇迹也让我动摇了之前的猜测。也许那些被修改的文件名是其他的编译目标而设定的吧?可是为什么会存在于同一个可执行文件内呢?这个似乎是不可能的吧?
  4. 所谓的lanhooks是定义了一系列的接口并使用宏来初始化,其中有些是所谓的c和c++通用的部分就放在了c-family这个目录下的所谓的common-xxx.c里,比如最重要的接口LANG_HOOKS_PARSE_FILE被c++hook定义为了c_common_parse_file这一点是让我花了不少时间才明白因为很多时候代码文件名字是会误导人的,比如cp-objcp-common.h我就误以为里面都是objectiveC++的相关定义,所以当我只看到这里把这个接口LANG_HOOKS_PARSE_FILE定义为c_common_parse_file而找不到其他gnu/c++的定义时候就让我感到惶恐了以为自己理解错误了。文件名不副实是一个gcc的大问题。 我要记录下这个parser的流程
    
    #0  c_parse_file () at ../../gcc-10.2.0/gcc/cp/parser.c:43993
    #1  0x0000000000c28c2c in c_common_parse_file () at ../../gcc-10.2.0/gcc/c-family/c-opts.c:1190
    #2  0x000000000139cf47 in compile_file () at ../../gcc-10.2.0/gcc/toplev.c:458
    #3  0x00000000013a01f2 in do_compile () at ../../gcc-10.2.0/gcc/toplev.c:2278
    #4  0x00000000013a0500 in toplev::main (this=0x7fffffffdac6, argc=33, argv=0x7fffffffdbc8)
        at ../../gcc-10.2.0/gcc/toplev.c:2417
    #5  0x00000000022e4bbb in main (argc=33, argv=0x7fffffffdbc8) at ../../gcc-10.2.0/gcc/main.c:39
    
    我现在一个感觉是真正的行家关注的是passes因为这个可能才是所有的逻辑算法的核心部分,它本身不是实现的部分,但是它是所有实现的总调度相关的部分,可能也是最变化多端的部分吧?我应该在到达它之前就远远的停下来。我的感觉是parsing仅仅是很小的一部分工作也许有可能把其中相关的文件找出来,但愿gcc不像llvm的支持者说的那么不堪,我现在对于这些人的言论是半信半疑的。
  5. 我记得我很久以前就看过这篇文章,但是现在应该有不同的理解吧?
  6. 有一条路是在cppTest的基础上继续cc1plus,但是我想先保存一个版本,顺便整理一下思路。所以上传到了github

三月二十七日 等待变化等待机会

  1. 几乎每次读代码注释我都会发现一些新大陆大惊小怪,比如我刚刚发现有些代码是自动产生的这不是大惊小怪是什么?这个文件名叫做genmodes.c实际就是一个小的可执行程序作为工具使用一个machmode.def作为输入产生新的代码头文件和代码文件,这也就是很多时候我死活找不到一些函数的定义的原因。其实这个简单的事情我早应该想到了,比如在编译路径下为什么会有那么多的.c/.h文件呢?不是编译时候产生的难道是拷贝来的?这个不是显而易见的吗?所以gcc的困难不是普通程序员所能想像的,对于很多以为天经地义的事情在编译器的实现阶段是一个未知数。
  2. 其实这一切对我来说是很实际的问题:假如我断章取义的从中间截取调用的流程的一部分而没有充分还原初始化的种种在我看来不可理解的步骤是否会有难以令人察觉的错误?因为程序crash对于任何程序员来说都是最幸运的,而怕的就是莫名其妙的垂而不死不知道在多少时间后在匪夷所思的地方炸尸还魂,这才是让人揪心。
  3. 将不胜其忿而蚁附之,杀士三分之一而城不拔者,此攻之灾也。
    目前的局势就是美国在逼迫中国尽快在台湾问题上摊牌,能否有足够的时间去准备即将到来的不可避免的战争的决定必须在现在做出。
  4. c-cppbuiltin.c里我看到了我以前寻找不到的众多的cpp_define的来源。c_cpp_builtins有大量的事情要去做,我之前的仅仅是简单的预处理因此很多即便不定义也不会出大错吧,或者根本看不出来除非你要把结果作为编译器的输入才会暴露。
    • 首先是和语言无关的定义define_language_independent_builtin_macros 包含define__GNUC__具体就是先从一个BASE-VER的文件里读取gcc的版本信息:major, minor, patchlevel
      
      cpp_define_formatted (pfile, "__GNUC__=%d", major);
      cpp_define_formatted (pfile, "__GNUC_MINOR__=%d", minor);
      cpp_define_formatted (pfile, "__GNUC_PATCHLEVEL__=%d", patchlevel);
      cpp_define_formatted (pfile, "__VERSION__=\"%s\"", version_string);
      
      其中
      const char version_string[] = BASEVER DATESTAMP DEVPHASE REVISION;
      后面这些memory model我很陌生
       
      cpp_define_formatted (pfile, "__ATOMIC_RELAXED=%d", MEMMODEL_RELAXED);
      cpp_define_formatted (pfile, "__ATOMIC_SEQ_CST=%d", MEMMODEL_SEQ_CST);
      cpp_define_formatted (pfile, "__ATOMIC_ACQUIRE=%d", MEMMODEL_ACQUIRE);
      cpp_define_formatted (pfile, "__ATOMIC_RELEASE=%d", MEMMODEL_RELEASE);
      cpp_define_formatted (pfile, "__ATOMIC_ACQ_REL=%d", MEMMODEL_ACQ_REL);
      cpp_define_formatted (pfile, "__ATOMIC_CONSUME=%d", MEMMODEL_CONSUME);
      
    • 接下来的是
       
      define_builtin_macros_for_compilation_flags (pfile);
      define_builtin_macros_for_lp64 (pfile);
      define_builtin_macros_for_type_sizes (pfile);
      这些里有一些是依据用户传入参数来设定的,很不好处理。同时我注意到使用cpp_define_formatted是更好的选择。
    • 此外是这个全局变量cxx_dialect的相关设定,它的值是在c_common_post_options设定的,和这个c++方言相关的东西是非常微妙的,没人敢乱来。
    总而言之,parser的初始设定比cpp复杂多了,很不容易看清楚。
  5. 所以有了昨天关于c-opts.c:c_common_parse_file是怎么成为lang_hook的回调函数的概念后就明白要集中看这个函数的逻辑。同时看到了c-opts.c:c_finish_options里是怎么初始化cpp_reader的line_table的,这里使用linemap_add而文件名使用<built-in>并且要设定cpp_force_token_locations (parse_in, BUILTINS_LOCATION);是什么用意呢?是说明这些设定的宏cpp_define都是不属于任何文件的而是builtin的?而随后的类似的<command-line>是添加用户定义的宏就是-D, -U,-A这个最后的自问自答的功能是很强大的,你可以询问cpp问题,它就cpp_assert那个宏回答你。真的是很神奇。

三月二十八日 等待变化等待机会

  1. 连链接都不成功就不要指望有什么可能了,我决定从头一步一步检查c_compile_file的所有的需求。首先我注意到libcpp的链接需求,这里有一个困惑我很久的疑问就是它在gcc编译过程中有两个编译的版本,一个在源代码的libcpp下,一个在目标机器的build-x86_64-pc-linux-gnu下,二进制文件不同,但是包含的obj文件个数至少是一样的,那么检查两者的Makefile我看到很少量的变化好像是和多语言的差别以及链接的库有差别,比如-static-libstdc++ -static-libgcc这个也许关系不大吧?至少我觉得这个是有运行库和没有运行库的差别,具体差别哪里在好像是异常处理吧?我又忘了。总之可能没关系因为在源代码的libcpp目录下有这些额外的库的链接问题不大。
  2. 我在链接静态库的时候有些迷惑不知道为什么定义的symbol总是说找不到于是我尝试-Wl,--whole-archive结果遇到这个错误
    libgcc.a(_muldi3.o): In function `__multi3':
    (.text+0x0): multiple definition of `__multi3'
    ,这条路似乎走不通。
  3. 我现在意识到链接thin archive的办法是--whole-archive,可是在遇到libgcc.a有众多的multiple definition后我在尝试--start-group archives --end-group

    The archives should be a list of archive files. They may be either explicit file names, or `-l' options.

    The specified archives are searched repeatedly until no new undefined references are created. Normally, an archive is searched only once in the order that it is specified on the command line. If a symbol in that archive is needed to resolve an undefined symbol referred to by an object in an archive that appears later on the command line, the linker would not be able to resolve that reference. By grouping the archives, they all be searched repeatedly until all possible references are resolved.

    Using this option has a significant performance cost. It is best to use it only when there are unavoidable circular references between two or more archives.

    我想这个是我长久以来一直想找到的,但是目前对于我的问题似乎没有效果
  4. 慢慢的我也许有点明白我的问题不仅仅是链接的问题,实际上也许是gcc本身的问题,那么多的symbol如果彼此关联纠缠不清你能否模块化的编译链接呢?这个让我回忆起llvm的所谓的模块化,我假如给你把每个所谓的目录下的obj都编译成一个静态库然后告诉你可以按照模块化来随心所欲的选择链接一个个的静态库,你是否如我当初一样的天真相信这个是真的?如果能够使用静态库就解决逻辑代码里的相关性人类编程岂不是不用再去发明种种的规范设计直接在二进制码上就能实现?这个不是天大的笑话吗?静态库编译过程没有暴露的依赖性就以为省却了动态库解决的烦恼是给初学者的假象,所有的依赖最后都要解决,只不过静态库留作将来而动态库必须在链接的时候最起码要有一个symbol解决的安慰。所以,在gcc繁杂的各个obj里不是简单的搜索到那个symbol就把它添加到你需要的静态库里那么简单,在没有c++namespace的c语言里重复的symbol名字是你的噩梦,更何况gcc努力模仿多态故意在不同的实现里使用相同的函数名字,这个才是你的噩梦的开始因为你作为一个不了解整个框架的前提下去寻找唯一正确的可能性,而这个道路之所以连设计者都不提供的可能性是它的不可能。这个也许就是为什么退而求其次的libcp1.so/libcp1plugin.so的做法,因为在如蜘蛛网般复杂的迷宫里没有人能够依靠希腊神话里的一根线走出来。连迷宫的设计者都不可能做到,这个也许就是他们为什么已经放弃了。
  5. 完全陷入了黑暗之中。。。

    Galadriel: Farewell, Frodo Baggins. I give you the light of Earendil, our most beloved star.

    Galadriel: May it be a light for you in dark places, when all other lights go out.

    TL;DR

    • 我想从gcc的编译流程来找出线索,首先这个目录结构就够复杂的了
      
      ├── build-x86_64-pc-linux-gnu
      │   ├── fixincludes
      │   ├── libcpp
      │   └── libiberty
      │       └── testsuite
      ├── fixincludes
      ├── gcc
      │   ├── ada
      │   │   └── gcc-interface
      │   ├── analyzer
      │   ├── brig
      │   ├── build
      │   ├── c
      │   ├── c-family
      │   ├── common
      │   ├── cp
      │   ├── d
      │   ├── doc
      │   ├── fortran
      │   ├── go
      │   ├── include
      │   ├── include-fixed
      │   │   ├── boost
      │   │   │   └── predef
      │   │   │       └── os
      │   │   ├── root
      │   │   │   └── home
      │   │   │       └── nick
      │   │   │           └── opt
      │   │   │               └── gcc-10.2.0
      │   │   │                   └── include
      │   │   │                       └── c++
      │   │   ├── X11
      │   │   └── x86_64-linux-gnu
      │   │       └── c++
      │   ├── jit
      │   ├── lto
      │   ├── objc
      │   └── objcp
      ├── gmp
      │   ├── cxx
      │   ├── demos
      │   │   ├── calc
      │   │   └── expr
      │   ├── doc
      │   ├── mpf
      │   ├── mpn
      │   ├── mpq
      │   ├── mpz
      │   ├── printf
      │   ├── rand
      │   ├── scanf
      │   ├── tests
      │   │   ├── cxx
      │   │   ├── devel
      │   │   ├── misc
      │   │   ├── mpf
      │   │   ├── mpn
      │   │   ├── mpq
      │   │   ├── mpz
      │   │   └── rand
      │   └── tune
      ├── intl
      ├── isl
      │   ├── doc
      │   ├── imath_wrap
      │   └── include
      │       └── isl
      ├── libbacktrace
      ├── libcc1
      ├── libcpp
      ├── libdecnumber
      ├── libiberty
      │   ├── pic
      │   └── testsuite
      ├── lto-plugin
      ├── mpc
      │   ├── doc
      │   ├── src
      │   └── tests
      ├── mpfr
      │   ├── doc
      │   ├── src
      │   ├── tests
      │   └── tune
      ├── x86_64-pc-linux-gnu
      │   ├── libatomic
      │   │   └── testsuite
      │   ├── libgcc
      │   ├── libgomp
      │   │   └── testsuite
      │   ├── libitm
      │   │   └── testsuite
      │   ├── libquadmath
      │   │   ├── math
      │   │   ├── printf
      │   │   └── strtod
      │   ├── libsanitizer
      │   │   ├── asan
      │   │   ├── interception
      │   │   ├── libbacktrace
      │   │   ├── lsan
      │   │   ├── sanitizer_common
      │   │   ├── tsan
      │   │   └── ubsan
      │   ├── libssp
      │   │   └── ssp
      │   ├── libstdc++-v3
      │   │   ├── doc
      │   │   │   └── xsl
      │   │   ├── include
      │   │   │   ├── backward
      │   │   │   ├── bits
      │   │   │   ├── debug
      │   │   │   ├── decimal
      │   │   │   ├── experimental
      │   │   │   │   └── bits
      │   │   │   ├── ext
      │   │   │   │   └── pb_ds
      │   │   │   │       └── detail
      │   │   │   │           ├── binary_heap_
      │   │   │   │           ├── binomial_heap_
      │   │   │   │           ├── binomial_heap_base_
      │   │   │   │           ├── bin_search_tree_
      │   │   │   │           ├── branch_policy
      │   │   │   │           ├── cc_hash_table_map_
      │   │   │   │           ├── eq_fn
      │   │   │   │           ├── gp_hash_table_map_
      │   │   │   │           ├── hash_fn
      │   │   │   │           ├── left_child_next_sibling_heap_
      │   │   │   │           ├── list_update_map_
      │   │   │   │           ├── list_update_policy
      │   │   │   │           ├── ov_tree_map_
      │   │   │   │           ├── pairing_heap_
      │   │   │   │           ├── pat_trie_
      │   │   │   │           ├── rb_tree_map_
      │   │   │   │           ├── rc_binomial_heap_
      │   │   │   │           ├── resize_policy
      │   │   │   │           ├── splay_tree_
      │   │   │   │           ├── thin_heap_
      │   │   │   │           ├── tree_policy
      │   │   │   │           ├── trie_policy
      │   │   │   │           └── unordered_iterator
      │   │   │   ├── parallel
      │   │   │   ├── pstl
      │   │   │   ├── tr1
      │   │   │   ├── tr2
      │   │   │   └── x86_64-pc-linux-gnu
      │   │   │       ├── bits
      │   │   │       │   ├── extc++.h.gch
      │   │   │       │   ├── stdc++.h.gch
      │   │   │       │   └── stdtr1c++.h.gch
      │   │   │       └── ext
      │   │   ├── libsupc++
      │   │   ├── po
      │   │   ├── python
      │   │   ├── scripts
      │   │   ├── src
      │   │   │   ├── c++11
      │   │   │   ├── c++17
      │   │   │   ├── c++98
      │   │   │   └── filesystem
      │   │   └── testsuite
      │   └── libvtv
      │       └── testsuite
      └── zlib
      
    • 可是怎么知道编译的流程呢?我没有找到编译的log文件,手册上也没有明确的说明,于是我只能依靠很多目录下的config.log的生成时间来判断,使用查找并排序。
      
      find . -name config.log -print0 -printf " %T@\\n" > /tmp/sort.txt
      sort -k2 </tmp/sort.txt
      ./config.log 1615674928.7171014080
      ./lto-plugin/config.log 1615674968.0291659660
      ./build-x86_64-pc-linux-gnu/fixincludes/config.log 1615674970.5451701210
      ./fixincludes/config.log 1615674970.6131702330
      ./intl/config.log 1615674971.4371715940
      ./build-x86_64-pc-linux-gnu/libcpp/config.log 1615674974.5651767660
      ./zlib/config.log 1615674977.0011807960
      ./libdecnumber/config.log 1615674977.6731819090
      ./libbacktrace/config.log 1615674981.9291889570
      ./build-x86_64-pc-linux-gnu/libiberty/config.log 1615674983.9651923320
      ./libiberty/config.log 1615674985.6411951110
      ./gmp/config.log 1615674998.8252170150
      ./libcpp/config.log 1615674999.9772189320
      ./isl/config.log 1615675034.9972774730
      ./mpfr/config.log 1615675044.2732930590
      ./mpc/config.log 1615675067.7333326220
      ./gcc/config.log 1615675103.7213936950
      ./libcc1/config.log 1615675121.8934247010
      ./x86_64-pc-linux-gnu/libgcc/config.log 1615675507.0301019860
      ./x86_64-pc-linux-gnu/libssp/config.log 1615675539.0661596290
      ./x86_64-pc-linux-gnu/libquadmath/config.log 1615675542.0421649930
      ./x86_64-pc-linux-gnu/libatomic/config.log 1615675546.8101735870
      ./x86_64-pc-linux-gnu/libgomp/config.log 1615675550.9061809720
      ./x86_64-pc-linux-gnu/libstdc++-v3/config.log 1615675613.7582945880
      ./x86_64-pc-linux-gnu/libvtv/config.log 1615675715.7944800520
      ./x86_64-pc-linux-gnu/libitm/config.log 1615675719.9984877170
      ./x86_64-pc-linux-gnu/libsanitizer/config.log 1615675720.6864889720
      
      至少目前我可以明确知道libcpp被编译了两次,第一次在build-x86_64-pc-linux-gnu比我想像的更早,做什么用就不知道了,看看Makefile吧?
      # For stage1 and when cross-compiling use the build libcpp which is
      # built with NLS disabled. For stage2+ use the host library and
      # its dependencies.
      很明显的第一次是为了做stage1或者cross-compilation,我是本地编译,所以,stage1是必须的?什么是stage1?什么是NLS?查到了是Native Language Support(NLS),这个就对了和我早上查看到的编译的makefile里没有各种语言的msg的情况是吻合的。诚不我欺也。
      我看到了我感兴趣的几个编译的先后顺序,但是依旧摸不着头脑,不过我想最主要的编译故事都发生在gcc目录下,探索的重点应该是那里。
    • 一个坏消息是如果我使用我自己编译的gcc-10.2.0的话最后的链接阶段报出错误,我决定使用官方的enable-bootstrap的版本再实验一下,也许我的disable-bootstrap的确有问题?看来官方的编译方法没有问题,也许是我的bypass bootstrap的编译方法有问题吧?那么我再实验一下吧。现代软件早已发展到如此复杂的地步,很多时候一个人的能力极其有限,可能就是无数条流水线上无数的装配工的一员,如果他还算称职的话也不过是站在流水线上机械的拧螺丝干着日复一日的CRUD工作,偶尔有什么心得也许就是在拧螺丝的过程里发现了从什么角度切入拧螺丝的效率最大化而已。正如很多公司非常的骄傲的宣称他们的产品复杂到给了你源代码或者设计图纸你也未必看得懂,造的出来的地步了。我现在又无法重现那个错误了,这个导致一个问题,同样的编译器代码用不同的编译器编译出来的编译器是否是等价的?或者说任何应用用不同的编译器应该要编译出相同的应用,否则就是编译器的regression了。
  6. 我在想也许这个是一种测试方法就是说先用自己编译的编译器再次编译一个编译器如此循环往复,假如有任何微小的错误应该都会放大以至于轻易发现吧?不过这个是很幼稚的想法我觉得不需要这种迭代就应该能够发现错误,但是我自己的编译器再次编译自己就出错让我还是比较惶恐。实际上每次编译的时候都是有一系列的测试的,所以我编译两次也许和stage1/stage2等价吧?我也不知道对不对。顺便说一下编译指定编译器就是设定CC=...和CXX=...
  7. 很喜欢乌合麒麟我做了一个小游戏把他的画里的要点标记出来,看你能不能都找出来!,我以为他代表了未来的艺术发展方向,那就是数字艺术,现实与艺术的不着痕迹的完美结合,大有文艺复兴时代绘画写实风格的意味。这幅Better Cotton Initialive尤其好。 这幅画里有很多的关键点,看你能不能都找出来呢?

三月二十九日 等待变化等待机会

  1. 我试图从gcc/Makefile寻找一些线索,其中大部分可以看得很明白是做什么,比如通常的安装,文档,测试等等这些我不关心,但是遇到大量的所谓的generated的部分就很不理解,尤其是反复出现的什么GTY是什么东西呢?GGC是garbage collection这个我早就知道可是。。。还有总是出现的什么OMP之类的似乎和并行运行相关的东西。
    
    # The gengtype generator program is special: Two versions are built.
    # One is for the build machine, and one is for the host to allow
    # plugins to define their types and generate the supporting GGC
    # datastructures and routines with GTY markers.
    
    这些电邮就如听天书一样毫无概念。终于找到了原来如此

    gengtype

    As C does not have any means of reflection (and even C++98 does not have enough reflective abilities), gengtype was introduced to support some GCC-specific type and variable annotations, which in turn support garbage collection inside the compiler and precompiled headers. As such, gengtype is a one big kludge of a rudimentary C lexer and parser.

    终于明白了为什么!这个是很好的第一步,至于是什么和怎么样就是以后的工作了。
  2. 仿佛打开了一道闸门立刻信息的洪流奔涌而出,如果用诗来表达心情就是忽如一夜春风至,千树万树梨花开。这里就解释了我一直的困惑GTY marker是什么东西的疑惑。
    ...It is explicitly triggered by a call to ggc_collect and not by allocations. It is implemented in files gcc/ggc.h and gcc/ggc-*.c. Pointers (variables, fields, ...) are explicited (thru the GTY(()) marker), and marking and scanning routines are generated.
    这里是直接回答了我之前关于众多的gt-xxx.c的文件的产生的疑问。
    ... be aware that some additional C files (eg gt-tree-FOO.h or gtype-desc.c) are generated in the object (build) tree (but not in the GCC source tree).
  3. 这里有详尽的解读那个古怪的GTY的所谓的marker的语法及其用法,之前我对于这个东西百思不得其解是干什么的,甚至于对于他的语法正确性都表示怀疑。但是对于这个做法的必要性我还是不太清楚,只不过当初编译llvm耗费的巨大内存是令我印象深刻的,如今已经是2021年了不知道能否编译器使用一些现代c++的功能来解决这个问题呢?比如这个我一无所知的部分。当然我的理解是最主要的还是c/c++缺乏reflective机制,所以,垃圾回收不是大问题,大问题是如何知道一个类和其成员的类型等等的信息。

三月三十日 等待变化等待机会

  1. 明白了ggc的大概机制给了我信心,这个也许是最大的帮助因为它让我明白了所有的gt-xxx.h的头文件的用意,所以,我继续重复机械的工作,我决定把cp,c-family目录下的obj编译成两个新的静态库,然后连同原来的gcc下的libbackend.a来进行链接。这个决定是在我无休止的重复了几个小时的手工徒劳的一个一个symbol仅仅解决之后的无奈选择,因为我发现gcc目录下有四百多个obj文件,我手动添加了几个小时才六十几个,我当初的想法是找到一个最小的集合的obj文件,现在看来是不可能的,因为gcc的代码错综复杂全部都关联了。现在需要做的是在libbackend.a的基础上再去添加需要的obj。

    终于我总算把一个看似简单的函数c_compile_file的所有symbol解决了,这个是静态库的顺序

    -lparser -lcfamily -lgcc_main -lcpp -lcommon-target -lcommon_special -lbacktrace -liberty -lisl  -lmpc -lmpz -lmpfr -lgmp -ldecnumber -lz  -ldl
    其中parser是我创建的cp下的所有的.o的静态库,cfamily是c-family下的所有的.o的静态库,而common_special是一个非常特殊的处理,它来自于libcommon.a但是我去除了ggc-none.o这个是因为我需要ggc-page.o可是它们两个里有很多函数是重复定义了,我的理解是libcommon.a是给所有不需要ggc的模块使用的,其中的关于ggc的细节实现都是一个空函数,但是作为gcc的实际的实现需要像ggc-page.c里那样的实现。

    这里最核心的库是libgcc_main.a它是怎么制作的呢?我是在原来的thin achive:libbackend.a的基础上制作的,首先,要吧thin archive转成普通的full/current archive。

    for i in $(ar t libbackend.a); do ar rvs libbackend_real.a "$i"; done
    然后,在这个基础上添加了下列的.o文件
    
    ar t libbackend_real.a | sort > /tmp/back.txt
    ar t libgcc_main.a | sort > /tmp/gcc.txt
    comm --check-order -3 /tmp/back.txt /tmp/gcc.txt
    	attribs.o
    	cc1plus-checksum.o
    	glibc-c.o
    	i386-c.o
    
    就是说我一个早上就添加了这么四个文件?这好像不对吧?

  2. 魔鬼在细节,当我回忆我是怎么把原本的libbackend.a改造为我所需要的libgcc_main.a的时候,我才意识到thin archive和full/current archive的一个重大差别在于它的文件名是full path,因为
    A thin archive only contains a symbol table and references to the file. The file format is essentially a System V format archive where every file is stored without the data sections. Every filename is stored as a "long" filename and they are to be resolved as if they were symbolic links.
    所以当我反复想要重新创建一个current archive拥有full path的时候是不成功的,这里的an pageP是可能的,也许我理解错误吧?
    P

    Use the full path name when matching names in the archive. GNU ar can not create an archive with a full path name (such archives are not POSIX complaint), but other archive creators can. This option will cause GNU ar to match file names using a complete path name, which can be convenient when extracting a single file from an archive created by another tool.

    所以这个就是英语母语的优势,我又一次理解错误,这个只有当创建工具支持full path的时候才有意义,而gnu是不支持的所以我是徒劳的,我看到的是thin archive的类似的symlink,换句话说当我要转换的时候是不可能full path name的。
  3. 我对于怎样链接thin archive依然不是很清楚,之前我以为是--whole-archive,但是我现在不确定了,因为这个明显是我的理解错误
    --whole-archive

    For each archive mentioned on the command line after the --whole-archive option, include every object file in the archive in the link, rather than searching the archive for the required object files. This is normally used to turn an archive file into a shared library, forcing every object to be included in the resulting shared library. This option may be used more than once.

    这个英文讲的是什么意思?就是说这个是转为动态库才有用的,因为动态库要解决所有的symbol?但是为什么我不理解,总之和thin archive没有半毛钱关系!
  4. 对于链接thin archive我的理解是必须在project当地才能做到因为它的symlink应该是编译时候在当地的相对路径,那么我从编译目录以外来链接就相当于分发了,这个是做不到的。所以,放弃吧。

三月三十一日 等待变化等待机会

  1. 昨夜西风凋碧树。我打算调整小目标打算先从提高cpp做起,在理解提高之后再跨越到parser的阶段,这么做的跨度就小多了。
  2. 我打算从最简单的做起就是直接使用gcc的driver并传递参数,这个就是原汁原味的了,可是我还是发现我对于gcc的机制不清楚,比如只有gdb之后才明白谁是入口程序。你搜索会看到好几个main,比如真正的是gcc-main.c它定义了一个driver来执行这个类的成员函数main,接下去是gcc.c里的driver::main做所有的初始化设置,这里我尤其感兴趣的是搜索路径的函数这里有很多的关于spec的解析查找的逻辑很繁琐的,但是非常的必要。但是这个所谓的gcc.c不是backend,它属于frontend,所以,我的程序基本上属于backend,要怎么把前端的逻辑包含进来呢?直接拷贝这几个函数吗?这样的意义在哪里呢?我们如果要更好的控制就必须重新整理前端后端的逻辑代码,而这样做的意义难道变成了把参数解析为一个全局的context之类的大的结构体因为gcc令人头疼的是无数的全局变量!可是这个工作不应该这么做啊!矛盾啊。
  3. driver的一项工作在这里似乎就是选择所谓的compiler程序来具体执行,比如从可执行文件名字来判断调用哪一个程序,如果不是绝对路径那么就是从正常的程序搜索路径来找,所以这就是我以前很头疼的fork无法跟踪的问题。然后就是所谓的main.c里的所谓的toplev的类摒弃又一次执行它的所谓的成员函数的main。这里我看得困死了。
  4. 还是有些收获的因为至少对于gcc的前端有了比较清晰的理解了。

四月一日 等待变化等待机会

  1. 看代码是最辛苦的,尤其是跟踪gdb结合着看gcc的代码,前端其实大家还是都能看得懂,因为估计后端产生目标码是基本不能看的懂的因为需要很多目标机器的知识吧?这一点根本就没有指望的。但是尽管前端代码不是很深奥,可是没有睡好看得头晕想吐。首先一个任务是确定所谓的compiler的概念,就是说前端cpp/as/cc1/cc1plus/ar/collect2等等要选择哪一个这个是前端的一个任务,说起来也很简单但是很啰嗦,首先当然是用户呼叫的可执行程序是那个,听上去像是愚人节的费话,但是其实也不然,取决于你呼叫的是全路径的可执行程序还是文件名,这个我现在又有些模糊了,因为在strace里看到的也许是bash在解析,总之这里要解析的是前端的driver这个类要根据或者用户参数明确指明的语言,比如-x c++,或者就是根据输入文件名的后缀来决定所谓的compiler是谁?而这个逻辑又是依赖于所谓的spec来决定的,我是昨天跟踪代码才看到这个是所谓的spec里的execute相关的部分主要指明了所有的编译器中每个对应的输入文件的后缀名,而前端的工作就是搜索spec之后选择spec里的execute-spec里对应的符合的后缀名的编译器。所以,在这个之前寻找正确的spec就是一个很大的问题,这个搜索路径我一直看得不是很清楚其中牵涉到各种目标机器以及版本的组合,这一套逻辑很啰嗦因为spec里各自对应有的要版本号有的不要,如果找不到就使用默认的builtin spec。所以单单从这一点来看前端就已经很复杂了因为它也许是对应了多种支持的语言的比如ada/fortune/objectivec[++]/c[++]等等,加上之前每种语言的流水线上的不同的阶段的所谓编译器这个组合可能有几十吧。单单解析spec就是一个头疼的事情,我至今对于spec的结构依然一无所知,但是看起来至少前端的逻辑几乎是不怎么更新的除非c++的新规范出炉里面要配置所谓的各种新开关,我估计这部分的代码几乎是不变的因为它是纯粹对后端可执行程序的fork的调用根本没有任何的关联性是依靠文件传出结果的,这个是最小的耦合性了。而且现在我才意识到使用spec可以轻松改变编译器后端而无需重新编译前端,这个我以前模模糊糊看到过各路神仙使用spec来调用包括gdb在内的前端来运行各种神奇的变化,现在真正的开始有意识了其中的一个粗浅的道理。
  2. 我想先订立一个可以实现的小目标一步一步前进吧,比如先从spec的搜索路径开始,这个是最容易的了吧?

    TL;DR

    • 首先存储搜索路径的结构是所谓的path_prefix
      
      struct path_prefix
      {
        struct prefix_list *plist;  /* List of prefixes to try */
        int max_len;                  /* Max length of a prefix in PLIST */
        const char *name;           /* Name of this list (used in config stuff) */
      };
      
      这里的max_len应该是指的链表的个数才对吧?
    • 有预定义的我眼花了怎么看成四行了?大搜索路径
      
      /* List of prefixes to try when looking for executables.  */
      static struct path_prefix exec_prefixes = { 0, 0, "exec" };
      /* List of prefixes to try when looking for startup (crt0) files.  */
      static struct path_prefix startfile_prefixes = { 0, 0, "startfile" };
      /* List of prefixes to try when looking for include files.  */
      static struct path_prefix include_prefixes = { 0, 0, "include" };
      
      他们的意义似乎是不言自明的,但是对于所谓的startup就是现在意识到的所谓的crt0我依然不是很理解。但是无论如何这个路径是在寻找到了spec之后的逻辑这个的确是寻找spec之前的逻辑,这里的一个关键点就是所谓的spec_host_machine这个值是怎么得到的,在你没有搜索任何spec之前怎么知道当前host的目标名字或者triplet呢?答案是这个是hardcoded它是这个常量DEFAULT_REAL_TARGET_MACHINE 以前我对于在代码里怎么也找不到它的赋值感到不可理解后来在编译路径下才找到是Makefile里的宏开关才理解这个是编译配置时候计算出来设定在gcc/Makefile里的宏比如
      ./gcc/Makefile:  -DDEFAULT_REAL_TARGET_MACHINE=\"$(real_target_noncanonical)\" 
      ,同样的道理是这两个常数
      
      static const char *const spec_version = DEFAULT_TARGET_VERSION;
      static const char *spec_machine = DEFAULT_TARGET_MACHINE;
      
      它们的定义也是相似的
      ./gcc/Makefile:  -DDEFAULT_TARGET_VERSION=\"$(version)\"
      ./gcc/Makefile:  -DDEFAULT_REAL_TARGET_MACHINE=\"$(real_target_noncanonical)\" 
      
      这一层虽然简单但是如果不明白那么就会对于这个搜索机制的逻辑产生怀疑的。anyway,先记录下来吧。
    • 其中的链表prefix_list*plist记录的是真正的prefix路径
      
      struct prefix_list
      {
        const char *prefix;	      /* String to prepend to the path.  */
        struct prefix_list *next;   /* Next in linked list.  */
        int require_machine_suffix; /* Don't use without machine_suffix.  */
        /* 2 means try both machine_suffix and just_machine_suffix.  */
        int priority;		      /* Sort key - priority within list.  */
        int os_multilib;	      /* 1 if OS multilib scheme should be used,
      				 0 for GCC multilib scheme.  */
      };
      
      这里有两点要注意,所谓的machine_suffix就是
      machine_suffix = concat (spec_host_machine, dir_separator_str, spec_version,
      			   accel_dir_suffix, dir_separator_str, NULL);
      比如:x86_64-pc-linux-gnu/10.2.0/ just_machine_suffix是名如其人
      
      just_machine_suffix = concat (spec_machine, dir_separator_str, NULL);
      
      比如x86_64-pc-linux-gnu/ 注意两个都是有/结尾的。 关于OS multilib schemeGCC multilib scheme我现在还不明白需要再探索。
    • 所以要阅读gcc.c:process_command这个几百行的长函数才能明白其中的逻辑路线。首先遇到的一个头疼的就是跟中各样的prefix,这里指的是相关的字符串,比如所谓的gcc_exec_prefix应该是是说gcc可执行程序的前缀,比如我们在编译器安装目录里可能看到的是这样子的
      
      $ find /usr/bin/ -maxdepth 1 -type f -executable -name "*gcc"
      /usr/bin/c89-gcc
      /usr/bin/c99-gcc
      
      而这个所谓的前缀是可以使用这个环境变量GCC_EXEC_PREFIX来提前设定的。
    • 另一个小小的头疼的就是所谓的各种各样的版本号,比如第一个就是编译器的版本
      
      /* Figure compiler version from version string.  */
      compiler_version = temp1 = xstrdup (version_string);
      
      这个一开始我还是被蒙了好一阵子,因为这个version_string声明在version.h里extern const char version_string[];定义在version.c里
      
      /* The complete version string, assembled from several pieces.
         BASEVER, DATESTAMP, DEVPHASE, and REVISION are defined by the
        Makefile.  */
      const char version_string[] = BASEVER DATESTAMP DEVPHASE REVISION;
      
      和上面的编译器DEFAULT_TARGET_MACHINE类似的都是在编译期才确定并且hardcoded的常量。
    • 那么让我们看看它们是怎么定义的吧
      
      grep  'BASEVER\|DATESTAMP\|DEVPHASE\|REVISION' Makefile
      BASEVER     := $(srcdir)/BASE-VER  # 4.x.y
      DEVPHASE    := $(srcdir)/DEV-PHASE # experimental, prerelease, ""
      DATESTAMP   := $(srcdir)/DATESTAMP # YYYYMMDD or empty
      REVISION    := $(srcdir)/REVISION  # [BRANCH revision XXXXXX]
      
      原来是定义在文件里的!
      
      nick@nick-HP-Laptop:~/Downloads/gcc-10.2.0/gcc-10.2.0/gcc$ cat BASE-VER
      10.2.0
      nick@nick-HP-Laptop:~/Downloads/gcc-10.2.0/gcc-10.2.0/gcc$ cat DEV-PHASE
      nick@nick-HP-Laptop:~/Downloads/gcc-10.2.0/gcc-10.2.0/gcc$ cat DATESTAMP
      20200723
      nick@nick-HP-Laptop:~/Downloads/gcc-10.2.0/gcc-10.2.0/gcc$ cat REVISION
      cat: REVISION: 没有那个文件或目录
      
    • 所以我们的version_string是10.2.020200723才对啊。可是当我满怀信心的在gdb打印出来的却是
      
      (gdb) p version_string
      $5 = 0x53abac <version_string> "10.2.0"
      
      这是怎么回事呢?我想查看version.o的二进制码是怎么样的
      
      objdump -Ct version.o | grep .rodata | grep version_string
      000000000000001c g     O .rodata	0000000000000007 version_string
      0000000000000023 g     O .rodata	0000000000000007 pkgversion_string
      
      那么这个symbol它的值是什么呢?
      
      readelf -x .rodata version.o 
      “.rodata”节的十六进制输出:
        0x00000000 3c687474 70733a2f 2f676363 2e676e75 <https://gcc.gnu
        0x00000010 2e6f7267 2f627567 732f3e00 31302e32 .org/bugs/>.10.2
        0x00000020 2e300028 47434329 2000              .0.(GCC) .
      
      的确是偏移000000000000001c长度7个字符31302e322e3000等于10.2.0(最后一个NULL字符是c语言literal自动添加的)。
    • 我感觉20200723似曾相识,我几乎敢肯定这个是某种magic word,果然有些关系,这个宏__GLIBCXX__是用这个时间戳定义的。
      
      ./x86_64-pc-linux-gnu/libstdc++-v3/include/x86_64-pc-linux-gnu/bits/c++config.h:#define __GLIBCXX__ 20200723
      ./x86_64-pc-linux-gnu/libstdc++-v3/src/c++11/cxx11-ios_failure-lt.s:	.string	"__GLIBCXX__ 20200723"
      ./x86_64-pc-linux-gnu/libstdc++-v3/src/c++11/cxx11-ios_failure-lt.s:	.string	"__GLIBCXX__ 20200723"
      
    • 数据是没有错,那么一定是编译的过程被掉包了。 在Makefile里version.o是这样编译的:
      ...
      -DBASEVER=$(BASEVER_s) -DDATESTAMP=$(DATESTAMP_s) \
      	-DREVISION=$(REVISION_s) \
      	-DDEVPHASE=$(DEVPHASE_s)
      	...
      
      关键是这里的DATESTAMP_s怎么定义的
      
      DATESTAMP_s := \
        "\"$(if $(DEVPHASE_c)$(filter-out 0,$(PATCHLEVEL_c)), $(DATESTAMP_c))\""
      
      这里的filter-outmake的函数就是把PATCHLEVEL_c里的0去除掉,可是不管怎么说应该返回DATESTAMP_c才对啊?而它就是简单的
      DATESTAMP_c := $(shell cat $(DATESTAMP))
      所以我无法解释。
      这里的关键就是这个if看到后来我才意识到我没有看懂它!条件是空所以不成立,这就是为什么被定义为空了!
    • 于是我只好重新编译gcc把命令记录下来查看。
      -DBASEVER="\"10.2.0\"" -DDATESTAMP="\"\"" \
      -DREVISION="\"\"" \
      -DDEVPHASE="\"\"" -DPKGVERSION="\"(GCC) \"" \
      果然是空字符串,可是为什么呢?这里有一个小技巧就是make SHELL='sh -x'我看到了什么呢? 结果还是没有看到什么最后只好祭出终极法宝直接$(info "DATESTAMP_s ==> ${DATESTAMP_s}")结果还是空,这个时候我才恍然大悟,我看代码太粗率了,这个条件是要
      $(DEVPHASE_c)$(filter-out 0,$(PATCHLEVEL_c))
      合在一起才行,就是说只有DEVPHASE_c和PATCHLEVEL_c都有货的时候加上时间戳才有意义!这就是为什么时间戳是空!
      我为了这么一个小东西version_string耗费了一个早上的时间!
    • 这里还有一个关于make_relative_prefix的小插曲,我本来以为是什么简单的宏,结果搜了半天才发现是libiberty的一个函数,挺长的我都懒得看了。问题是这个东西我根本就没有听说过是干什么的,后来看到注解才猜想这个可能是VMS才有的东西吧?恰恰相反这个是除了VMS的系统都要的东西,我的理解就是假如这个环境变量GCC_EXEC_PREFIX么有设定就设定这个环境变量,它的计算方法是依赖于这样的常量STANDARD_BINDIR_PREFIXSTANDARD_EXEC_PREFIX 而这两个又是Makefile里的预设的宏,前者是${prefix}/bin,后者是${prefix}/lib/gcc
  3. 中美为什么不能和平相处?匹夫无罪,怀璧其咎。因为中国的强大动了美国人的奶酪,这个地球的食物链的顶端只能是三亿美国人没有更多的空间容纳一个十四亿的中国人,哪怕他们中的大多数人愿意牺牲自我降级到食物链的中下端也不行。这是一个弱肉强食的丛林世界,统治这片黑森林的法则被称为三体人Trisolaran)式思维。

四月二日 等待变化等待机会

  1. gcc_exec_prefix这个变量很重要吗?我以为是很重要因为否则就无法确定gcc的,但是这个东西说简单也简单,说复杂也复杂。首先,这个是可以使用用户定义的环境变量GCC_EXEC_PREFIX来指定的。假如没有指定的话它的确定方式是采用libiberty的一个函数make_relative_prefix或者是make_relative_prefix_ignore_links,后者是前者的canonicalized的版本,也就是要resolve symlink。这两个函数都要求三个参数
    /* Given three strings PROGNAME, BIN_PREFIX, PREFIX, return a string that gets to PREFIX starting with the directory portion of PROGNAME and a relative pathname of the difference between BIN_PREFIX and PREFIX. For example, if BIN_PREFIX is /alpha/beta/gamma/gcc/delta, PREFIX is /alpha/beta/gamma/omega/, and PROGNAME is /red/green/blue/gcc, then this function will return /red/green/blue/../../omega/. If no relative prefix can be found, return NULL. */
    这里的算法我看了好久还是将信将疑,到底什么是gcc的executable prefix呢?说来好笑它要计算的是计算BIN_PREFIXPREFIX之间的common prefix,然后得到这个common prefix和BIN_PREFIX之间的相对路径,最后把这个相对路径安在PROGNAME的除却程序名之外的目录后。比如BIN_PREFIX(/alpha/beta/gamma/gcc/delta)和PREFIX(/alpha/beta/gamma/omega/)的comon prefix是/alpha/beta/gamma/,那么它相对于BIN_PREFIX的相对路径就是../../omega/而这里恰恰是理解上的关键,为什么计算相对路径看上去好像是从delta算起,而不是delta/?照例说它是一个path,不应该啊?这个是我费了不少时间才看明白还做了实验才确认的。细节在后面。接下来把PROGNAME(/red/green/blue/gcc)的目录部分求出来加上之前的相对路径。

    我做实验是使用llibiberty的静态库直接链接上来运行这两个函数,中间闹了个笑话就是我忘了声明这两个函数为extern "C"结果导致链接不上迷惑了我很久。这里的一个细节是不能引用libiberty.h的头文件,原因是其中包含了basename这个函数和glibc里的定义不一致。所以,只能链接。

    另一个方式是看源代码,其中计算相对路径用到的函数split_directories实际上很幼稚的就是数有几个/,所以,你传入的参数里的路径是否以/结尾很重要!,有和没有直接导致输出相差一个../,联想到之前我们看到gcc处理所有的路径都要在加为保证添加/.,就知道这个问题有多重要,不仅它能保证symlink也会成为合法的directory,而且路径的结尾一致可以保证相对路径的计算正确。其实是相对的路径,只要报纸一致就可以,比如大家结尾都不加/也可以。

  2. 看了半个早上就看这么一个前缀究竟有多大意义呢?我回过头来想有一点很重要就是gcc的安装路径是非常的critical的,原因是gcc有大量的变种,比如不同的toolchain的安装在统一台机器里,这个相对路径至关重要,说穿了就是从当前的可执行的gcc找到gcc的安装路径的根目录或者说gcc编译的configure里的prefix的相对路径。
  3. 我们设置完了gcc_exec_prefix我们就来到了lang_specific_driver在这里需要配置语言相关的参数。我们重点看看c/c++的相关参数。

四月三日 等待变化等待机会

  1. 花费了一天时间在探索关于路径设置的代码,因为它是我们在前端最最需要的一个变量,现在看来这是一个非常浑浊的污水潭,有些地方似乎水深不见底,实际踏足上去发现也不过如此的浅陋,可是当你刚刚想喘一口气却发现踩在一个迅速深陷的流沙加烂泥地上瞬间就把你完全的淹没还不知道是怎么回事!Welcome to the real world!

    TL;DR

    • 关于make_relative_prefix的算法的目的是什么呢?其实昨天并没有真的搞明白,所以,一上gdb就原形毕露,比如我们的prog是${INSTALL}/bin/g++,bin_dir=${INSTALL}/bin,而prefix=${INSTALL}/lib/gcc,那么究竟我们得到什么relative path呢?答案是空,看到它注解作者的意思是这个是标准配置我们不需要。我简直就是无语了。到底这个relative path是要干什么呢?
    • 一切都围绕着一个我并不知道重要性在哪里的全局变量gcc_exec_prefix在展开,一开始要检查是否用户设置了环境变量给它,如果没有就按照make_relative_prefix计算standard_bindir_prefixstandard_exec_prefix的相对值然后再去人为的设置了环境变量,可是现在看来按照正常安装路径计算就是空值!这一点对于gcc_libexec_prefix也是类似的只不过换成了standard_libexec_prefix
    • gcc这里关于这些prefix的设定逻辑非常的令人费解,我跟踪了半天才意识到读代码很困难只有gdb才能告诉你结果,比如对于把一些prefix打上标签去所谓的translate_name我就非常的难以理解因为这些加上@BINUTILS是做什么用的?我反复看到这个关键的函数add_prefix因为这个是我在这一阶段最最关心的函数因为它直接关系到搜索路径的设定,而这个函数的一个参数COMPONENT
      COMPONENT is the value to be passed to update_path.
      就是在这个update_path里面去做所谓的translate_name结果又把设定的@BINUTILS, @GCC翻译回了原先的std_prefix也就是正常的gcc-configure-prefix。这个骚操作是在干什么呢?我最后的解释只能是这个是gcc适应某些安装方式比如在windows下的情形,比如cygwin使用注册表来存储这一类的环境变量吧?总而言之gcc的前端要适应多种复杂的安装配置还有数不清的参数开关,经年累月积累下来的复杂行为实在是让人头疼啊。
    • 折腾这些鸡零狗碎的真的是烦人,我想我需要的是一个最普通的结论就是在没有设定任何环境变量,安装目录中规中矩的情况下搜索路径是这样子设定
      搜索路径组名称前缀名称前缀路径标签名称
      exec_prefixesstandard_libexec_prefix${INSTALL}/libexec/gccGCC
      exec_prefixesstandard_libexec_prefix${INSTALL}/libexec/gccBINUTILS
      exec_prefixesstandard_exec_prefix${INSTALL}/lib/gccBINUTILS
      startfile_prefixesstandard_exec_prefix${INSTALL}/lib/gccBINUTILS
      exec_prefixesconcat (tooldir_prefix, "bin", dir_separator_str, NULL)tooldir_prefix = concat (gcc_exec_prefix ? gcc_exec_prefix : standard_exec_prefix, spec_host_machine, dir_separator_str, spec_version, accel_dir_suffix, dir_separator_str, tooldir_prefix2, NULL); tooldir_prefix2 = concat (tooldir_base_prefix, spec_machine, dir_separator_str, NULL); 所以我的系统是${INSTALL}/lib/gcc/x86_64-pc-linux-gnu/10.2.0/../../../../bin/BINUTILS
      startfile_prefixesconcat (tooldir_prefix, "lib", dir_separator_str, NULL)tooldir_prefix = concat (gcc_exec_prefix ? gcc_exec_prefix : standard_exec_prefix, spec_host_machine, dir_separator_str, spec_version, accel_dir_suffix, dir_separator_str, tooldir_prefix2, NULL); tooldir_prefix2 = concat (tooldir_base_prefix, spec_machine, dir_separator_str, NULL); 所以我的系统结果是${INSTALL}/lib/gcc/x86_64-pc-linux-gnu/10.2.0/../../../../lib/BINUTILS
      startfile_prefixesconcat (gcc_exec_prefix ? gcc_exec_prefix : standard_exec_prefix, machine_suffix, standard_startfile_prefix, NULL) standard_startfile_prefix=$(unlibsubdir)在没有设定accelerator compiler的情况下我的系统等价于${INSTALL}/lib/gcc/x86_64-pc-linux-gnu/10.2.0/../../../BINUTILS
      startfile_prefixesstandard_startfile_prefix_1/lib
      startfile_prefixesstandard_startfile_prefix_2/usr/lib
      那么这两个目录下究竟存的什么呢?
    • 首先官方ubuntu-7.5的配置让我有些意外
      ../src/configure -v --with-pkgversion='Ubuntu 7.5.0-3ubuntu1~18.04' --with-bugurl=file:///usr/share/doc/gcc-7/README.Bugs --enable-languages=c,ada,c++,go,brig,d,fortran,objc,obj-c++ --prefix=/usr --with-gcc-major-version-only --program-suffix=-7 --program-prefix=x86_64-linux-gnu- --enable-shared --enable-linker-build-id --libexecdir=/usr/lib --without-included-gettext --enable-threads=posix --libdir=/usr/lib --enable-nls --enable-bootstrap --enable-clocale=gnu --enable-libstdcxx-debug --enable-libstdcxx-time=yes --with-default-libstdcxx-abi=new --enable-gnu-unique-object --disable-vtable-verify --enable-libmpx --enable-plugin --enable-default-pie --with-system-zlib --with-target-system-zlib --enable-objc-gc=auto --enable-multiarch --disable-werror --with-arch-32=i686 --with-abi=m64 --with-multilib-list=m32,m64,mx32 --enable-multilib --with-tune=generic --enable-offload-targets=nvptx-none --without-cuda-driver --enable-checking=release --build=x86_64-linux-gnu --host=x86_64-linux-gnu --target=x86_64-linux-gnu
      应当说这个是教科书式的示范!信息量极大的配置样本,目前紧密相关的就是--libexecdir=/usr/lib
    • 而要真正了解这些常人不关心的配置只有gcc的官方文档
      目录名意义默认值
      --prefixthe toplevel installation directorydefaults to/usr/local
      --exec-prefixthe toplevel installation directory for architecture-dependent files The default is prefix
      --bindirthe installation directory for the executables called by users (such as gcc and g++). The default is exec-prefix/lib
      --libexecdirthe installation directory for internal executables of GCCThe default is exec-prefix/libexec
    • 下面这些目录是从gcc的Makefile里看到的,是一种内部的使用概念,它们应该是和配置选项对应的,其中很多都是一种我认为不足为外人道也的东西,根本那不上台面的东西,但是对于理解gcc的配置和编译过程设置确实是很有帮助的东西,比如gcc的gcc_gxx_include_dir尤其有用,他解决了我长久以来看到的困惑就是c++的头文件安装在哪里的问题。又比如对于到底gcc的二进制文件放在哪个目录这样子的问题我是回答不上来的,因为这个问题的定义有问题,就是你所说的gcc的binary是指的gcc/g++还是后台真正干活的cc1[plus],collect2呢?而且我的感觉一般程序员根本就分不清到底gcc/g++和它们是什么关系,因为我至今也不是非常的清楚。因为前者的真正目录是libexecsubdir,而这一点在官方配置gcc比如Ubuntu-7.5的时候故意人为设定--libexecdir=/usr/lib 让人产生很多的误会,这也就是当我自己编译gcc的时候看到的众多的在官方ubuntu配置下看不到的现象,让人无所适从!
      目录名变量意义
      prefixCommon prefix for installation directoriesdefaults to/usr/local
      local_prefixDirectory in which to put localized header files. On the systems with gcc as the native cc, local_prefix may not be prefix which is /usr /usr/local (NOTE: local_prefix *should not* default from prefix.)
      exec_prefixDirectory in which to put host dependent programs and libraries ${prefix}
      bindirDirectory in which to put the executable for the command gcc${exec_prefix}/bin
      libdirDirectory in which to put the directories used by the compiler${exec_prefix}/lib
      libexecdirDirectory in which GCC puts its executables${exec_prefix}/libexec
      libsubdirDirectory in which the compiler finds libraries etc.$(libdir)/gcc/$(real_target_noncanonical)/$(version)$(accel_dir_suffix)
      libexecsubdirDirectory in which the compiler finds executables$(libexecdir)/gcc/$(real_target_noncanonical)/$(version)$(accel_dir_suffix)
      plugin_resourcesdirDirectory in which all plugin resources are installed$(libsubdir)/plugin
      plugin_includedirDirectory in which plugin headers are installed$(plugin_resourcesdir)/include
      plugin_bindirDirectory in which plugin specific executables are installed$(libexecsubdir)/plugin
      libsubdir_to_prefix$(prefix), expressed as a path relative to $(libsubdir).这个是一个中间变量有一个非常复杂的sed表达式,但是一板情况下就是../它的作用是用来帮助toolchain找到prefix的路径。
      unlibsubdirUsed to produce a relative $(gcc_tooldir) in gcc.oifeq ($(enable_as_accelerator),yes) unlibsubdir = ../../../../.. else unlibsubdir = ../../.. endif
      prefix_to_exec_prefix$(exec_prefix), expressed as a path relative to $(prefix).echo "${exec_prefix}" | sed -e 's|^${prefix}||' -e 's|^/||' -e '/./s|$$|/|' 我的实验结果就是这个复杂的表达式在以上设定的情况下结果就是${prefix}
      gcc_tooldirUsed in install-cross$(libsubdir)/$(libsubdir_to_prefix)$(target_noncanonical)
      build_tooldirSince gcc_tooldir does not exist at build-time, use -B$(build_tooldir)/bin/$(exec_prefix)/$(target_noncanonical)
      gcc_gxx_include_dirDirectory in which the compiler finds target-independent g++ includes.$(libsubdir)/$(libsubdir_to_prefix)include/c++/$(version)
    • 其实有很多的东西我现在还没有条件理解,也没有必要,就是关于所谓的交叉编译部分的相关的东西,所以,我想跳过关于tooldir_prefix的部分。
    • 上面经常出现的所谓的accel_dir_suffix居然是什么A prefix to be used when this is an accelerator compiler,很明显的大多数人的机器里都是空,这是什么古老的东东呢?我实在是懒得google了。
    • 那么我花了这么多时间究竟明白了什么呢?我的目的是什么呢?不就是要明白所有的搜索路径是怎么加入的吗?不如这样子总结 在常规操作之后在exec_prefixes被加入了好几次结果就是
      ${prefix}/libexec/gcc/:${prefix}/libexec/gcc/:${prefix}/lib/gcc/
      最后一个toolchain的目录很多时候是不存在的这就是我每每都看到的gcc报出的那个很长又不存在的搜索目录的原因:
      ${prefix}/lib/gcc/x86_64-pc-linux-gnu/10.2.0/../../../../x86_64-pc-linux-gnu/bin/
      其实这个如果参照上面的计算公式就是${prefix}/$(real_target_noncanonical)/bin,当然这个观察也能明白吧?
    • 对于startfile_prefixes相对简单,添加的就是${prefix}/lib/gcc以及哪个又臭又长的toolchain的目录
      ${prefix}/lib/gcc/x86_64-pc-linux-gnu/10.2.0/../../../../x86_64-pc-linux-gnu/lib/
      它等价于${prefix}/$(real_target_noncanonical)/lib/ 这个startfile_prefixes的重要性除了runtime library之外应该就是寻找specs的作用,当然我从来不知道怎么使用自然没有,但是用户使用自己的specs是一个高超的技巧。
  2. 也许关于初始化路径的战斗告一段落,我应该先把初始化specs的代码看完再说吧?

四月四日 等待变化等待机会

  1. 看spec的相关代码一头雾水,决定还是先看文档吧?首先它的标题就解释的很透彻Specifying Subprocesses and the Switches to Pass to Them,真的是一句顶一万句啊!但是后面的文档我像看天书一样摸不着头脑,其中的语法非常的费解,我和代码里定义的spec根本对不上,只好放弃转而使用gdb看看究竟这个最后转化为呼叫参数究竟是怎么样的。
  2. 代码里又是一些让人无法理解的各种骚处理,比如在所谓i的添加add_sysrooted_prefix的各种条件里为什么standard_startfile_prefix是相对路径就不添加了呢?注释里说这个是native compilation。而且随着代码展开我的这个表也在不断加长。
  3. 
    ${INSTALL}/bin/g++ -v -std=c++17 -I${INSTALL}/lib/gcc/x86_64-pc-linux-gnu/10.2.0/plugin/include -O0 -g3 -Wall -c -fmessage-length=0 -MMD -MP -MF"src/parserTest.d" -MT"src/parserTest.o" -o "src/parserTest.o" "../src/parserTest.cpp"
    
    
    ${INSTALL}/libexec/gcc/x86_64-pc-linux-gnu/10.2.0/cc1plus -quiet -v -I ${INSTALL}/lib/gcc/x86_64-pc-linux-gnu/10.2.0/plugin/include -imultiarch x86_64-linux-gnu -MMD src/parserTest.d -MF src/parserTest.d -MP -MT src/parserTest.o -dD -D_GNU_SOURCE ../src/parserTest.cpp -quiet -dumpbase parserTest.cpp -mtune=generic -march=x86-64 -auxbase-strip src/parserTest.o -g3 -O0 -Wall -std=c++17 -version -fmessage-length=0 -o /tmp/ccGN42w0.s
    
    g++cc1plus的参数变化是添加 -quiet -imultiarch x86_64-linux-gnu -dD -D_GNU_SOURCE -quiet -dumpbase -mtune=generic -march=x86-64 -auxbase-strip 我感觉有些沮丧就是说费了很大劲儿处理spec其实得到的东西并不多,大部分就是把g++的参数照样传递给cc1plus,仅仅添加了少数几个似乎不太变化的开关,这个也许可以通过解析spec的函数轻易得到,所以,最后还是又回到了toplev.main。汇编器的参数就很简单了。
    as -v -I ${INSTALL}/lib/gcc/x86_64-pc-linux-gnu/10.2.0/plugin/include --64 -o src/parserTest.o /tmp/ccQXFUvq.s
  4. 独上高楼,望尽天涯路。其实,并非完全没有收获,因为搜索路径的初始化是不是在fork的时候传递给了子程序呢?真是荒唐怎么可能。头脑混乱了其实就是增加了以两个关键的宏开关就够了,这个情况和我在libcpp的实践中是相似的。

四月五日 等待变化等待机会

  1. 每次gdb都可能揭示一些新的我所不知道的部分,比如我刚刚才意识到前端的一个复杂性在于命令行的所谓的gcc_options是一个在编译配置后自动产生的巨大的结构数组,这个对于熟悉libc里的long option可能感觉很亲切,但是对于gdb尤其我根本看不懂这个handle函数是怎么设定的,难道处理函数也是运行期再设定?总之前端其实相当的复杂,很多看上去不起眼的几行代码却牵涉了很多的全局变量的设置让人感觉扑朔迷离。gcc被人诟病绝对不是少数人,但是你有能怎么样呢?gcc的开发者可以自豪的宣称它已经是事实上的行业标准又有谁敢挑出来质疑,一些聪明人暗地里摇摇头转而制定了自己的标准改变了语言称之为所谓的GO语言lang。而大多数聪明人对此嗤之以鼻转而开辟全新的语言。其他的替代编译器事实上都在追赶gcc。
  2. 感觉越来越像是标准的hack的定义,因为你违背了gcc整个流程而断章取义,你错过了option的处理而是人为的设置,这个翻译过程本身就是问题重重,多亏设置gdb:watch才发现原来翻译-std=c++17之类的是设置(至少是最基本的一部分吧?)cxx_dialect = cxx17;可是c++语言相关的设置太复杂了,不但需要把tree设置以及相关的hashtable等等还有各种各样的所谓的decl/stmt/eh等等,我头昏脑胀的只在这个汪洋大海里抓到了一片小树叶就是这个全局变量main_input_filename,我需要设置,之前我是直接把它当作一个可有可无的东西,可是这个全局变量被无数次的直接引用,而且更糟糕的是它是在动态产生的options.h里定义为parsing comandline option里的一个全局变量。这个让人格外的头疼,它透露出的信息是我被又一次深深的拽入了更深的泥潭:你想绕过gcc前端的无数设置越来越困难了。gcc到底被人最多诟病的地方是在前端吗?也许吧?照例说前端是最没有技术含量的部分,95%的工作是目标码的产生和优化,剩下5%里至少要分配大部分给parsinghe preprocessing吧?居然还有这么多的百分比被人诟病?多么不可能啊?这个应该是我个人的无能造成的吧?可是如果前端是多个后端程序的指挥者在运行之初就要为它们做好规划的话那么这个工作量应该是不小吧?即便前端不用去具体负责实施后端工作,但是它必须把各种配置的可行性做了验证更何况多种语言多种输出多种后端的组合?单单看这个option的数组多达接近2000个选项这能不是一个惊人的复杂体吗?一个命令行如果理论上有两千个可能的参数选择这个程序的复杂度有多大呢?那么这个不到5%的所谓工作量的通常的论断是否应该有所调整呢?难道说。。。我感觉这个是一条无尽的漆黑的隧道。。。
  3. 我感觉我看不到这条隧道的尽头。还是出去晒晒太阳吧,也许太阳的光辉能洒一些光亮给那条隧道吧?
  4. 一走到灿烂的阳光里我就意识到我的战役决心和战役目标的偏差,从来我就没有奢望这么轻易的前进到parser这一步。我的战役目标原本就是通过读取spec获得cpp的参数的自动设置从来进一步理解提高之前的libcpp的可用性。在明确了这个目标的同时一切都变得容易多了。
  5. 在bin下面的gcc/g++/cpp都是前端,而它们都是要呼叫libexec/gcc/${target}/${version}/下的cc1[plus]/collect2。所以,我找到了一条路径是这样子的
    
    #0  scan_translation_unit (pfile=0x3650ab0) at ../../gcc-10.2.0/gcc/c-family/c-ppoutput.c:175
    #1  0x0000000000c2d280 in preprocess_file (pfile=0x3650ab0)
        at ../../gcc-10.2.0/gcc/c-family/c-ppoutput.c:102
    #2  0x0000000000c28be0 in c_common_init () at ../../gcc-10.2.0/gcc/c-family/c-opts.c:1167
    #3  0x0000000000a4fe1f in cxx_init () at ../../gcc-10.2.0/gcc/cp/lex.c:329
    #4  0x000000000139f946 in lang_dependent_init (
        name=0x7fffffffe1b5 "/home/nick/eclipse-2021/parserTest/src/parserTest.cpp")
        at ../../gcc-10.2.0/gcc/toplev.c:1974
    #5  0x00000000013a018d in do_compile () at ../../gcc-10.2.0/gcc/toplev.c:2263
    #6  0x00000000013a0500 in toplev::main (this=0x7fffffffdb76, argc=21, argv=0x7fffffffdc78)
        at ../../gcc-10.2.0/gcc/toplev.c:2417
    #7  0x00000000022e4bbb in main (argc=21, argv=0x7fffffffdc78) at ../../gcc-10.2.0/gcc/main.c:39
    
  6. 但是这个前提是需要传递这两个附加的参数-imultiarch x86_64-linux-gnu-march=x86-64,它们的来源只能是spec吧?所以,现在任务明确了,就是一个是builtin spec的根据目标输出的相关解析,再一个就是以上路径的函数调用。

四月六日 等待变化等待机会

  1. 基本上我理了一下思路就是说我之前是自下而上的创建一个最简单的libcpp的应用,但是我缺乏一个可靠的机制。结果我现在是按照gcc前端检查各种option设定的方式来探究如何可靠的设定搜索路径和几十上百的那些cpp_define的宏开关。依靠gdb/watch的强大功能我一步一步把各个全局变量设定,目前几乎冲到了最后一公里,但也是强弩之末了。明天再说吧。

四月七日 等待变化等待机会

  1. 折腾了一个早上就在我几乎要放弃的时候终于似乎把cpp的参数都设置正确了,可是我却对此不满意了,因为这个debug的过程固然辛苦,但是最重要的是我对于这个程序的意义开始怀疑就是说我需要太多的手动设置全局变量,那么我当初期待使用gcc前端来翻译众多的参数开关的目的在哪里呢?比如这些全局变量究竟是对应于那些option呢?我固然可以在将近2000个选项里查找可是这个意义就很小了,因为我不如直接就在driver这一层直接传递参数不就行了吗?
    
    cxx_dialect = cxx17;
    imultiarch="x86_64-linux-gnu";
    flag_abi_version=14; //latest_abi_version=14;
    flag_preprocess_only=1;
    user_label_prefix="";
    ix86_arch_string="x86_64";
    ix86_tune_string="generic";
    
    除此之外还不包含我需要把相当的几个内部函数出来化繁为简的简单实现,这种做法是英文hacker的原型,但是却是最拙劣的一种。
  2. 在debug过程中我曾经一度怀疑是我传递的cpp_define不完整,我希望把它们都打印出来发现没有很直接的工具遍历内部数据结构因为obstack是一个黑箱操作。后来发现回调函数里有一个define的很可以一用。所以就很简单的了
    
    void define_cb(cpp_reader *cpp, location_t loc, cpp_hashnode *hash){
    	cout<<cpp_macro_definition(cpp, hash)<<endl;
    }
    cpp_callbacks * callback= cpp_get_callbacks(parse_in);
    callback->define=define_cb;
    
  3. 我现在唯一能看到的方向就是大刀阔斧的砍掉大部分的无用的初始化的部分,因为我的降低目标的cpp目前唯一需要的就是路径设置和宏开关设置,因为大部分的初始化实际上是为了parser作准备的。所以,我可以先实现一个之前的preprocessor的好的版本再回过头来看parser的可能做法。

四月八日 等待变化等待机会

  1. 昨天做了一点的清理工作准备再思考一下下一步的方向。摆在面前明显的两个步骤是一步一步缩减非必要的初始化部分代码,但是另一个小的更好的开始是彻底清理spec相关的函数,理清builtin-spec的部分,我感觉这个是更加必要的小任务。

四月九日 等待变化等待机会

  1. 昨天我决定从spec开始,可是很快就发现这个非常的吃力,完全不像是应该给外界暴露的对象,也就是说相关函数都是内部的,我也茫然不知如何入手。现在看到一个入口是输入文件名后缀选择编译器的部分。这个结构叫做compiler,它的功能当然容易明白就是根据后缀名推理使用哪一个spec,但是这中间更加复杂的是进一步的相关的扩展就看不懂了。
  2. 另一个入手的地方是在以上从后缀名得到的是一大堆的字符串而不是某个spec文件名,那么这里的逻辑类似于复杂的regex的开关吧?这个函数validate_all_switches处理了这么几个部分,一个是后缀名得到的一个字符串,一个是用户可能输入的指定spec文件内容,还有一个是固定的关于linker的spec,注意这里linker的spec对应的文件后缀名为空,能不能理解就是linking的时候不需要输入文件名的因为很多时候编译指令就是要产生目标可执行程序所以linking是必须的。简单来说大部分时候linking是一个隐含过程是流水线的一部分,承接前面的中间过程到最终输出,而且gcc只是有着众多前端语言支持但是毫无例外的大家都是要产生可执行程序,在这一点来看不管什么语言产生的都是obj来作为linking,所以无需后缀名。,不过这个link_command_spec是有预设值的,什么时候override的部分代码我看不太清楚。
  3. 关于spec我的理解是这样子的,它实际上就是关于众多的命令行开关之间的某种逻辑关系的界定,因为传统的options只能记录什么开关导致什么行动,但是对于开关之间的逻辑关系无法进行描述,所以这个就是spec的产生原因。换言之就是用一种自定义的类似于regex的表达式来简约的表达开关之间的逻辑因果关系。比如某些开关不能共存,某些开关是另一些的充分条件或者必要条件等等,而这里spec可以是开关本身的字符串也可以代表字符串的变量甚至于存储字符串的文件名。可以说非常的灵活复杂。
  4. 所以,让人烦心的事情就是一部分开关的逻辑是在spec里就定义下来了,而在随后的命令行解析的时候这两个方面的逻辑是交织在一起的。也许我们不应该关注于用户说了什么,而是应该关注于用户要做什么。这个看似一句政治性很强的断句,不是吗?民主体制里的民意和民粹在中文里是一字之差,但是这里的含义却很深奥,人民最终需要的其实对于多种前端语言来说都是一样的就是一个可以执行的可执行程序,尽管各个语言如ada,fortune,c/c++.d,objectiveC/C++等等看似表达的诉求各不相同风马牛不相及,需要的开关中间过程五花八门,但是到了翻译成目标语言的时候却有着惊人的相似,而且最后的object文件的链接毫无二致,最后的翻译结果却都是一样的需要可以执行的程序,这一切对于人文社会的统治者有什么启示呢?邓公的白猫黑猫论是高度的浓缩,治国理政其实最后的本质大都是一样的,只不过对于前端用户来说不足为外人道也。你去和一个只能听得懂c语言的用户来说fortune的描述方式肯定是对牛弹琴,同样的各个语言的操纵者彼此是完全不可能互相理解的,但是作为编译器却要集合如此海量的不同诉求来解析它们的各自的独特要求,最后去粗取精融会贯通在复杂的排查纠错之后生成用户所期待的可以执行的政策来具体实施。然而,作为用户根本无力也无兴趣自己的复杂要求怎么样被编译器理解并产生自己期待的可执行程序达到用户的千奇百怪的目的,它们这里我使用非生物的既代表了客观中立的看待用户,更是隐含了对于无知如动物的用户的鄙视和可怜都是结果导向的经验生物,只会运用几百万年进化的唯一武器,神经系统的条件反射来判断自己的行为的唯一目的是否达成:也就是生物的唯一存在目的所导致的趋利避害的行为。面对如此不可预测的用户作为编译器的最好的生存逻辑是适应用户的多种繁杂的语言以及连带造成的复杂各异的开关逻辑并竭尽所能取悦用户真实的目的,还是说从更高层级来越俎代庖主动为用户来设计一条它们看得懂听得懂的道路。这是一个人类终极的哲学问题,甚至是一个社会学的伪命题,因为从社会实践来看人类社会还处于发展的婴幼儿时期,还不明白自己的发展方向也不明白究竟自己真的需要什么,仿佛婴儿唯一的思维就是来源于饥饿的条件反射的哇哇啼哭。而作为执政者如何翻译貌似无理的各种用户选民的诉求是否应该经过严格的逻辑甄选而不是一味的盲从?而这些相关的诉求之间的某种内生的逻辑关系应该是早已刻画在spec里的至理名言。
  5. 无约束的财政并不是打垮现政权的重锤,但是它是让社会成员丧失辛勤劳动才能创造财富的信念的破坏者,就好比毒品短时间内并不一定就对肌体造成物理性的伤害,甚至于靠吸食毒品某些个体在肌体上还能短暂的获得了超越同类的能力,但是毒品最大的危害部分来源于神经系统的摧残,它不但让吸食者产生短暂的超越正常意识形态对于客观世界的真实反应而且让其产生精神上的依赖以至于今后在现实中任何的必要的挫折失败都不得不借助于吸食毒品来获得那种超越同类的幻觉。这就是今天美国国会不加约束的滥发国债的本质。
  6. 总之,我觉得这个路线有问题,因为这些函数基本上都是不对外的,如果要使用就只能拷贝出来。这个让人担心什么时候这些spec的语法就改变了也不知道。其次,翻译这些开关本身就落在gcc的将近两千个option的泥潭里让人不能自拔。

四月十日 等待变化等待机会

  1. debugging的一个很大的好处是逼迫你去理解看不懂的代码。我想我也许才真正接触到gcc最让人感到恐惧的代码的边缘,那就是关于那个tree的部分,单单它的巨大的union的类型就把我吓坏了,而使用大量的宏定义访问让我在gdb里也看不懂到底变量名是什么!
  2. 陷入泥潭,因为无论是spec还是缩减都似乎很困难。但是当我碰壁回头希望通过plugin的回调事件来探寻编译过程时候才再一次明白我的尝试的意义,那就是哪怕最简单的preprocessing的过程即便是libcpp的回调函数都是苍白无力的有限。比如plugin的PLUGIN_INCLUDE_FILE事件是不包含系统的builtin的头文件部分的,即便我再深入使用cpp_callbacks注册parse_in的回调函数include得到的也是如此。所以,我们面对的gcc是一个不会开口说话的闷葫芦
  3. 这个是我的一个plugin例子实际上我查了plugin的这个event就是PLUGIN_INCLUDE_FILE和我所直接调用cpp_callbacks是没有什么差别的。
  4. 对于GTY虽然已经有了一个大图景的概念,但是遇到细节的实作就又迷茫了。这里再读一遍。 也就是说如果不明白GTY的概念看到这个语法就一头雾水了,我相信大多数有经验的程序员对于这个语法也是茫然的
    
    union GTY ((ptr_alias (union lang_tree_node), desc ("tree_node_structure (&%h)"), variable_size)) tree_node{
      struct tree_base GTY ((tag ("TS_BASE"))) base;
      struct tree_typed GTY ((tag ("TS_TYPED"))) typed;
      struct tree_common GTY ((tag ("TS_COMMON"))) common;
      struct tree_int_cst GTY ((tag ("TS_INT_CST"))) int_cst;
      ...
     };
    
    我想我当时对于这个声明是看了好几分钟也不明所以然。我的做法就是把GTY部分完全做无视,至于它里面的那些细节我更加不想去了解了。
  5. 我对于gimple之类的tree的结构细节一直抵抗,因为实在是太复杂了,而一旦落入这个泥潭就会万劫不复了,但是能够一直回避吗?三不政策能够持续多久呢?直到我看到这个定义才有些恍然大悟的样子。
    
    union tree_node;
    typedef union tree_node *tree;
    

四月十一日 等待变化等待机会

  1. 一直在读到GENERIC,但是始终不知道是什么,现在不得不了解。它就是一个通用型的函数
    The purpose of GENERIC is simply to provide a language-independent way of representing an entire function in trees.

四月十二日 等待变化等待机会

  1. 从youtube下载了《三体》的有声小说,直接使用youtube-dl --extract-audio --audio-format mp3,但是文件名里有中文和空格,花了一个小时也没有处理好rename的问题,最后下载了mmv一分钟就搞定了。
  2. 再一次的尝试使用plugin,可是再一次证实这些event是很不可靠的,比如我同时注册PLUGIN_START_PARSE_FUNCTIONPLUGIN_FINISH_PARSE_FUNCTION结果只看到后者,大概看看代码我觉得问题是函数的parsing的开始分为declaration,可是对于没有declaration就漏掉了吧?总之,gcc的开发者已经几乎完全放弃了这个东西,因为也许有更重要的部分需要这些plugin,而对于比较低级的parse这类工作维护者不愿意再费心思了吧?而且另一个问题是plugin非常的难以debug,极其困难,因为我尝试gdb,感觉首先从前端gcc/g++的角度来看就是在子进程里运行的cc1[plus],而这个子进程里加载的动态库segfault的时候应该是用signal捕捉打印出一个类似bt的输出告诉你,那么我在前端的gdb里是无法设断点的。总之,很困难。plugin作为了解各种各样的tree_node是一个办法。
  3. 我无意中遇到了所谓的gcc-wikii我对于这个wiki的看法是只不过是一些新人希望改变gcc文档长期落后更新的现状,更加专注于html格式而不是传统的manpage/html/pdf一体化的固定格式。我个人以为它的权威性是不够的。第一步是发现有不少的有趣的plugin,但是2011年一定发生了什么重大的事件导致大量的程序在那一年后停止了。那一年发生了什么大事?
  4. 很多年以前有人开始改造gcc也许那个时候发生了很多事情吧?这个也许是一个很好看的tree的继承吧?不过我很怀疑他的计划被接受了。
  5. 不过今天有一个小小的收获就是我自己的gcc-7.5没有安装gcc-7-plugin-dev,结果导致编译一些plugin的时候还要指明我使用的自己编译的gcc的路径,这个是很tricky的问题,因为plugin是一个既灵活又死板的东西,有些时候它使用的是某个gcc版本的特定的内部的东西,你到底要编译运行是同一个版本的gcc吗?哪怕是libiberty.h这个似乎不怎么经常改变的库也很难说吧?所以。。。我其实也是很混乱,因为我下载的那个plugin居然是gdb的plugin但是它实际上是在gdb运行中去挂载上默认的gcc上来运行,类似于那个我曾经感到困惑的所谓的在gdb里编译源代码实施运行的超级铉酷的功能。大多数时候没有人会考虑说你在gdb的时候这个可执行程序是用哪一个版本的gcc编译的这种问题吧?难道说两个版本的gcc编译的可执行程序不一样吗?这个不是笑话吗?也许不是?
  6. 现在回过头来看wiki关于编译gcc的基本点就感觉舒服多了。
  7. 关于编译器我的水平就是幼儿园阶段,对于寄存器目标代码部分完全是在黑暗中。什么是LRA,我最高的期望就是明白它的缩写就好了。
  8. 很多时候我根本不明白到底一个东西是在gcc还是在libstdc++,也许我应该看看后者,两个家伙手牵手,但是毕竟是不同的。
  9. 《三体--黑暗森林》里创立了所谓的宇宙社会学这种三体人式的的思维模式同样适用于中美关系,事实上我非常怀疑作者当初设想的对象就是如此:

四月十三日 等待变化等待机会

  1. 又一次为build module而烦恼。
  2. 关于艺术我始终认为这个表述是经典的透彻深邃。

  3. 关于libstdc++的编译问题,看样子是必须依赖于gcc的代码结构,因为编译过程中需要好几个脚本文件,结论就是单独拷贝libstdc++v3是无法编译的,在gcc目录结构中编译是毫无问题的,只不过需要选择一个版本足够高的c++编译器,比如我默认使用gcc-7.5是无法编译10.2版本下的libstdc++的很多新的move的语法相关的会出错等等。当然指定编译器需要的是CXX=而不是CC=因为主要的区别在c++而不是c语言的不支持。其中的错误似乎是无害的吧?

四月十四日 等待变化等待机会

  1. 这个似乎可以写进《XX宣言》一样的不言自明了吧?每种目标机器天然的拥有各自独特的特性,否则我们也无需专门定义这些特性,因为它们就自动的可以称为某种兼容的机器了吧?在这些众多的特性之一就是所谓的64-bit_data_models,我记得我以前就看过这个东西,但是要真正理解其中的含义其实未必那么简单。首先,这个在gcc里是怎么体现的呢?用脚趾头想也知道是实现配置的,也就是按照configure脚本里配置的。所以,这个东西是在所谓的target-machine相关的部分吧?所以,当你看到一个头文件的名字叫做tm.h就不应该意外,而且它一定是在编译gcc的目录下才有,不是吗?这一点都想不到就不要再看下去了。其实大多数人能够接触的就是LLP64LP64两种,而它们的区别也就是关于long int的不同。
    64-bit data models
    Data model short (integer) int long (integer) long long pointers,
    size_t
    Sample operating systems
    ILP32 16 32 32 64 32 x32 and arm64ilp32 ABIs on Linux systems.
    LLP64 16 32 32 64 64 Microsoft Windows (x86-64 and IA-64) using Visual C++; and MinGW
    LP64 16 32 64 64 64 Most Unix and Unix-like systems, e.g., Solaris, Linux, BSD, macOS. Windows when using Cygwin; z/OS
    ILP64 16 64 64 64 64 HAL Computer Systems port of Solaris to the SPARC64
    SILP64 64 64 64 64 64 Classic UNICOS[45][46] (versus UNICOS/mp, etc.)
    这里的这个表之所以重要是因为这一段代码没有它你就看不懂! 对照上面的表里的long int=64和指针size=64,和int=32,所以它就是LP64!
  2. 但是我对于表里的LLP64感到一些怀疑,不错微软的确是如上面所说的
    • int: mostly 32 bits long (Linux, Mac and Windows)
    • long: 64 bits on Mac and Linux, 32 on Windows
    • long long: 64-bit on Mac, Linux, and Windows x64
    • (u)intptr_t: exact length of pointer (32 on 32-bit, 64 on 64-bit systems)
    但是当我搜索LLP64目标时候发现似乎只有ARM64的aarch64才是这个目标,那么gcc在所谓的cgywin下是怎么样的呢?也许我这里混淆了什么,不过我从来不用windows下的东西了,所以,管它呢?
  3. 但是以上的代码是不完整的,你怎么知道所谓的precision的呢?我找到了一直困扰我的部分 不过我还是太天真了以为有人会好心的为你定义这个所谓的SIZETYPE,实际上它是要去推理的。所谓的precision是一个很复杂的过程的结果。首先,有一个相关的常量是在编译的时候定义的#define NUM_INT_N_ENTS 1。它是好几个相关数组的长度,其中我关心的是int_n_data它的类型是相当让我吃惊的复杂东西 说它复杂在于scalar_int_mode_pod是一个让人感到很吃惊的类,因为它背后的所谓的machine mode是一个我一无所知的领域。 而编译过程的定义让我也是摸不着头脑 我在gdb里看到
    (gdb) p SIZETYPE
    $2 = "long unsigned int"
    
    真是感到由衷的高兴,因为否则的话要从机器模式推理出来就太难了。
    且慢!在gdb开始的时候默认的不是这样子的
    
    (gdb) p SIZETYPE
    $4 = "unsigned int"
    
    它是在gcc初始化的所谓的global_options_init时候才变的,而它是在gcc编译的时候定义的一个极其庞大的所谓的options 这里我被一个诡异的事情纠缠,就是这个结构里没有一丝一毫提到TARGET_LP64或者SIZE_TYPE可是这个结构指针赋值之后为什么这两个所谓的变量就变了呢?在我看来它们是用宏定义的literal而已,不是吗?在defaults.h
    
    #ifndef SIZE_TYPE
    #define SIZE_TYPE "long unsigned int"
    #endif
    
    x86-64.h里定义为
    
    #undef SIZE_TYPE
    #define SIZE_TYPE (TARGET_LP64 ? "long unsigned int" : "unsigned int")
    
    可是它们是宏啊!不是变量,怎么改变它们的值的呢?我肯定是头脑发昏了,该吃饭了。
  4. tm.h里定义了很重要的两件事,一个是关于使用什么c library的问题,这个是我以前一直没有注意到的,gcc支持四种glibc, uclibc, bionic,musl,这个bionic我还真的没有听说过。另一个就是定义一大堆的config下相关的目标机器相关的头文件。这些头文件对于一个各个平台共用的defaults.h下默认定义的宏进行了重定义,这个就是目标机器的由来的一部分。这里当然包含了大量的细节,我能叫的上名字就不错了,更不要提细节了,那些应该都是各个平台专家才能明白的细节了,比如说指令集的定义等等吧?
  5. 几乎每一个程序员第一个接触的数据类型就是int或者其他什么size之类的,其实没有其他,就是integer但是我对于stdint.h究竟有多少概念呢?我几乎从来不用,只有某些时候编译很奇怪的程序才被迫包含。所以当我看到需要定义stddef.h里的各种宏就感到很陌生了。不过我查看了gcc编译和源码之间的stddef.h是毫无改变的,改变的是stdint.h几乎在各个不同的配置参数下都不同!仔细一看其实没有什么大不同,除却gcc编译需要用的isl的大家用的都是一个版本。

四月十五日 等待变化等待机会

  1. 经过一晚上的狐疑疑惑困惑怀疑妄想,早上终于明白了一个程序员的基本技能:那是debug_macro啊!以前我是如此的无知以为gdb就是使用elf的symbol而已,那么对于程序员的噩梦宏要怎么办呢?难道你去strtab里搜索吗?那个就是一个地址,你是要程序员怎么告诉你呢?看来我的思想还是停留在程序实践的萌芽时代,这种每天都需要的需求我没有意识到只能说明我平常的实践是如此的肤浅。所以,当我反反复复看到elf里大量的debug_macro的section居然无动于衷可见我的浅陋。那么回到gcc-main.c的时候,那里的头文件一定是定义了一堆的宏,对于tm.h里包含的x86-64.h里自然就是定义了我们所需要的宏,所以,在gdb里很轻易的就明白为什么gdb能够打印一个宏的值,因为它储存了所有的宏的信息啊!
    
    (gdb) info macro SIZE_TYPE
    Defined at /home/nick/Downloads/gcc-10.2.0/debug-libcpp-build/gcc/../../gcc-10.2.0/gcc/config/i386/x86-64.h:40
      included at /home/nick/Downloads/gcc-10.2.0/debug-libcpp-build/gcc/./tm.h:33
      included at /home/nick/Downloads/gcc-10.2.0/debug-libcpp-build/gcc/../../gcc-10.2.0/gcc/opts.c:26
    #define SIZE_TYPE (TARGET_LP64 ? "long unsigned int" : "unsigned int")
    
    所以,余下的故事就很容易明白了,为什么你赋值一个结构opts.c:288 *opts = global_options_init;居然让一个宏的值发生改变,的确是内存的变化,只不过这个宏的推理的逻辑线有些长,因为核心的变化是另一个宏TARGET_LP64的变化,而它的定义是牵涉到了global_options_init的一个成员变量
    
    (gdb) info macro TARGET_LP64
    Defined at /home/nick/Downloads/gcc-10.2.0/debug-libcpp-build/gcc/../../gcc-10.2.0/gcc/config/i386/i386.h:203
      included at /home/nick/Downloads/gcc-10.2.0/debug-libcpp-build/gcc/./tm.h:26
      included at /home/nick/Downloads/gcc-10.2.0/debug-libcpp-build/gcc/../../gcc-10.2.0/gcc/opts.c:26
    #define TARGET_LP64 TARGET_ABI_64
    (gdb) info macro TARGET_ABI_64
    Defined at /home/nick/Downloads/gcc-10.2.0/debug-libcpp-build/gcc/./options.h:8268
      included at /home/nick/Downloads/gcc-10.2.0/debug-libcpp-build/gcc/./tm.h:22
      included at /home/nick/Downloads/gcc-10.2.0/debug-libcpp-build/gcc/../../gcc-10.2.0/gcc/opts.c:26
    #define TARGET_ABI_64 ((ix86_isa_flags & OPTION_MASK_ABI_64) != 0)
    (gdb) p ix86_isa_flags
    $2 = 0
    (gdb) info macro OPTION_MASK_ABI_64
    Defined at /home/nick/Downloads/gcc-10.2.0/debug-libcpp-build/gcc/./options.h:8141
      included at /home/nick/Downloads/gcc-10.2.0/debug-libcpp-build/gcc/./tm.h:22
      included at /home/nick/Downloads/gcc-10.2.0/debug-libcpp-build/gcc/../../gcc-10.2.0/gcc/opts.c:26
    #define OPTION_MASK_ABI_64 (HOST_WIDE_INT_1U << 4)
    (gdb) info macro HOST_WIDE_INT_1U
    Defined at /home/nick/Downloads/gcc-10.2.0/debug-libcpp-build/gcc/../../gcc-10.2.0/gcc/hwint.h:70
      included at /home/nick/Downloads/gcc-10.2.0/debug-libcpp-build/gcc/../../gcc-10.2.0/gcc/system.h:1204
      included at /home/nick/Downloads/gcc-10.2.0/debug-libcpp-build/gcc/../../gcc-10.2.0/gcc/opts.c:22
    #define HOST_WIDE_INT_1U HOST_WIDE_INT_UC (1)
    (gdb) p OPTION_MASK_ABI_64
    $3 = 16
    (gdb) info macro ix86_isa_flags
    Defined at /home/nick/Downloads/gcc-10.2.0/debug-libcpp-build/gcc/./options.h:53
      included at /home/nick/Downloads/gcc-10.2.0/debug-libcpp-build/gcc/./tm.h:22
      included at /home/nick/Downloads/gcc-10.2.0/debug-libcpp-build/gcc/../../gcc-10.2.0/gcc/opts.c:26
    #define ix86_isa_flags global_options.x_ix86_isa_flags
    (gdb) p global_options_init.x_ix86_isa_flags
    $5 = 18
    (gdb) p opts->x_ix86_isa_flags
    $6 = 0
    (gdb) 
    
    我只能感叹前人做了那么多的工具开发出了这样没有你做不到只有你想不到的gdb,而我竟然不知道怎么用!
  2. 再一次的说明一个真理:你不能怀疑前人打下的坚实的基础,在你无法理解前辈的历程之前最好重新体会一下前人经历的磨难,不经风雨,哪有彩虹?

  3. 为了弄明白一个简单的LP64模式我折腾了差不多整整一天半!现在我才意识到那么复杂的算法要得到的所谓的各种数值的precision/bitsize等等的原因是因为在交叉编译下这些无法从当前的编译器获得,换言之如果不考虑交叉编译我完全可以很容易的获得。
  4. 这个是一个简单的debug的小技巧,我对于的定义感到迷惑,
    
    struct default_include
    {
      const char *const fname;	/* The name of the directory.  */
      const char *const component;	/* The component containing the directory
    				   (see update_path in prefix.c) */
      const char cplusplus;		/* Only look here if we're compiling C++.  */
      const char cxx_aware;		/* Includes in this directory don't need to
    				   be wrapped in extern "C" when compiling
    				   C++.  */
      const char add_sysroot;	/* FNAME should be prefixed by
    				   cpp_SYSROOT.  */
      const char multilib;		/* FNAME should have appended
    				   - the multilib path specified with -imultilib
    				     when set to 1,
    				   - the multiarch path specified with
    				     -imultiarch, when set to 2.  */
    };
    
    因为从一个个宏的定义来看auto-host.h里定义的觉得有些不可思议。于是我就去在可执行程序寻找这个变量,这个使用objdump -Ct寻找是很容易的。得到它在.rodata里的地址,
    
    objdump -Ct parserTest| grep cpp_include_defaults
    000000000084615c l     F .text	0000000000000015              _GLOBAL__sub_I_cpp_include_defaults
    00000000021fb680 g     O .rodata	0000000000000120              cpp_include_defaults
    
    我们可以使用readelf -x .rodata来dump查看,它是一个机构的数组,第一第二个成员变量是两个字符串指针,我一开始被迷惑了一下,因为它的地址是little endian的,所以要倒过来看。
      0x021fb680 d8b21f02 00000000 5ab31f02 00000000 ........Z.......
      0x021fb690 01010000 00000000 60b31f02 00000000 ........`.......
    
    它的地址d8b21f02当然就是这里0x021fb2d8
    
      0x021fb2d0 ffffff7f 02000000 2f686f6d 652f6e69 ......../home/ni
      0x021fb2e0 636b2f44 6f776e6c 6f616473 2f676363 ck/Downloads/gcc
      0x021fb2f0 2d31302e 322e302f 6763632d 31302e32 -10.2.0/gcc-10.2
      0x021fb300 2e302d64 65627567 2d6c6962 6370702d .0-debug-libcpp-
      0x021fb310 696e7374 616c6c2f 6c69622f 6763632f install/lib/gcc/
      0x021fb320 7838365f 36342d70 632d6c69 6e75782d x86_64-pc-linux-
      0x021fb330 676e752f 31302e32 2e302f2e 2e2f2e2e gnu/10.2.0/../..
      0x021fb340 2f2e2e2f 2e2e2f69 6e636c75 64652f63 /../../include/c
      0x021fb350 2b2b2f31 302e322e 3000472b 2b000000 ++/10.2.0.G++...
    
    同样的第二个指针的地址5ab31f02的地址实际上是0x021fb35a。当然还有一点就是64位操作系统的指针长度是8个bytes,所以当然的后面的四个00000000不用考虑了。另外一个就是这里也是一个练习结构对齐的好机会,因为两个指针后面跟了四个char的成员变量,根据对齐默认原则,后面要再跟4个bytes所以,总共12个bytes。

四月十六日 等待变化等待机会

  1. 昨天我依靠在gdb里的info macro来判断很多宏的定义实际上是不准确的,完全解释不通,原因在于理解它的机制,我虽然没有看文档说明,但是从它的显示可以猜出来它是基于preprocessor的内容的,总之是基于源代码的,这个和我搜索源代码是一致的。换言之,如果编译指示的命令行参数是不包含的,比如-D,我得出这个结论是因为实际运行结果和从gdb的宏定义是不相符合的。结果我只能从编译好的.o文件里的.debug_macro去寻找,这个是readelf -dump=macro来查看,并且我在gcc的Makefile里得到了证实。 以下就是证据: 总而言之,当你在gdb时候遇到不可解释的情况时候,还是在.o文件里找答案吧。
  2. 有一点关于pch就是要怎么保证一定使用呢?这个东西昨天摘抄了一个开头就被带偏了。

四月十七日 等待变化等待机会

  1. 似乎有必要再一次的重新审视gcc和LLVM之间的异同。现在在有了一些gcc的体会后回过头来看LLVM也许有不同的感受吧?
  2. 我觉得认真读一下这个关于Template Metaprogramming的wiki是很有帮助的。
  3. 《黑暗森林》的结尾是给人们一种虚假的乐观,那个牺牲自己以及自己种族文明生存机会而冒死警告叶文洁不要回答的三体监听员居然在脱水状态还能和罗辑畅谈宇宙中也许广泛存在所谓爱的萌芽,幻想着有一天阳光能够照进黑暗森林,并且自信的相信今天的太阳虽然落下,但是明天太阳照样能够升起。这是哪里来的自信与乐观?弱小与无知不是毁灭的原因,傲慢才是。
  4. 很多年以前我对于一个传教的小团体有些好奇,这个是可以理解的,因为从一个无神论国度来到一个基督教传统的国度总是对于文化宗教感到好奇。他们非常的真诚善良邀请我去他们家里看了一部录像片,其中谈到一个生命产生的难题,它和费米悖论一样的让人困惑,现在在《三体》里都做了一种解答,生命看似对于自然环境的要求如此苛求让现代人类认为生命产生的条件在概率上如此的小以至于出现了智慧设计的理论,而大刘说生命反过来对于它所处的环境也有反作用以至于我们错误的把目前的生存环境当作了原因而不是互作用的结果。联想到今天还在肆虐的病毒让我们看到了生命顽强的一面,对于病毒这种半生命半非生命的物质我们也许没有意识到在上古的营养汤里大量的有机质无机质的相互作用的二十亿年里不仅孕育了最原始的生命也不断的尝试生命可能生存的环境参数,我们仅仅是无数试错的一个结果。当然作为智慧设计信徒看来这个正是不可能的地方。对于如此巨大的时间尺度下我们人类是没有真实的观感的。

四月二十日 等待变化等待机会

  1. 这个纯粹是一点点的体会而已。就是我使用我下载的这本免费书flex-bison结果发现结果总是不对,我一开始还以为是我自己做错了什么,后来发现bison的标准的范例设定的语法就不是这样子的,也许作者使用的古老的版本的做法吧?其实就是一个很小的问题,就是在line的时候才应该是输出结果而不是在作者的退出才打印。此外,以前遇到gcc代码里使用这个#line number filename这样子的语法理解不够深刻,现在看到bison产生这样子的代码就明白这个是为了给debugger方便查询源代码的一个机制,也就是说作者希望你在gdb里看到的文件的行数。这个对于gcc里很多由宏产生的代码是很好的机制来帮助你理解当前代码的意思。另一个小小的学习心得,就是要先使用bison产生所谓的xx.tab.h/xxtab.c代码文件,这样子的话flex文件里才能引用这个xx.tab.h里定义的bison指定的token。其实这个小小的细节就是我对于flex/bison的全部想知道的地方。
  2. 吾尝终日而思也,不如须臾之所学。曾经看到c++语法书里说操作符的优先级没有在语法中显式的体现,当时心中有些惶恐不知什么意思,其实现在看到这个例子就知道在BNF里体现优先级并不是什么很高深的东西,看这个例子就明白乘除的高优先级是怎么体现的。
    
    exp: factor
    	| exp ADD factor   { $$ = $1 + $3; }
    	| exp SUB factor   { $$ = $1 - $3; }
    ;
    factor: term
    	| factor MUL term { $$ = $1 * $3; }
    	| factor DIV term { $$ = $1 / $3; }
    ;
    term: NUMBER
    	| ABS term        { $$ = $2 >= 0? $2 : - $2; }
    ;
    

四月二十一日 等待变化等待机会

  1. Why can't C++ be parsed with a LR(1) parser?
    Using google, I only found that C can be perfectly parsed with LR(1) but C++ requires LR(∞).
  2. 十几年前人们就在抱怨我所抱怨的,我现在才明确的意识到CDT是不允许使用symtab之类的,这个使得c++parsing非常的困难。这里是所谓的wave实现的一个cpp,而它提到使用boost的库来实现是一个看起来更好的实现路径。我实在是孤陋寡闻,因为wave和spirit一样是boost的成员了。
  3. 我对于自己的无知没有一丝丝的羞耻感,因为,《三体》中的技术爆炸理论就是建立在人类的科学知识技术积累而让后人在有限的生命周期内少走弯路从而更上层楼的基础上的,然而,站在巨人肩膀上不是一个轻而易举的动作,没有任何一个民族或者个人天生就能翻身骑上马背更不要说爬上巨人的肩膀了,很多时候在前人经验指导下攀爬是不可替代的学习过程,这个正如不学习历史就无从知道为什么我们今天能够站在当下,这个过程是一个直接经验与间接经验的结合,没有直接经验而完全来自于间接经验很多时候是沦为典型的理论空想家,不是说这样的理论不好只是他们往往自己缺乏实践机会只能指望别人来实施自己的想法,而一旦自己的想法没有得到实践的验证很可能导致整个理论大厦的轰塌,和空中楼阁相似。但是如果纯粹都来自于直接经验而不借鉴前人的结果那确实是重新造轮子,并不是不好,因为至少证明了具有和前人相当的能力,但是也许辜负了前人对后人的期望,没有更上一层楼。所以,这个中间要把握一个度。然而,英文里研究(research)通常被人们解读为再次搜索(re-search)假如没有对前人的搜索结果不做基本的评定也许也会错失发现前人遗漏的机会,因此,适度的重新体验前人的辛劳才能体会今日的难得,所谓不忘初心方得始终就是这个意思。因此,我一点儿也不觉得我做错了什么。相反,我觉得至少在cpp这个领域使用cpplib是一个可靠的选择,因为它是gcc的一部分,而且gcc预先定义的那些宏开关和预处理器的实现是无关的,你即便使用wave实现了一个cpp,依然需要设定那些builtin的宏开关。当然这和cpplib无关,只不过它在gcc内部使用其原生的机制更可靠一些,尽管我之前的实践似乎表明这个路径极其的复杂和不切实际。
  4. 我的另一点体会是c++语法有歧义的说法是有一个细节的,就是单单语法定义的BNF本身不能称作是有歧义吧?它的歧义存在于如何解释,就是说sematic层面有多种解释吧?就好像说按照语法写出的句子也不一定是没有歧义,也就是说简单的验证语法是没有问题的吧?这个对于CDT的编写者也许是有意义的。当然我对于这一点自己也不是有什么理由。
  5. 阅读wave的起源,这句话和我心有灵犀。
    Only today we begin to understand, that preprocessor generative metaprogramming combined with template metaprogramming in C++ is by far one of the most powerful compile-time reflection/metaprogramming facilities that any language has ever supported.
    这段话的真正的含义我肯定不可能在可视的将来能够真正体会的出。
  6. 那么让我先体验一下boost preprocessor再说吧。因为我编译第一个wave的例子就不知道哪里有什么问题。可能是eclipse的设置有问题吧?总之,我发现这个preprocessor并不是一个我想像的东西它是纯粹的一个macro的工具,我需要的是wave,而它有一个tool/cpp是一个符合ISO的cpp,编译它需要额外这些库-lboost_wave -lboost_filesystem -lboost_program_options -lboost_datetime -lboost_system -lboost_thread -lpthread

四月二十二日 等待变化等待机会

  1. boost里的各种各样的看似奇怪的风格常常能颠覆我的看法(options_description_easy_init),原来程序也可以这样写,真好似武侠小说里一个初出茅庐的武学新手看到武林宗师把一些匪夷所思的拳经剑法演绎的淋漓尽致让人叹为观止:原来程序也可以这样子写啊!
  2. 我平常很少愿意使用istream做一些精细的字符处理,因为这个黑箱子里到底干了什么我并不是很清楚,于是只能对着原始的char*来操作,比如在读取字符并识别的过程里,它会自动跳过空白字符,这一点对于很多代码文件的处理也许并不适合,所以,当我看到unsetf(std::ios::skipws)时候才明白其实它可以有很多的控制方法,因为使用istreasm的一个最大好处之一是利用它内部的>>操作符直接解析类型。这里的如此多的flag让人感到陌生。其中大部分都是和数字相关的,比如这些个internal就是只针对数字的,而且对于货币我一直没有机会使用这个put_money真的是好神奇的一个使用locale的典型例子。也就是locale里的各种各样的奇葩的形式,这里我意识到了一个常常的痛苦,我因为把ubuntu的locale设定成为中文,结果编程里输出经常被这个所困扰,看到这个imbue才意识到可以很轻松的改变。
  3. 这个unsetf(std::ios::skipws)之所以重要是因为我以前读取整个文件的时候,它会跳过空白字符串,而这一点我一直都没有注意到。 所以,添加这个in.unsetf(std::ios::skipws); 不过我随后发现我始终对于两个iterator混淆不清,如果我们使用istreambuf_iterator即便没有设立这个所谓的flag也是没有问题的,因为我的理解就是它直接暴露了stream的rowbuf,自然就不存在跳过空白字符的问题了。 而且,我注意到istreambuf_iterator接受stream或者它暴露的rdbuf(),效果似乎是一样的,我没有看源代码,我想内部是一回事吧?
  4. 看boost的代码比较的困难就说明对于c++的掌握程度的高低,因为像我每一行都有语法和库的用法的疑惑,那么这个速度是可想而知了。总之,我几乎是一步一步都在查手册,这个简简单单的wave的cpp的例子让人打开了视野。
  5. 每次我都能从boost的大侠代码里学到新东西,比如我长久以来不知道要怎么复制或者说克隆一个cout,这个方法就给你指明了cout的核心是什么?是它目前的fmt/rdstate/rdbuf。那么flag呢?
  6. 对于llvm我的感觉是复杂的,一方面我觉得它做的是正确的方向,但是另一方面我又不是很喜欢它走的道路。总的来说我的笔记本sudo lshw -class memory已经有16Gib内存了但是编译llvm依旧在链接的时候出错,真的不知道大家是怎么编译llvm的,而且编译完了占用磁盘就是几十个G,这个让人实在是难以令人接受。这个虚拟机的做法虽然有其优势但是对于普通人它是无法和gcc相提并论的。尽管gcc也是很庞大,可是对于编译linux内核它是无可替代的。总之,我的感觉llvm就是无法让人掌握自己命运的意思。

  7. 无言。

四月二十三日 等待变化等待机会

  1. 对于boost bjam的如此简单的东西居然没有任何人给你解释什么是target!就不能明说就是debug/release吗?难道你不能把读者想像成只有九岁?最后下载了这本手册才明白,真的是浪费时间,同时提醒一下我的系统安装的bjam很旧不要用,要使用boost自带的编译好的。
  2. 毕竟还是有所进展的,boost/wave的确是一个很好的起点,和我预期的相似就是同样的各种各样的include-search-path以及predefined-macro这些问题是要程序员自己处理的,因为这个世界上并不是只有gcc这个唯一的编译器,每种编译器和它自带的c++库是紧密相联的。
  3. wave里的c++11设定里把宏__cplusplus硬编码为201103L,而且不许修改,也就是使用undefine会报错,这个在一般的情况下是没有问题的,因为我以为c++11和c++20在c preprocessor的层面来说是没有什么大的变化吧?也许module新增的import算是preprocessor的指令?,总之当我处理我的gcc-plugin的时候遇到了很多错误因为有很多的条件编译是基于这个宏__cplusplus的值的。
  4. 美利坚的焦虑(Strategic Competition Act Of 2021)

    We are going to have to act
    If we want to live in a different world
    
    There is a very unstable situation on the ground
    That is unfolding very quickly
    
    Move toward more ideas that would help, uh
    Bring this thing to an end
    
    We are going to have to act
    We are going to have to act
    
    There is a very unstable situation on the ground
    That is unfolding very quickly
    

四月二十四日 等待变化等待机会

  1. 因为我自己的输入目录的一个愚蠢的错误没有去除空白字符导致debug了将近一天,实在是太可笑了。当然这个也是一个熟悉代码的过程。其中最令人印象深刻的还是boost里代码的玄幻超出了一般认为c++就是面向对象编程的类c程序员,因为大部分的难点都不在这类地方,实际上现在流行的编程流派似乎认为遥远的微软那种在运行期都不知道对象的类型的编程场景的现象其实是不多的,程序员依靠继承来实现运行期多态的做法有一定的应用场景,但是更多的在boost里体现的是另类的风格。比如用模板来实现的开放式的编程。比如在遥远的年代openGL设定一个黑箱子的context通常使用一个void*指针,但是如何扩展和操纵是完全的不透明的API函数才能操纵,因为这个context实在是太不透明了,只能通过方法来暴露功能。而方法有时候是僵硬的,一旦分发者使用二进制码就是固化的。在boost这样子尽量使用头文件的开放工程要怎么做呢?使用模板参数,而模板参数几乎是万能的回调函数,而配合所谓的hook这种回调函数来注册达到类似的回调函数的功能。这听上去像是废话,这里面的编程者的行话可以说这些hook是functor也可以说是policy,总之能够按照设计者指定的行为方式来让继任者个性化,你也可以说这就是继承,但是对于不透明的context的void*指针是否是一种进步呢?我以为是增加了类型的检查,虽然回调函数有一定的函数调用参数约束,但是毕竟通过某种高级技巧也许可以有更大的灵活性。它的本质在我看来就是那种静态继承
    The curiously recurring template pattern (CRTP) is an idiom in C++ in which a class X derives from a class template instantiation using X itself as a template argument.[1] More generally it is known as F-bound polymorphism, and it is a form of F-bounded quantification.
    继承者必须遵循继承来的模板参数类所定义调用方法,最大的好处是省却了据说昂贵的运行期虚方法表的获取。而假如hook或者说policy使用boost::any也许可以允许任意类型的模板参数,当然它毕竟是回调函数最起码的参数传递还是有的,没有可能真的任意,除非它不需要什么额外的参数?

四月二十五日 等待变化等待机会

  1. 早晨起来听到一个经济学家关于bitcoin的评价很中肯,虽然用技术手段限制了发行上限,做到了去中心化,公共账本的透明化,然而货币的属性不是设计者能够左右的,因为限制了总发行量反而无法防止整个币值的通货膨胀化,账本的去中心化包含的匿名化并不能防止它成为贩毒走私和各种犯罪的交易媒介,更不要说最近它成为华尔街和众多草根的博弈场。货币也和武器一样的是一把双刃剑,它是否作恶不在它本身而在于掌握它的人手里。
  2. 而关于美元的滥发,我现在的看法是这是一个不可阻止的过程,而且可能会持续很多年,最后的终止因素是什么?我以为仿佛让物体运动停止的力只能来自于自身遭受的阻力。同样的美元滥发最终被阻止的力也是来自于自身的阻力,这个阻力是什么?一般认为是来自于通货膨胀的压力。这个一般性的认识并没有错,最终也许是,但是很多人迷惑于为什么没有这么快到来,诸多因素不一而足。但是有一点是金融的一个本质在货币身上的体现就是一个信用与预期,用一个简单的例子就能诠释。在电影《百万英镑》里一个一名不闻的美国穷光旦居然可以凭借一张不能兑现的在伦敦上流社会里呼风唤雨,不仅白吃白喝,还赢得众多名媛贵妇的青睐,并且单单靠不用投入一分钱的背书就赚取了常人难以想像的投资收入。一个月后那两个开玩笑打赌的银行家回来收回那张神奇的百万英镑的钞票的时候,这个穷小子已经赚取了一大笔钱。这中间起作用的是那张不能兑现的百万英镑的钞票的信用和预期,无数人被之折服的背后的力量是两个超级富翁银行家的信用在这张纸上的背书,由此引发的是穷小子身边人们的预期,短短一个月时间,相当于银行家给了穷小子一个一百万英镑的贷款额度,尽管他并没有使用这个额度不,他的确使用了,因为吃饭住旅馆买衣服都是赊帐就是一个提前消费就是使用这个贷款额度,只不过就像他的口头承诺投资股票一样,饭店老板,旅馆老板,商店老板依靠他的知名度就转回了他的赊帐自然免除了他的消费,导致他似乎并没有花一分钱,实际上他的股票口头投资是有风险,中间他的钞票丢失就是导致所有的债主来挤兑就是证明。,但是他能够利用额度本身的这就是货币的基本属性信用在使然。货币归根结底的本质是信用,而影响信用的因素很多,有人说是预期收入也就是还款能力及其还款历史作为信用记录积分。这无疑是正确的,但是还有很多其他的因素使然,比如在众多的国际货币里它们作为交易的手段固然是基础属性,而作为投资回报的标的物则越来越成为主要因素,国际上巨大的和国际贸易交易需求不成比例的货币在流动中,一方面促成了国际资本投资国际贸易结算,但是更多的成为了投资对象本身,当一国货币能够带来更多的投资回报的时候,国际资本对于这个国家货币的滥发是有容忍度的。那么什么时候这个容忍度会达到极限呢?其中一个因素一定是这个国家中央银行超发货币的效率。美国经济今天之所以叫做债务经济的根本原因是因为它的全部增长都已经来自于借债,大多数人对此没有深刻认识的原因不仅仅是因为被美国经济在很多领域里还表现了增长所迷惑,更主要的是没有意识到美国国际整体经济GDP每年全部增长甚至比美国从联邦到各个地方以及个人所新增债务还低,比如以美国整体经济规模大体上20万亿美元为例,它的经济增长基本上就是2%左右,折合4000亿美元,还不及美国联邦政府一年的财政赤字,更不要说地方政府以及企业和个人新增的债务,这些债务的本质可以想像就是美国全体人民不吃不喝应该靠新增加的借贷也应该增长至少2%以上很多,换言之,美国全体人民以及公司企业政府部门闲坐了一年也能实现这个2%的增长,那么忙忙碌碌难道是白忙了吗?也就是说美国的经济在持续萎缩!不靠借债经济就会大幅萎缩,这个也是所有即将破产的企业和个人的典型特征,收不抵支日常开支靠借债维持,至于说能够维持多久完全取决于那个借债的额度的多少。回过头来说美元滥发的极限必定就是无法继续维持所谓的表面上增长,当今无数的企业与个人都在玩的日复一日的游戏,只要你能营造出一种增长的表象,那么你就可以从投资人那里获得新的贷款,或者从你那张信用卡里支取新的借债。所以,美元的终极发生在美国依靠债务驱动增长模式的终极,这个是可以想像的,因为随着债务的累加新增债务驱动增长的阻力是越来越大,这个阻力不仅来自于债务本身的利息,更来自于随着增长后规模更大的美国经济的再增长的困难,这不仅要求持续加大的债务投入,更加可怕的是来自于新兴大国类似的债务驱动的效率的竞争。同样的债务驱动,如果效率更高就意味着它会把本来国际资本给美国的额度吸引过去,这个也说明了中美竞争不可避免的一个本质问题,崛起的中国不仅仅是它的人民要消耗掉美国人民在地球食物链顶端的资源,更加要挤兑美国债务增长驱动经济增长的借债额度,而这一点没有比一山不容二虎更加简单明了了。总而言之美元滥发的阻力来自于它自身维持债务驱动增长模式的阻力,当债务的利息抵消了大部分的增长,当债务投入增长回报不成比例,显而易见的是有一天人们意识到与其政府把那么多的债务投入创办企业发展经济还不如直接发给人民消费来的轻松的时候,世界和资本是会无情的抛弃美利坚的。更主要的是当国际资本发现有另一种货币能够带来更加稳定高速的回报的时候,美元滥发的机制就被自动刹车系统锁死了。这个另外的竞争货币无疑是人民币,原因不仅仅是货币背后代表的经济增长预期,还有更加重要的是透过数字人民币体现的严格的财政纪律所带来的信用。也就是人们常说的信用比黄金还宝贵。并不是加密货币本身的技术来实现所谓的信用,因为我们已经看到比特币并不能自律,自律要依靠发行者的强力干预才有可能达成。从这一点来看我觉得今天的bitcoin已经证明了创立者的初衷是失败的,那就是用技术手段来强制实现原本需要人的意志来达到的自律性,因为尽管加密货币在技术上本身解决了发行总量不会滥发的问题,但是它忽略了一个货币所代表的人类社会基本的事实:那就是人类通过不断的劳动是在不断创造新的价值。那么发行总额忽视这个基本事实把人类社会总的财富僵化的认为一成不变就必然导致比特币的不断升值,而这个做法实际上是让比特币的早期持有者收割后来者的财富的一种类似于金字塔式的传销模式,由此带来的和中国炒房团一样的简单低级的盈利模式。它的结果就是这种击鼓传花的财富增值模式必然导致投机者不断推高比特币币值直到最后因太高造成的恐慌性的抛售盈利的彻底崩溃。
  3. 这里比特币的通货膨胀和魔兽世界里的虚拟金币的贬值有异曲同工之效,因为前者是因为总发行量限定导致过多的实体货币投入流通导致货币升值,而后者是众多中国玩家生产劳动创造出更多的金币导致总量增加的通货膨胀。这两者一正一反恰恰说明了一个货币的本质属性就是不断增长的属性,换言之适当的通货膨胀是货币本身的属性因为根本原因是使用货币的人类的不断的劳动创造增加的价值。所以,从这个角度来看单纯的期待美元滥发会很快停止是不现实的,因为它单单靠自然的通货膨胀就能够维持一定程度的增发很多年,这个和今天的英国英镑很像,一个垂死的昔日大国依靠它过去的积累的信用就能维持一个货币这么多年的增发而不垮台。可见维持一个信用不容易,但是摧毁一个成熟的信用也不容易。所以,货币滥发最根本的阻力机制还是来自于货币背后的劳动者的劳动创造能力的降低,假如美国人民有一天全体都采取养猪模式依靠纯粹的消费/借债驱动增长那么它的阻力总有一天要超过增长带来的驱动力,因为你固然可以依靠提高各种消费服务价格来提高增长回报,但是来自于其他国家的同质服务价格竞争会自然拉低这个做法,更重要的一点是非服务性商品的价格往往不那么依赖于人力价格,比如固定商品价格取决于原材料价格,生产设备,劳动生产率等等,而国际比较价格更容易通过进口来阻止本国通过拉高人力成本造成的虚高商品价格,这一点在美国制造业外移表现的尤为明显。这个正像今年拜登政府数困法案试图提高最低工资水平被否决一样,民主党试图通过提高服务价格水平来维持增长的小伎俩在共和党看来无异于更快的减少债务驱动增长的上限空间使得这个货币滥发终极摩擦力提前增大到平衡的那一刻的到来。
  4. 《三体--黑暗森林》里三体人对于罗辑的追杀是基于一个事实,那就是关于宇宙社会学的公理定理演绎出的制衡宇宙文明的终极机制是除了主人公罗辑之外并不为其他人所领悟的高深道理。当然三体派遣的间谍智子也许理论上可以监听地球上所有人的任何言行,其他不知名的领悟者也会被秘密处死,但是三体人在水滴探测器到达地球之前必须要借助于三体地球组织来实施,而三体人又不想因为扑灭这个想法而让他为世人所意识到,所以,肉体消灭罗辑必须伪装成自然死亡。相反的,美元霸权的终极毁灭是一个世人皆知的公开秘密,谈不上为此杀人灭口,美国政府唯一能够做的是怎么阻止那个新兴大国来分割它那张永远不能兑现的空头支票式的百万英镑的举债额度。这就是中美大国竞争的本质原因。

四月二十六日 等待变化等待机会

  1. 很久以前我遇到c++module里语法把import/export从preprocessor directive修改为keyword的疑惑,现在我的理解是这是为了减少cpp的工作,这样子就可以照样使用传统的cpp,因为它不是预处理器的工作,我的理解是module绝对不是简单的include头文件或者声明一个namespace那么简单组合就能实现的,它是一个非要在编译链接阶段才能实现的全新的功能,但是它的实现是依赖于对c++的语义理解而不是在预处理器能够做到的,所以,索性根本就不应该让预处理器了解。但是这个部分实在是太复杂了,我语法看了一头雾水,简直就是天书。
  2. 抽丝剥茧式的学习是一个基本的过程。module包含着这些截然不同的部分,而cppreference在这一点上还没有人更新。
  3. 把boost/wave/quickstart的例子改造后的demo存储在github

四月二十七日 等待变化等待机会

  1. 需要理解的东西很多,我还无法完全读完这个关于module的wiki部分,对于引入module的论文,似乎是有必要阅读的。

四月二十八日 等待变化等待机会

  1. 可以说我对于moudle的理解依然是几乎是完全错误的,它是远比我想像的复杂的多,否则也不会这样子千呼万唤始出来的等待了好多年。首先,编译器的支持是还未发行的gcc-11,其次,它的使用是要首先编译所谓的CMI(Compiled Module Interface),这个要怎么做我依然似懂非懂,遇到这种情况我认为就是不懂。因为我现在的理解是它是和现行体制平行生存的,而且要达到提高编译效率,它被当作了cache一样的,而且二进制码是符合elf32,着一些问题,就很头疼了,本身把libstdc++编译成module就是一个技术活,我还不知道官方有没有这样做,因为现在也没有正式发行,即便有这个做法也不一定公布,需要测试。另一个我还难以理解的就是所谓的module-mapping的问题,我对于这个几乎完全摸不着头脑,首先不理解目的,其次不理解怎么做,难道要自己编写规则吗?看到原来的proposal里引用了clang的一个ppt,其中的一些做法似乎也有不同,也许标准之下怎么实现是各家有各家的高招,对于这类mapping或者CMI的格式之类都是实现者自定义的吧?毕竟对于使用者是很简单的一个import解决所有的头文件引用烦恼,可是怎么做到呢?此外,我针对昨天对于phase1-7的问题,看样子也许的的确确这个是在效果上是preprocessing的一个结果,当然并不是指的实现是真的这么做。因为对于sdk的开发者来说,现有模式就是引用头文件链接库文件,引用头文件其实达到的效果就是一样的,只不过要把第二步的链接动态库也包含进来那就是一个很大的工作了。总之,我觉得现在module的环境还不成熟,现在尝试是要花很多时间的。
  2. 直到现在我才想起来为什么我突然开始看module的直接原因,其实是一个非常简单的问题的答案而已。就是module是否需要预处理器的干预,我一开始以为不需要,现在看起来是要的,因为它们是所谓的preprocessor directive,尤其是import和include有相似之处,那么换句话说现有的预处理器是无法支持c++20的。为了这么一个结论我花了一两天时间,难道我不应该直接google这个问题吗? 我为了再次证实我的结论又把10.2和11.xx的libcpp的lex.c代码做了简单的比较,的确是要在这里增加module的部分才能实现。这个似乎是多此一举的,因为status上也是这么说的。

四月二十九日 等待变化等待机会

  1. 昨天唯一的收获是了解了wave的引擎使用的是re2c这个工具基于regext产生的c代码,这个解决了我一个头疼的地方,之前我认为regex很强大但是对于复杂逻辑就非常的困难而且debug更加的困难,对于高级功能各家的引擎规范不一致,往往要依赖于某个引擎的背后的特别功能,尤其是那些backtrack让我头大。现在看来re2c似乎是容易了很多,因为它可以在c语言的注释里插入类似于flex产生的代码,所以,你可以在c语言里插入regex,而不是像flex那样在regex里插入c语言,这两个的差别就天差地别了!
  2. 当然我还有一个最大的疑惑在于module的实现机制上,在我看来绝对预处理仅仅是第一步,是无法单独完成的,但是这个似乎不重要,因为从预处理器的角度来看只要它没有做错什么并且不在其后的过程中再次需要它那么这个流水线的任务就算完成了,这岂不是很好?
  3. 另一个好的方向是boost/spirit它是接受EBNF的,这个远比古老的bison/flex组合来的好的多吧?
  4. 使用gch/pch还有一个细节,就是我制作pch/gch的编译开关要和主程序的开关一致,这个说起来容易还是有小小的细节,比如我直接把bits/stdc++.h下的头文件编译为gch,然后不再允许我的主程序包含它而直接使用开关-include stdc++.h并指明gch的路径在search path里,结果在-Winvalid-pch下报出了一些莫名其妙的错误,我一开始不知所以然,直到我直接从#include "stdc++.h"正常编译才发现居然这个是因为产生PIE需要编译-fPIC的缘故,这个真的是意外,我从来没有意识到标准库居然要求强制编译PIC,我的猜想是PIE(Position Independent Executable)是安全上的强制要求。所以,重新编译gch/pch才可以使用-H开关看到的确是使用了gch/pch,而这个好处在于源代码里根本不包含任何include,这个会让看代码的人感到困惑,这个是不是很好的做法呢?注意编译的搜索路径现在不用再使用原本stdc++.h的冗长的目录!
    
    ${CXX_INSTALL}/bin/g++ -std=c++2a -fmessage-length=0 -g3 -Wall -fPIC -Winvalid-pch -x c++-header ${CXX_INSTALL}/include/c++/10.2.0/x86_64-pc-linux-gnu/bits/stdc++.h -o stdc++.h.gch
    ${CXX_INSTALL}/bin/g++ -H -std=c++2a -fmessage-length=0 -g3 -Wall -fPIC -Winvalid-pch -x c++ -include stdc++.h -I. src/gchTest.cpp  -o Debug/gchTest
    
    这样子主程序是这样子的简单到让人困惑,因为没有任何的#include的头文件。
  5. 关于Qi和Karma,简直是有些禅宗的意味,对我来说是一种耻辱因为他们问世都十多年了。之前我一直以为去年阻止我进一步前进的是他们兄弟,刚刚才发现是fusion和mpl,这些东西都是太过于玄幻了。

四月三十日 等待变化等待机会

  1. 编译pch的复杂程度是超过了我的想像,原本我直接把stdc++.h和boost/spirit/include/qi.hpp合编成一个pch是没有问题的,但是当例子需要多个这样子的头文件时候,合编后的pch在使用时候总是有各种错误。我怀疑把各个不同的头文件比如spirit/support.hpp/karma.hpp等等放在一起也许是有什么问题吧?总之,也许是顺序也许是宏,这个让人分外的期待module来解决这些烦恼。
  2. 要看懂spirit的代码是有些难的,首先就是遇到很多的复杂的宏,比如一个简单的terminal的uint_定义我就看得头晕目眩,它是一大堆的基本类型使用boost的循环的宏来定义一大堆的,我本人对于宏的滥用是比较抵触的,原因是显而易见的,最后我只能先预编译再来找定义,它被定义成了 这里的tag::unit_是一个巨大的宏定义出来的,具体是什么呢? typedef integral_c_tag tag; 对于这样复杂的宏eclipse帮不上很多忙,因为那些宏只能自己依靠eclipse的有限帮助脑补,所以,预编译成为了唯一的办法,搜索integral_c_tag我才明白原来是mpl里的东东,难怪我看到这些似曾相识。我当时不敢相信是从mpl借用过来的,因为没有明显的引用fusion/mpl,仅仅用到了phoenix,可是我应该能够想到,看来我去年卡壳的部分还是要去继续攻破,因为boost里的最难的之一就是他们兄弟,从型参到实参,再从实参到型参,这种混搭产生的能力是令人难以想像的。我以为spirit应该理解到大量的操作符重载就不会感到不可思议了,倒是它使用到的phoenix是我应该先突破的,而它用到的fusion我也逃不掉。
  3. 天和号上天我当时并没有当作一回事,现在回放实况录像看到军方所有的高层都出席发射现场观摩才意识到这个是中国空天军在不声不响的推进。相比美国俄罗斯的大鸣大放中国是更加的实打实做事。想想看空间实验室将来不仅是科研而是太空基地就明白了。
  4. 在遭遇了编译pch/gch的困境后我的反思是这不一定是一个好的办法,因为我有一种错误的意识认为头文件是不包含很多代码的,或者说是便宜的中间过程代码,但是现在回头想一下boost里的代码都在头文件里,而头文件的Include不是库的链接可以选择需要的添加,而是全包含,就好像一个代码文件一样无法剥离,所以,把所有的头文件编译成一个巨大的预编译头文件是不明智的,没有人愿意这么干。想明白了这一点我也就释怀了。这本是一个任何程序员都能想到的事情,我为什么现在才意识到,这个又不是三体里宇宙社会学的公理定理推论,应该是人人都能意识到的。
  5. 我现在不知道应该从哪一个入手,是fusion还是phoenix,看到这个网站(https://theboostcpplibraries.com/)似乎是boost的帮助学习的网站,看看吧。
  6. 我以前对于阿三(libasan)很头疼,一方面它是内存检查的利器,对于内存越界泄漏的检测是神器,但是由于我使用最新版的编译器动态库要重新部署很烦恼,直到发现它是可以静态链接-static-libasan的才解决。回想起来这个库是很特殊的,不是在用户层面使用-l能够指示的,这个应该就是我当初始终无法静态链接的原因,毕竟这个是非常的神奇的部分。
  7. 这个https://theboostcpplibraries.com确实还不错,作为一个学习入门的网站和原来的boost文档有别样的优点,不是它能够深入而是能够提纲契领给你展示大图景。所以,我看了几眼phoenix觉得它和c++标准的lambda之类的也大同小异,相信不必再看了,原因是当初学习boost::function的遭遇,很多时候发现我费尽力气用boost::function做的事情还不如直接使用标准库的lambda来的简单,反而污染了我的视听。所以,我觉得我直接再次尝试fusion就好了。现在先去雨地里走走再说吧。

五月一日 等待变化等待机会

  1. 工欲善其事,必先利其器。我对于我现在使用pch来编译boost的众多头文件的做法感到满意,一个简单的eclipse的makefile工程指定简单的makefile的dependency导致每次再precompiled.h里添加的boost头文件引起pch的编译成为precompiled.h.gch,而我的实验代码里只包含这个头文件,而不是使用-include的做法,原因是我想让eclipse明白怎么去建立它内部的indexing,这个是使用eclipse的几乎最大的优点。而对于namespace污染防止我严格限制using namespace xxx;在每个测试函数内部,当然每个测试例子用到的带模板的函数结构也都定义在单独的namespace。另一个避免当初eclipse崩溃的措施是把boost每一个库建立一个makefile project,因为它处理不了太多的头文件。总之,现在这个运行的还算良好。从头来看fusion的文档,感觉容易多了,比如对于这个fusion里map的理解就轻松多了,严格说来associative tuple是更加贴切的名字。之前我看过李伟的那本书对我是非常的有增益的,可惜没有看完,但是开头部分已经是走过了很艰难的第一步。
  2. 总之我重新阅读fusion的感觉就好像脱胎换骨一样的新境界,仿佛当初一个爬行在二维平面的小蚂蚁试图理解三维空间的复杂的几何结构一样无助,而当前却好像已经跳跃到四维时空后开了天眼一样的俯视低维结构体一样。而当初看似复杂的三维结构在降维打击成为二维的后一幅保留有三维世界无限细节的二维图画。因为我已经看透了不论是什么变换无方的结构表面背后都是tuple这不变的一种编译期的幻象,所有的结构序列都不存在什么存储的固化,而一切的颠倒错乱不过是遮人耳目的障眼法,想透了这一层就仿佛当年杨过面对绝情谷主的阴阳倒乱刃法一样,其实刀使得还是刀法,剑使得还是剑法,世上武功归根结底还是那一套。
  3. 但是千万不可小看了fusion,一个不小心就要丢人现眼,比如这个简单的associative tuples的例子,一不小心我居然没有意识到fusion里的pair是所谓的half runtime pair的真实含义,这里也是为什么要定义那些只有forward声明却不需要实现定义的空结构的意涵,他们纯粹就是作为key而存在的,而pair实际上不需要第一个key的实例,所以,make_pair把它们作为模板参数实例化第二个实参,而始终要保持高度清醒的是所有的sequence都是tuple,所以,不论是fusion的constructor也好还是各种make_xxx帮助函数也好都不过是创建tuple,所以,打脸的快速在于查看源代码,map_impl是一个类似于tuple的重新实现,不如说它就是它自己一个indexed的序列?这行关于怎么获取元素更加的清楚 这里通过index获取pair说明了它的get的和tuple的相似,但是从逻辑上来说我觉得使用tuple来实现也是有可能的吧?无非就是另一个typedef?不不不,因为要实现at_key我不知道原始的tuple是否可能,应该是不行吧?这里的所谓的call/apply概念我还没有建立起来,这些都是mpl的要补课的地方。只不过这里我想记录的是mpl的namespace必须是boost,因为在using namespace boost::fusion;下编译器不可能明白这个新的namespace,总之我觉得boost里namespace是最tricky的地方。因为fusion下面也有mpl,但是那个是在boost下直接添加进去mpl的相当于说一部分mpl的实现是在fusion下添加的,这个做法是迫不得已,因为两个是双体婴儿,但是通常来说大家都不愿意踏足别人的namespace。这个是例外,因为它们是一体的。 补足例子是一个很好的学习过程,先贤们写文档的时候不可能写给所有的9岁小孩子的,因为说不定他的读者里有其他领域的大师,所以,众口难调啊。
  4. 关于fusion的头文件目录结构这一点是很有必要强调的,最宽泛的就是fusion/include下的头文件,它们是可以独立灵活使用的,而且以container.hpp为例,include下的包含了fusion目录下的,而fusion目录下的包含了所有类型的container,如map/list/vector等,所以,你要包含更少的就选择container目录下的具体的类型的map.hpp等等,所以,我觉得文档这里有一个小小的顺序错误应该是这个顺序 就是说include下的container.hpp头文件包含了fusion下的container.hpp

五月二日 等待变化等待机会

  1. 遇到一个小小的问题,就是-Wbool-operation,它是关于bit操作符~的结果被提升为bool的值的问题,这个在boost/spirit里本来是没有问题的,因为它使用了pragma
    
    #if defined(__GNUC__) && (__GNUC__ >= 7)
    # pragma GCC diagnostic push
    # pragma GCC diagnostic ignored "-Wbool-operation" // '~' on an expression of type bool
    #endif
    
    现在的问题是我使用了precompiled header,那么这个pragma似乎就没有用了,也就是说照样warning会爆出来。至少这个是我看到的,我的意思是pch是否会隐藏或者丢弃了原来头文件的一部分内容呢?比较奇怪的是-Wall或者-Wbool-operation都会暴露出这个warning,可是-Werror反而没有出错,让我感觉似乎这个不是warning的问题,也许是debug信息吧?

五月三日 等待变化等待机会

  1. 这位仁兄和我有着一模一样的问题,而且他的头脑比我还清晰,我是对于boost的版本的test_returnable是否使用了SFINAE感到将信将疑,这个怀疑毫无道理,我脑子肯定还是不清楚根本看不懂这个代码的真正的意义在哪里。我决定摘录这段代码记录下来长长记性 这里在说什么呢?我们定义了一个模板函数test_returnable名如其人,其实我们只需要它的返回值,所以使用了指示返回值->但实际上这里的decltype(exp1,exp2)是对我的最大考验因为decltype的语法是这样子的,它只是接受一个带挂号的参数这里又是英文阅读的问题,就是decltype(xxx)本身是可以再跟随参数的,不要把文档里的参数和我们讨论的xxx混为一谈,如果xxx这里要么是所谓的entity,要么是所谓的expression,那么对于expression当然包含了comma expression了,所以,这个是一个常识就是comma expression总是返回最后一个,前面的一切都可以当作是初始化随后被抛弃,而代码的核心目的是利用这个初始化的副产品到达SFINAE的目的。
    I get the general idea: if nullptr cannot be statically casted to a pointer to a function returning T, it will be considered a failure (and not an error) so calling test_returnable will take the second overloaded function and return std::false_type.
    我无法说出比他更贴切的解释。但是我们俩的核心问题都在于什么样的类型是无法返回的?我以前怎么丝毫没有意识到数组是不能被返回的呢?我一定是一直在函数的参数上使用指针,所以从来没有意识到通常也有人喜欢传递强类型的数组类型,而这个时候就遇到返回值的问题了。比如我还是不熟悉怎么传递一个数组的引用的 那么这样子就明白什么是不允许返回的了。这位大侠其实答的是最准确的我却将信将疑。而且发现大家流行使用https://godbolt.org,我在演讲中听到过大侠们对于它的推崇,只是我还想不透它到底玄机在哪里?应该不仅仅是在线编译的虚拟机那么简单吧?不然人人都能做到了?总而言之,数组是不能返回的 ,而对于类,You cannot make a class type that is constructable but not returnable.我想即便没有ctor的类在使用std::declval的情况下也是可以获得值的,更何况获得它的类型呢?至于说variadic argument的问题,我觉得我可以理解提问者的疑惑因为我也有类似的感觉,就是即便你要调用第一种模式你也要给一个假参数0,这一点我没有意识到结果没有给任何参数结果就是总是返回第二个结果std::false_type,所以,回答者的这个包装很能说明问题的核心 我对于这一行代码还是非常的佩服的因为它抓住了核心,我一开始并没有领悟到!
  2. 和libasan类似的如果要静态链接libstdc++的话用普通的开关是不行的,只有这样子-static-libstdc++,这一点我以前没有注意到,这些都是有文档说明的。
    If libstdc++ is available as a shared library, and the -static option is not used, then this links against the shared version of libstdc++.
    这个做法不适合于我因为我有阿三libasan当我使用-static的时候,编译器报错说error: cannot specify -static with -fsanitize=address,所以,这个是我唯一的选择。
  3. 直到这个时候我才意识到我已经从最初的例子阅读偏离了不知道十万八千里了,我究竟是怎么从这里到这里的呢?可能是因为例子里漏掉了 traits::is_sequence这个namespace害得我去查看is_sequence的代码才带偏了。简直要疯了。古人是长夏唯消一局棋,我现在读boost的文档是一页纸就是一个早上或一整天。
  4. 而最最考验一个人读码能力的实际上是后半部分的test_compatible部分就是如何判断两个类型是可以互相转换的?两个类型是否可以转换或者兼容说起来容易做起来难,几乎涉及编译过程的方方面面,我个人以为这个工作除了编译器实时的工作没有任何预设的算法能够解决,说白了就是说要把这个问题交给编译器来判断,这个说起来实在是废话,其实要从类型兼容的使用场景和目的来看,归根结底我们需要类型兼容的目的往往是传递参数,明白了这点就能够想到如何判断两个类型From和To的兼容情况,就是说我们把其中From当作型参,而把To的实参代入这样一个函数就可以了,全部工作交给编译器去判断。 这里最最紧要的是看懂这个void(...)里面是什么。当然这个是在之前明白了decltype(xxx,yyy)的comma expression的前提下才能到这一步。首先,要头脑清晰的明白是以To为型参的一个函数原型实例化,明白了这一点就理解后面挂号里的就是把From进行实参实例化,也就是说函数引用void(&)(To)代入了实参From...这里我开始怀疑自己。。。因为最外面的void()是什么呢?
  5. 在我寻找函数引用的c++标准过程中我遇到这个例子让我看得目瞪口呆。

    struct S { typedef void (*p)(); operator p(); };

    请问p是什么类型?operator p()的返回类型是什么?例子里的错误要怎么改正?这个问题看似不复杂因为

    void (*q)() = S();

    这个肯定是可以的,但是我感到疑惑的是S()是conversion operator在default ctor之后起作用吗?似乎是的吧? 当我做以下测试的时候 我得到了错误undefined reference to `test6()::S::operator void (*)()()',这个当然是因为函数只有声明没有实现,我当然可以简单的实现一下 我从来没有写过这么个函数指针的初始化方法,当然不用声明变量直接返回return nullptr;也是可以满足编译器要求实现的。问题是我能不能使用declval来绕过它呢? 我认为我的理解是对的 我对照这个例子对于一个虚函数你可以,对于一个没有实现的函数当然也是可以的了。 总而言之这两个做法是等价的
  6. 这里是一个小窍门就是当你不确定一个表达式的类型的时候我往往想使用static_assert来证实,可是它的报错有时候并没有告诉你什么,这个时候的上古神器BOOST_MPL_ASSERT具有神奇的作用可以打印出你相比较的两个类型,所以,简单的试错就能获得一些神秘的表达式的类型,当然它是一个宏往往需要双层挂号。
  7. 我尝试了好久几乎绝望了,我只能说我的理解力就到此为止了 也就是说一个函数引用的实际调用,这个是我的猜测,因为

    void(&)(int)

    的确是一个函数引用,这个是我的证明 那么函数引用要怎么调用呢?我对于类似的问题似乎不止一次的感到困惑,这里是摘抄的实验。其中的回答我看不太懂,这里的退化我不是很确定,语法里似乎看到过,但是。。。 对于这个解释我还是有些将信将疑
    Functions and function references (i.e. id-expressions of those types) decay into function pointers almost immediately, so the expressions func and f_ref actually become function pointers in your case.
    如果函数名和函数引用已经是一样了,那么为什么多次的deference还是成立呢?(****f_ref)(4);
    "Why function pointer does not require dereferencing?"

    Because the function identifier itself is actually a pointer to the function already:

    4.3 Function-to-pointer conversion
    §1 An lvalue of function type T can be converted to an rvalue of type “pointer to T.” The result is a pointer to the function.

    这个解释我理解起来有困难。

五月四日 等待变化等待机会

  1. 这里讲到的是更高级的层面就是两个类型之间的转换的前提,因为是针对类所以才有了constructable的问题,但是在我的例子里boost偏重于returnable,这个也许是一回事吧,因为returnable隐含了constructable因为返回一个对象的实例就要求能够constructable。可是我的疑惑在细节就是一个函数引用是否成立是应该把它进行模拟的调用,那么怎么做呢?
  2. 这个关于bind函数引用的帖子很有深度,其中关于perfect forwarding和move的微妙的地方不是一般人所能理解的,我看过相关的视频也还是一知半解。回答者的水平往往要比提问者高出至少一个数量级,这才有可能传道授业解惑,所以,教师是非常困难的职业,尤其是现代那种机械的基本的教学任务可以自动化的情况下,高级的教师将来可能是人工智能唯一无法取代的职位之一吧。
  3. 我看到了boost版本的is_convertible_basic_impl,这个就很好理解,几乎任何人一眼就能明白其中的奥妙,我们可以利用模板函数的参数传递来识别编译器对于类型转换的支持,这个远比普通函数的参数传递时候类型转换来的容易的多,而且我觉得这个也符合meta-programming的风格,两相比较高下立判,因为依靠declval来模拟调用函数是很麻烦的吧? 让我们来分析一下这段代码,首先这个是传统的SFINAE的风格的继承沿用了返回值大小来判断哪一个specialization被选用了,这个在理解上比较容易。其次,同样的读者要头脑清晰的明白使用decltype(x,y)的技巧,逗号前面的初始化的副产品是否成立决定了逗号后面的结果是否能够返回,而这里内心要始终默念那句六字箴言Substitution Failure Is Not A Error!编译器不会报错!不会报错!不会报错!编译器只会静静的尝试下一个two而这个是一个generic的模板是永远成立!再次,那么既然返回two的test永远成立那么是否这两个模板函数声明的顺序会影响编译器尝试的顺序呢?不会的,因为我们故意定义one的参数是int,而two的参数是不定,这一点决定是模板的specialization更加把one作为首选!这个就是定义模板函数实参类型的必要性!重要!重要!重要! 最后一点是不言而喻的,我们对于函数感兴趣的地方只有返回值,因为sizeof对于函数来说实际上测量的是函数的返回值,所以,使用decltype来定义函数的返回值来实现就是一个明显的选择。最后一点就是说这个老套的做法已经不时新了,因为既然我们关心的是返回值何必要依靠返回值的sizeof来多此一举,直接使用true/false不是更加的直接了当吗?而且新派的做法是根本就直接继承自std::integral_constant,让代码更加的简洁和容易将来扩展!
  4. 我盯着看这个例子看了好久才明白我的疑惑的根源在于我没有意识到我犯了一个错误 的确是函数的引用,但是模板参数To却是函数的型参,所以,它和 合在一起是代表的含义是在问编译器:型参To能否可以被实参的类型From的转换请求所接受? 当然我认为我以前的判断依然是对的两者合起来的确是在调用函数,而这一点对于metaprogramming是不必要的,也不可能的,所以,要用某种方式达到检验语法正确的方式使用外边包裹一个神奇的void(),但是这个是什么我依然不清楚。从第二种使用新版requires关键字的实现方式来看,在大挂号里直接调用函数是检验语法是否成立的一种方式,这个省却了逗号表达式的初始化副产品。 而且新版关键字noexcept能够大大简化目标代码的生成。至于说检验returnable的方式也更加的直观了就是直接使用static_cast。直接使用requires的关键字省却了利用编译器特殊性的SFINEA的奇技淫巧,那种在函数声明返回值里使用逗号表达式给我的感觉是螄螺壳里做道场。
  5. 长久以来我对于Ubuntu的自带的智能拼音输入法颇有微词,原因其实是盲目崇拜谷歌微软搜狗之类的,其实这个是不动脑筋的武断。仔细一想就明白了,这个输入法的池子其实并不深,原因是能做到的大家几乎都做得到,唯一的差别是上下文的词汇,而这个领域说难其实很难,说容易也很容易。说难是因为从科学的角度来看这个极其的难,因为汉语多音多义字极多,根据上下文判断的算法几乎没有完美的。说它容易因为科学解决不了的问题,工程师可以给出一个近似解,而且往往还很有效。比如人工智能目前如火如荼的热,可是真正的算法突破也许和几十年前相差不大,不过是工程师的解决方法,以前不敢想象的计算资源现在有了,以前认为太过于庞大的计算现在可以用硬件来解决了,于是算法的问题原本认为用软件的方法无法解决结果用硬件解决了。同样的,输入法里用户的体验的关键是词库,而这个词库的更新与创建几乎没有什么技术的成分,一个是个人使用习惯数据的累计,一个是软件提供商的分发。前者也许要用各家的帐号登陆来收集个人隐私数据,后者辛辛苦苦给别家做嫁衣裳,因为数据格式一旦被人洞悉立刻就被大家拷贝成了拿来主义的牺牲品。总而言之,我发现这个智能拼音底子并不差,我只要找到正确的词库来更新就好了。这里有一个号称转换各家词库的软件,不过我还懒得看源代码。倒是这一个博客关于智能拼音配置文件值得看看。总之,我目前知道用户数据导入导出用户界面可以添加我的个人习惯词库。不知道是不是一样的。此外sqlite3的命令行工具直接操作数据库也是一个好东西。
  6. 对于智能拼音的源代码里,我发现了一个数据文件可以让你实现怎么样把汉字转拼音,所以,当然也可以反向转换,而且longest match可以解决多音字的问题,只要有足够多的数据,就是说把一个多音字的所有的使用场景都收集起来,这个方法看起来很笨,可是人类难道不也是这么掌握多音多义字的吗?

五月五日 等待变化等待机会

  1. 对于spirit的看似奇怪的语法我渐渐的适应了,因为从parser的角度来看是更加的舒适的,联系之前的bison/flex的做法就一点儿也不奇怪了。直到我遇到要把一个字符串push_back进一个vector我才犯了愁,原本对于place_holder_1我就是很不明其所以然,它应该是仅仅重载了=等等的少数操作符吧,对于要使用成员函数似乎只有boost::bind的一条路,可是之前我已经有了bind对于重载成员函数绑定的噩梦经历对此是有抵触的,结果突然发现phoenix降临带来了涅盘复生,难道这个就是取名phoenix的原因吗?如果没有这个push_back我又会卡壳的,因为boost::lambda也未必可以吧?其中如果要使用phoenix::push_back也要一起使用phoenix::ref,也许内部和boost::ref甚至于std::ref都没有差别,可是谁知道呢?现在我的头脑被spirit/phoenix/fusion/mpl缠得一团浆糊,谁是谁也分不清楚。
  2. 我现在已经彻底混乱了,需要理一下思路:fusion依赖mpl这个是不言而喻的。phoenix依赖于fusion,spirit依赖于phoenix。这是一个鄙视链吗?我之所以要搞清楚的部分原因是我的pch里面摘录了例子里的各种各样的头文件,而很多时候例子希望能够包含一个最少的头文件,而boost的各个库的风格又有些许差别,有些更有条理一些,但是有些是为了效率设计。

五月六日 等待变化等待机会

  1. 我的困惑经常就在于我想要的也许是最最无用的功能,比如parser的generator从来不认为你需要一个matching一个字符串,这个根本不用实现。那么对于这个要求压根就没有意义,直到看到这个基本的部分我才觉得似乎比较开窍。让我感到烦心的应该主要来自于spirit对于操作符的重载已经到了另一个阶层,对于我来说就是登峰造极,把>>改成>就成了所谓的expecting我实在是难以理解。这里是几乎所有的操作符重载的表,这个比例子里的无序的介绍要清晰的多了。
  2. 这个表关于attribute非常的复杂,我还需要更多的经验才能理解。
  3. spirit的难度绝对是相比很多库高了一个数量级,这也不奇怪因为它是综合运用mpl/fusion/phoenix的一个非常复杂的库,而且它自身的特点导致它比某一个特殊领域的库比如wave来的复杂的多,因为后者是如何使用前者的创造的工具。所以,现在回想起去年我在fusion/phoenix就已经卡壳是正常的,我现在的体会是与其一开始就学习boost不如先把标准c++搞明白,因为后者显然是去粗取精经过了严格的论证与充分的实践考验,而更重要的是两者的巨大区别在于boost是在不依赖于c++语言的新特征情况下实现它们甚至扩展实践,这其中的艰难是和改进编译器的背后工作不可类比。
  4. 竹石

    咬定青山不放松,
    立根原在破岩中。
    千磨万击还坚劲,
    任尔东西南北风。
    一幅画就是一首诗,一首诗就是一幅画。一个时代是千千万万的画卷的展开,一幅幅画卷是一个个鲜活的人生。生长于一个大时代是有幸的因为你可以亲眼目睹沧海桑田;沉浮于一个大时代又是不幸的因为你会用一生来看倦潮起潮落。幸与不幸要跟谁人诉说?也许只有刘慈欣的“最璀璨的银河”可以让后人了解这个大时代变迁的亮度。
  5. spirit的代码实在是太难懂了,它完全改变了你的编程习惯,基本上老派的程序员习惯于写回调函数或者更进一步的functor之类的,可是在这种spirit面前都不灵了,你写的是constructor里的类型,或者是模板的特殊化,总之我连跟踪都不知道要怎么设断点了,比如你把所谓的attribute设置为一个vector/set/list它能自己帮你添加元素,这个是怎么做到的?我只能一步一步跟看看在哪里去呼叫container的方法,很辛苦的找到container.hpp的一个functor叫做很贴心的名字push_back_container,这个似乎是模板类,有默认的调用insert,或者类似fusion的push_back,可是对于我传入的一个类型spirit是怎么知道要调用什么方法呢?如果我传递一个自己定义的类呢?这个实在是太神奇了。我做了一个恶作剧故意使用stack,结果编译出错说没有member函数insert,原来它只是针对大多数container都有的insert方法而已,当然我估计你可以设定自己的方法吧?总之,看到这里才稍稍的消除了一点我对于这个庞然大物的敬畏之心。
  6. 另一个对于程序顺序执行感到怀疑的问题是关于这个on_error我一开始看不大懂这个on_error到底是什么,最后才意识到它是成员函数,只不过它是一个模板成员函数是给所谓的handler设定参数也对也不对,它的确在这里是成员函数,可是更通用的是使用那个同名的自由函数来做这件事。,不过对于placeholder我一直没有概念感到费解,心底里始终认为是类似于宏的东西,实际上应该是模板,但是怎么实现的我感到很困惑。我现在的理解是调用这个模板成员函数on_error就是把error_handler这个模板functor实例化,这个是通常的模板参数推理的机制吧?总之spirit的代码很玄妙,功力不够很难看懂。

五月七日 等待变化等待机会

  1. 感觉这个例子还是让人很有感触的,就是揭示了一些create_parser的概念,这个地方水很深,只不过我不知道是不是用户被鼓励自己去在traits namespace里添加自己的deep_copy?应该有更好的办法吧?难道是因为要让auto_实现这个功能才需要这样子做呢?我现在的功力还差得不是一点半点,完全不能领会这些例子。
  2. 刚刚才注意到这些库有一个共同的特点就是采用新派编程的所谓的model的思想,感觉这个和新feature里requires有异曲同工之效,这种思想我现在还只能慢慢体会。
  3. 这里又是一个例子在trait名字空间里个性化,这个难道是建议的吗?
  4. epsilon是一个debug的神器,是测试语法的断点,以后要多加注意使用。
  5. 先锋艺术家乌合麒麟又出新作了!G7这个组织究竟存在的意义是什么?它们要代表谁的利益?在当前世界经济大萧条大危机面前作为世界经济最发达的七大工业国本应当应该承担起的恢复世界经济领导抗疫斗争的责任,可是在联合声明里我没有看到一丝一毫的相关话题,这个组织出了清谈作秀就是满足美国一国的私利作为维护其超级大国霸权的工具。G7里隐藏了多少肮脏的秘密呢?你是否都能找出来呢?这幅画里有很多的关键点,看你能不能都找出来呢?
  6. 这一点是我之前没有理解到的lit, like string, also matches a string of characters. The main difference is that lit does not synthesize an attribute.

五月九日 等待变化等待机会

  1. 《三体》里有科学边界,那么对于个体是否也存在呢?一个人的精力能力与意愿应该都是这个边界的因素,甚至是健康状况也是一个制约因素。对于这个操作符&的重载,其意义我有疑惑,究竟什么才是 If the predicate a matches, return a zero length match. Otherwise, fail. 我把测试例子稍稍修改 这里我实验了一下epsilon作为debug的功能,它的[]操作符重载接受一个functor返回值为bool,所以,我去定义了一个简单的error 我曾经想直接在[]里声明我的lambda,但是直接嵌套[[]]不行?也许是因为操作符[]已经被重载了,这个难道会对于lambda的语法产生混淆吗?要知道lambda是语言本身的特性啊!我最初的疑惑是那个原始的&操作符并不能保证;是结尾的输入,至少test_phrase_parser的第二个参数是false制止了对于输入的完整的扫描。具体的功能是什么要看原始的定义,那个例子的说明是作者的想像发挥了。当然我是吹毛求疵了作者仅仅是说功能是验证最后一个输入字符,这里显然指的是空白字符之后,我去自己理解为一行输入的最后。
  2. 对于expectation operator我始终理解有困难,难道它的差别就在于失败是安静的返回false还是抛出异常吧?这里关于on_error的论述让我感到困惑。
    Using on_error(), that exception can be handled by calling a handler with the context at which the parsing failed can be reported.
    我的理解是on_errorgrammar类的成员函数on_error是一个自由函数是定义在qi名字空间的,这一点我不知道为什么会搞错,也许是当时的例子使用的是grammar的成员函数,所以,作者指的是使用这个自由函数来设定。但是我遇到的困难是on_error需要我把rule作为参数传递进去,我怎么把这个compound expression定义为一个rule呢?它的attribute是什么呢?这里的表让我无所适从。,我要怎么对于这个operator >定义它的on_error呢?结果下面文档说是抛出异常,这实在是让人摸不着头脑,看来on_error是作者的想像吧?我唯一能想到的是定义一个grammar来做这个on_error
  3. 我只好使出了无赖的做法来定义auto,我可以骗过编译器但是骗不过运行期。 看来这个和直接定义的参数传递是有区别的吧? 看来auto是骗小孩子的,这两行代码的区别在我看来唯一的差别是也许有临时变量传递的问题,可是我也不能使用phoenix::ref(r),那怎么办呢? 我又做了一个简单的实验就是auto和使用decltype是没有什么区别的,比如 我检查了它们的类型,至少从编译器来看这些类型的翻译是没有问题的,那么运行期内存出错我猜测有可能是spirit的parse的实现有问题,我没有花时间测试不同版本编译器。

五月十日 等待变化等待机会

  1. 关于昨天内存出错的问题我的猜测是也许和这个比较奇特的继承有关系吧? 关于rule和grammar的关系我没有搞清楚。首先,rule就是parser。
    The rule is a polymorphic parser that acts as a named placeholder capturing the behavior of a Parsing Expression Grammar expression assigned to it. Naming a Parsing Expression Grammar expression allows it to be referenced later and makes it possible for the rule to call itself. This is one of the most important mechanisms and the reason behind the word "recursive" in recursive descent parsing.
    那么grammar是什么?grammar是rule的组合,当然也包含比rule更原始的primitive parser。
    The grammar encapsulates a set of rules (as well as primitive parsers (PrimitiveParser) and sub-grammars). The grammar is the main mechanism for modularization and composition. Grammars can be composed to form more complex grammars.
    但是当你写一个expression你能分辨谁是rule谁是grammar呢?我觉得这个继承并没有什么内存分配释放的问题。
  2. 不过我也发现了一个细节就是phrase_parseparse很显然的参数类型是不一样的,但是这个模板函数使得参数的分辨很困难。而且从这里可以看出来auto得到的类型和rule是不一样的,它是expression,但是这个类型究竟差别在哪里呢?这个问题超出了我的现有能力。

五月十一日 等待变化等待机会

  1. 对于一些小的细节要注意。比如对于alternative,返回的attribute究竟是什么呢?我以前没有想过,其实一想就明白应该是variant。但是进一步的是我能不能用std的variant,而不是boost的呢?想法似乎很自然,两者区别还是很大的,但是更重要的是我对于attribute的理解还不够,我忘了这个需要在trait之类的名字空间里加入自己的对于这个attribute的实现才行。所以,这个想法草率了。
  2. 其中一个最容易混淆的地方是这些操作符的重载,我模糊记得它们似乎在别的场合下也被重载了,似乎是rule吧?不确定。。。
  3. 似乎有些开窍了,这个attribute是optional应该是顺理成章的。
  4. 关于这些optional的attribute我一开始认为直接访问optional的实例本身就好了,后来看了这个例子才意识到最准确的做法还是例子里直接使用fusion::at_c的方法直接查询parsing结果的那个tuple,这个才是正道。这个也是体现fusion的强大的地方。它直接反映了parsing的结果,而不是依赖optional本身的存储结果来判断。就是说依赖判断tuple里的类型参数的方式。这一点我似乎很容易犯迷糊因为那个vector很容易让我产生幻觉,以为是存储的形式,忘记它是tuple,因为是异构的类型。本来就只能使用fusion才能访问,它本来就是fusion的部分。

五月十二日 等待变化等待机会

  1. 今天终于意识到昨天为什么会犯那样的错误,原因是这个还是超出了我的认知,就是attribute究竟是什么?有时候我觉得是一个类型,这样子的话意味着我应该传递一个fusion的vector来提示parser返回的结果的类型,当然这里的所谓的fusion的vector是可以附加实例的,否则这个就是纯粹mpl的vector了,fusion就是在mpl里添加了这个超级功能所谓的融会贯通所以才起名叫做fusion,就是融合。所以,当然我其实是现在才想到这一点的,因为以前看到的例子都是看到直接把普通的std::vector传递进去,当然这个肯定是一组同类型的结果了,这个可能性不是没有毕竟比较少,如果都是一个类型的话估计使用parser的意义也不大了,毕竟就简单了嘛。可是这个看似理所当然的事情我却没有意识到,因为这个本来是应该传递类型我却传递了一个stl的vector的实例,这个现在看起来当然也是有道理的,因为这个自然是类型推导才能做到的。可是如果脑子不清楚看到两个例子摆在一起的话,你是否像我一样的发出惊叹呢? 没有对比就没有伤害对人类来说应该是没有对比就没有认识。人类认识世界的唯一途经是依赖于认识图景的不同,而最直观的差别对比是最基本的方法。
  2. 我费尽心机的尝试qi的例子的时候发现spirit还有一个很大的部分叫做lex,难道很多的实现已经是现成的了吗?这里谈到的使用spirit::lex的优点只有真正实践才能体会到吧?而它的必要性也是同样需要我开始实践才能明白。
  3. 发现了一个很trivial的bug就报告了一下,一个很好的练习。
  4. 遇到另一个小问题,我觉得答案是这里。我也提交了一下这个小问题

五月十四日 等待变化等待机会

  1. 我满心欢喜的发现一些例子编译的问题提交了bug结果发现维护人员不是很愿意去修正,这才意识到整个库已经转向第三代了,我看样子是浪费时间了。现在从第三代看起吧。
  2. 我不确定第二版的parser是否有类似的功能,但是这一个context的功能是很有用的。一点体会就是x3显然让你觉得和v2的语法形式上没有区别,除了需要在x3的namespace下外,这个本来就是一件好事,只是要小心引用正确的namespace。
  3. 关于这个context的访问函数我发现我的1.72版无法正确的显示_val,据说它是rule的attrib,可是我查看代码它始终都是使用unused,我于是怀疑是否最新的boost也是这样子,于是才有了编译最新版boost 的问题。我编译了最新版的boost发现并没有变化。这个就不明白这个是不是既有的问题了。
  4. 重新编译boost,却老是忘记要recursive

五月十五日 等待变化等待机会

  1. 这个是x3的相对应的parse结构的例子,其中的一个细节就是关于两个不同的char_的问题,首先x3::ascii::char_很显然的是ascii的char,而x3::char_是所谓的standard的,具体来说我如果在后者输入中文是可以正确识别的。需要强调的就是lexeme是所谓的parser directive,真正的parser是其中的char_所以,想明白了这一点才能理解这个为什么能够去除引号。 如果没有阅读这个关于attribute collapsing的话我是绝对无法理解这其中的关键的。这个就是文档的威力,有些东西是一目了然的,有些就是关键的,哪怕一个字一句话都是理解的关键。我对于这一点是信服的因为我之前在gdb里看到那个繁杂的sequence里让人望而生畏的模板参数,而其中有些代码里的确是在做过滤unused这类无用的attribute,所以,看到这个解说我是立刻得到豁然开朗的感觉。
  2. 一段关于symbol table的话的深刻让我久久无法彻底理解!
    Traditionally, symbol table management is maintained separately outside the BNF grammar through semantic actions. Contrary to standard practice, the Spirit symbol table class symbols is a parser. An object of which may be used anywhere in the EBNF grammar specification. It is an example of a dynamic parser. A dynamic parser is characterized by its ability to modify its behavior at run time. Initially, an empty symbols object matches nothing. At any time, symbols may be added or removed, thus, dynamically altering its behavior.
    怎么理解呢?首先symbol table是什么呢?我的理解就是AST里的节点名字,那么既然已经是到了AST阶段当然是semantic action了,那么对于EBNF来说本质上是语法的检验是抽象的语法元素的并非实际的输入,所以,照例说不应该有symbol table的存在,因为那个是实际使用parser的运行期建立的,可是spirit是一个很特殊的parser,它的建立也许就是parser的使用,两者是在同一个编译过程。与之对比的是flex/bison模式是先产生parser的代码再进行编译的两个阶段。至于说dynamic parser从数据结构的角度来说并没有什么很深奥的道理,只不过因为parser本身的特殊性导致了symbol table的表面上看起来是动态的,试想连parser本身也是动态的就不奇怪了。
  3. 如果没有看这段关于这个复杂的宏BOOST_SPIRIT_DEFINE的解说我是无论如何无法理解什么是rule的!部分原因是我承袭了v2的理念就是有所谓的grammar的概念它把一系列的rule集合成一个复杂的所谓的grammar并且包装成为一个类,这个是非常符合面向对象的传统的,难道不是吗?可是x3摒弃了这个思路,现在声明rule的意义在哪里呢?因为在我看来现在这个宏就是声明了一个overloaded的函数可以被调用而已,我可以理解为了和v2兼容你必须保留那个parse函数的参数形式内部去调用这个实际的parser的parse_rule的方法,内部再去调用所谓的rule_def的parse方法。总之我没有看实际的代码感觉这里并不是很漂亮,约束完全是不合理的要求rule的前缀加上_def作为parser的入口这个设计并不是很好看。我不理解为什么要抛弃grammar的想法。
  4. 有一点我忽略了就是关于传递的iterator第一个是引用,所以这个要小心,从这里看到你是可以得到当前parse函数返回的iterator的指向的,所以要注意不能传递rvalue!我的头脑还是不很清楚,这里iterator本身是const的但是你必须传递一个iterator的lvalue,就是说之前我曾经想要phrase_parse(str.begin(),...是不可以的。
  5. 无意中搜到一篇很有深度的帖子,当然我要说发光者有很多时候是被激发的,作为激发者本身能够引起这么灿烂的发光者的迸发本身是很有能量的,这一点正如引爆炸弹的雷管音信本身爆炸起来也是非常致命的,所以,我对于发帖者本身也是崇敬的,因为他的帖子我看的似懂非懂,也是牛人才有这么大的怨气。
  6. 但是让我感到泄气的是spirit似乎已经停滞不前了。也许这样子的复杂的parser很难被推广吧?

五月十七日 等待变化等待机会

  1. 有些事情本来很清晰,印象却是在搅混水,比如string literal到底是存在于哪里的?我想但凡学过编程的都知道是elf里的section里存者的,那么它是static storage的属性也是不言而喻的,那么这个还有什么可疑惑的呢?难道说我在函数栈里声明一个临时变量初始化为这个string literal它的消失是随着函数的调用结束就被释放了吗?指针是的,指针指向的literal呢?当然不可能被回收,这一点有什么可惊奇的呢?
  2. 我之所以又在捣鼓这些东西是遇到c++20淘汰了不少的旧函数,比如这个例子介绍的这个函数codecvt我没有什么印象了而例子里cout输出宽体字符wchar_t的函数在c++20里被删除了,于是我只能降级到c++17来编译,这个时候需要用到-fconcepts-ts,这些都是小事。
  3. 我想要说这是最后一次记录下这个c++标准网址,我总是忘记又找不到关键字来搜索。
  4. 关于string literal让我不止一次的烦恼,在以上的例子里这个 这个+u8的加号是什么意思呢?这种问题根本没有办法去google,你都不知道要怎么问问题,看标准也没有,改成-就出错,不用也可以。那么标准没有说的东西编译器怎么处理的呢?
  5. 对于这么一个例子里的escape string \z是什么我都找不到答案。眼花了不是escape,就是z这个是什么意思呢?DAMN!没有意思就是一个字符的作为对照的例子。我头脑发昏看成了escape了。而且偏巧还能google到这个\z居然是非标准的。
  6. 对于例子里的utf-8字符串为什么等价于我颇费了一些思绪,最后才弄明白是utf-8编码去除那个msb的bits然后平移得到的,这个本来是常识,我对于编码的感性始终停留在纸面上没有亲自实践中就反应不过来。
  7. 我对于例子里把utf-8转换为utf-16毫无兴趣,我不知道这个有什么应用场景,但是对于说wifstream在fin.imbue(std::locale("en_US.UTF-8"));之后可以正确的读取一个一个utf-8的字符印象深刻,因为不论是长度为1或者2,或者3,或者4,可是这里我还是犯糊涂就是我始终认为中文大部分都是3个字符,那么它是怎么表达成2个字符的呢?这里也许是我的误解吧?只是我想不清楚,不过有一点我算是搞明白了,所谓的wchar_t的长度居然是4,也就是说传说中的都是真的,实际上就是把多字节字符当作整数来处理的。

五月十八日 等待变化等待机会

  1. 今天头脑清醒了一点,我觉得spirit的值得借鉴的是如何运用fusion来实现一个很好的应用的样板,至于说它的具体的细节我还不是很了解,也许对于它之前的前言说明里的EBNF语法产生器的目标走的太远了一些吧?我觉得完全不一定要实现如此复杂的操作符重载,因为至少我以为这个复杂度实际上是在实现regex的做法,也许这个是项目的目标。但是对于我来说是过于复杂了一点。昨天看了一篇科技猿人的文章谈到人工智能的发展前景,中心思想是可能人们低估了它的困难度,我有些赞成,最关键的一点是否可能量变积累能够导致质变。我在洗澡的时候在想如果能够实现所谓的反射,比如GPT里设定了那么多的参数经过一段时间的强化学习是否可以输出增强的输入设置作为下一阶段或者同类机器的学习输入呢?肯定是在这么做的,比如alphaGO的学习结果可以作为下一代的Master的程序的强化学习的指导或者校验员,但是如果能够直接输出它的配置文件作为master的输入不是省却了这个起始化的学习过程吗?想法是好的关键是甲的输入无法成为乙的输入,根本没有什么通用的描述语言,如果spiriti之类的工具能够实现万能的parser也许可以动态的实现解析任何描述语言,可是世界上根本不可能存在这样的能力吧?我对于去年李伟的那本Metaprogramming的能力至今没有理解到,在meta language层面实现所谓的反馈究竟是什么意思呢?也许我应该继续学习那本书。只不过meta实在是太难了,首先就是工具,是否有什么好的编译器是专门针对meta programming呢?我好像google到一个。但是这个领域根本不是一般人能够涉足的领域。
  2. 我现在才有点明白我去年研究了半天的中文编码,其实连最基本的都没有搞明白!我居然没有搞明白CJK和GB2312根本不一样!这个简直是忍无可忍,我怎么会以为它们是一回事呢?撇开政治因素不谈,它们根本就不是一回事,cjk的设计是出于中日韩三国语言中的共享汉字部分,根本和国标是两回事!换言之,CJK就是unicode的中文部分的实现,这个理解不对吗?
    This encoding characters has defined 20902 CJK characters. The advantage of using this standard is that you can display Simplified Chinese characters, Traditional Chinese characters, Korean characters and Japanese characters on the same HTML page. No other encoding standards is supporting that for the moment.
    我以前以为GB2312和unicode兼容,其实应该是很容易转换而已吧?
  3. 还有一点我去年居然没有理解到,GB18030和GB2312的巨大改变是前者有包含繁体字,这个好处在于繁简字体转换,因此如果你在GB18030和BIG5之间做了mapping的话繁简字体转换就容易多了,至少是单纯的字体是没有问题了。可是GB2312因为不全无法做到容易的转换。
  4. 这里的解释远比我读了那么多的文档来的简单扼要!这个是关于GB2312的概要:

    GB (Guo Biao) Code is defined by China. It is the encoding standard used to represent Simplified Chinese characters. It has defined about 6763 Chinese characters (excluding all symbols). Countries such as China, Singapore and Malaysia are using this encoding standard.

    Every Chinese character is represeneted by a two byte code. The MSB of both the first and second bytes are set. Thus, they can be easily identified from documents that contain both GB characters and regular ASCII characters.

    多么简单扼要,中英文混用没问题!关于GB18030远比我理解的深刻:
    The Chinese authority soon realized that it cannot ignore the traditional Chinese characters. Thus, it had defined GBK (Guo Biao Kuozhan) to include all the traditional Chinese characters defined in Big 5. It claims that GBK is synchronized with Unicode standard, version 1.1.
    不过我对于synchronized with Unicode standard的意思还是不太理解,是说补全了吗?关于大五码我的理解是编码的原理和GB是相似的,也许不同的地方在于没有包含所有的ascii符号,这一点在当初GB2312的说明文档里似乎是一个值得炫耀的因为它营造了一个纯中文编码的环境不需要中英文混杂。这个是有一点见仁见智吧,不过总的来说不是坏事。
  5. 我也终于意识到我的旧日记的确是unicode编码就是CJK编码,这个很有可能是当年使用的编辑器的缘故。也就是说使用了这种Html entity(就是&#code;这个形式),我找到了这个很棒的网站比如这个字的编码,它是CJK的编码不是utf-8,所以,怎么转换是一个要费事的工作,因为iconv根本不可能理解到这一个问题,这个就是当初我使用iconv批量转换GB2312到utf-8的时候漏掉的部分。
  6. 关于怎么把unicode CJK code转换为utf-8呢?我对于c++文档的理解有困难,所以对于这个函数的使用搞错了,现在才知道正确的做法。 读取文件到一个string是普通的步骤,这样子省却了regex的麻烦。我已经无数次这么做了。 设定regex只搜索html entity,并且设定了codecvt 接下去的这个搜索替换拷贝的过程是标准的流程 这里对于把unicode的数字码转成整型似乎没有什么更好的办法,因为通常对于大批量转换是采用直接输入宽字符子串的办法,而html的这个entity在我看来是一个特殊的unicode的表达方式,也就是html的表达法不是普遍的做法,所以,也没有什么函数对应来解决。

五月十九日 等待变化等待机会

  1. 十年前我是如此的思想幼稚偏激,今天我重新读自己以前的日记给予了全面的批判
  2. 我对于自己的反思实在是不一而足,简直是要洗心革面重新做人了。
  3. 据说这个问题是中国最高层授意中国驻美大使崔天凯来发问美利坚合众国政府的,但是据说美国的智库说没有收到。唯一的解释是假装没有收到,对于这个态度是典型的没有答案本身就是答案。就是说美国没有准备不!压根儿没有打算和中国和平相处,因为高傲的美利坚认为和美国平起平坐不是一个错误而是一种犯罪。

    丢掉幻想准备战斗吧!

    Peace or War?


五月二十三日 等待变化等待机会

  1. 有时候知道的太多反而容易出错。我知道gb18030把所有的ascii字符都进行编码,也就是说标点符号数目字也都有中文编码,换言之原本一个字符能够标识的英文标点符号数目字在gb里要两个字符,而到了utf-8变成了三个字符,于是乎在我处理以前的日记编码的时候,我就往这方面去想,却忘记了大家通常也可能是中英文混编的,实际上很多时候我都是在标点符号数目字使用半角,结果更糟糕的是我使用midnight commander的时候总是忘记它能够很智能解析文件格式,比如去除html里的各种tag,结果让我看中文的utf-8编码感到莫名其妙。瞎折腾了许久,就为了怎么去除中文utf-8编码里的空白字符。这个问题在regex里是一回事,因为偏巧他们都是ascii编码,可是我偏偏糊涂了,我实际上需要的是搜索0x20而不是经过了从gb2312转换到utf-8的0x2420
  2. 另外在regex里一个我屡次都在犯的错误就是一个字符串在regex里空格是无意义的,我总是忘记把它转为\\s。而对于通配符.(dot)是否包含换行符我总是忘记,对于utf-8的话简单的\\w是不好用的,这个简单的trick是必须的[\\s\\S]*?注意最后的lazy?是必须的否则肯定要栈溢出。那么对于搜索Html的配对的tag怎么做呢? 这个已经是经典的帖子了,就是使用negative lookahead 当然了这个对于复杂的嵌套是无能为力的因为据说html不是所谓的regular language吧?

五月二十四日 等待变化等待机会

  1. 我很久很久以前印象中string的find方法现在我才意识到这个函数的签名变成了constexpr,这个是一个很好的c++20的改进。可以搜索一个字符串里的任意字符,结果昨天花费了很多时间debug今天才意识到这个是记忆的问题,那个函数是find_first_of,而且它也也改成constexpr这个实在是太棒了。

五月二十六日 等待变化等待机会

  1. 学而时习之,回头看visit依旧是那么的目眩神迷!当然啊现在比几个月前的无比震撼要好多了,至少我可以看懂一些了,然后我就照猫画虎的活学活用一下。首先我创建了一个函数返回任意输入参数的一个万能的variant的vector,声明这样子的类型是一个痛苦,所以最好是直接使用变量的decltype来图省事: 这里我相信如果不使用guidance编译器会不知所措或者误解你的意思的。总之我避免了声明这样子的varaint类型因为这个是由使用者来决定的,比如 那么怎么使用这样子一个怪物呢?这里就明白visit的强大了,对于这个奇妙的函数我以前是不能理解的,因为只有真正的使用才能明白它是怎么样子解除你的痛苦,可是真正的理解它远远不止于这里,它的实现肯定现在对于我还是一个比较复杂的,我的体会是它让你可以自由的访问variant的各个可能的成员的一个通用方法,你不用再去自己写那些令人尴尬的index或者是捕捉异常的std::get等等if-switch之类的。总之visit把你可能遇到的variant的成员类型都包含了,剩下的就是你自己去写一个通用处理的方法了,这里有意思的就是假如我们的variant包含一些复杂的类型我要怎么处理呢?比如对于普通的POD类型我们可以轻易的使用std::cout输出,如果我定义个类比如这样子的无穷多种的模板结构 然后我生成了这么一个复杂的variant 我原本是可以在visit里这么自由输出变量 现在肯定是不可以了,因为很明显的新的结构是不支持的,我天真的想避开这个企图在visit的lambda里靠类型判断来避开编译错误比如 这个做法是不可能的尽管我使用了constexpr想让编译器对于结构做特殊处理,但是因为visit的实现机制导致这种类似于所谓的方法的做法并不是一个真正的动态方法,它实际上是所谓的静态多态吧?这个是我的猜想总之唯一的解决方法是重载结构的operator 早晨起来,折腾了大半天感觉就像做了一些脑力体操,挺累的。
  2. 回过头来看我突然觉得,如果我能够这样子的创建任意的类型的异构向量,我不就等同于实现了tuple吗?那么fusion里最大的一个贡献不也就是这样子吗?或许它就是这么事先的,而tuple里最大的无奈莫过于无法向向量一样的访问成员,因为fusion里的类型与值的匹配和vector<variant>机制是不一样的。总之我感觉看到了一扇门,但是不太清楚是怎么样子的。
  3. 这部片子Unlocked是我现在在youtube上看到的最好的一部免费电影,至少目前如此。好就好在它的内容与当下的国际大背景。2017年预测到了铅019年底也许是美国军方的一小撮人导演的世纪大瘟疫流行,的确作为美丽国内不论是军方还是情报界的少数狂热的爱国者们,他们夙兴夜寐为他们所钟爱的祖国的前途所担心,作为牧羊者他们有责任看护好天选之民的国家国运长久,所以作为wakeup call,他们要用极端手段来唤醒美国民众给他们特权来保护他们,但是这个需要精确的数学计算到底要杀死多少美国人才能拯救多少美国人,这个是一个冷血的数学模型,比如蜘蛛993年俄克拉荷马的大爆炸死的人不够多,所以,恐怖主义的威胁没有引起美国人民的注意,因此要发动911恐怖攻击成为必要;而埃伯拉病毒同样没有引起美国人民对于生物战的重视,因此有必要发动这次世纪大瘟疫的生物战。具体实施者当然是极端伊斯兰恐怖分子,只不过中情局在恐怖组织的决策与执行机构的通讯中穿针引线帮助他们顺利实施而已,所以,严格说来的确是恐怖分子发动的,只不过在中情局的严密的监督下成功实施而已。

五月二十七日 等待变化等待机会

  1. 对于variant的理解是欠缺的,比如我昨天还在怀疑作为tuple的替代是否允许同类型出现多次呢? 这里如果不给指示std::in_place_index的话编译器会抱怨deduction之类的错误,这背后的机制很深奥,我还没有去看代码,估计会有困难吧?因为即便参数类型不需要转化的string也是会报错,所以,我不知道为什么不能依赖第一个匹配的类型而一定需要使用index来确定。
  2. 关于这个visit的确是有些玄妙,我一开始对于variant的理解是有问题的,它是所谓的safe union,那么它就是在任何时候只有最多一个元素,那么在任意时候是怎么正确的选择这个visit的函数的参数类型的呢?比如 这里的auto是怎么正确的设定arg1和arg2的参数类型的呢?看源代码是有些困难的,我也只能看懂一小部分,比如不管这个lambda的参数怎么变化,它的返回值是一定的,所以,明白了这一点才理解应用invoke_result_of获得返回类型只需要使用variant的第一个get<0>的类型。其次就是对于两个var1,var2来说他们的index()函数是可以用来得到类型的,不过是metaprogramming的类型。总之,这些代码是比较不容易看懂的。

五月二十八日 等待变化等待机会

  1. 对于visit这个例子我可以说是每次只能理解一点点。上一次我能体会到它是有一种表现出来的多态的样子,但是我并不理解它是怎么做到的,现在我依然不清楚是怎么做到的。比如到底有没有虚方法表呢?我以前认为这个也许是静态的实现,就是说编译期就决定的,但是现在看来是不对的,从直觉上看如果这个variant是通过一个函数的参数传递进来的照理说这个就不大可能是静态的了,所以我稍稍的改动做了一个lambda来证明这个是某种动态行为,归根结底我还是看不懂源代码里的所谓产生虚方法表是否是真的如同我们通常面向对象里编译器帮助产生的那样的虚方法表? 这里我先把这个神奇的overload万能类声明为一个变量。 然后制作一个lambda以便把variant当作函数参数传递进去, 我和原来的例子本质上没有变化,我仅仅想看看overloaded能不能在lambda或者函数里面依然能够根据传入的variant的正确类型来挑选相应的operator 其实,真正的硬核的代码是原来例子里的这个神奇的overloaded的定义,我现在才再次意识到它是它constructor的传入的所有参数的子类,所以它是所有实参的子类,我以前错误的理解是specialization,就是模板行为是编译期挑选最接近的类型,那么从现在使用lambda来看,以及实现代码来看我以为这个是动态的就是运行期的行为。这个variant的实现代码非常的复杂,我只能说它肯定是动态选择的,当然constexpr也许可以让这个选择是编译期就决定,也就是针对里的各个类型提前选择相应的overloaded里的各个operator(),也许我可以gdb看看运行期到底有多少代码在执行?
  2. 其实最让我头疼的一点还是关于这个parameter pack的语法,在这里 这个using Ts::operator()...;应该是所谓的using declaration可是,这里它究竟在做什么呢?很明显的overloaded是所有的Ts...这些类的子类,那么根据定义它的作用是
    Using-declarations can be used to introduce namespace members into other namespaces and block scopes, or to introduce base class members into derived class definitions, or to introduce enumerators into namespaces, block, and class scopes
    把基类的成员引进到子类定义里!我一直以为using仅仅是用来声明一个类就是替代typedef的新玩意,现在才意识到在没有引进新的类型的别名的话它就是仅仅引进到名字空间或者类的空间,这个是它最主要的功能之一啊!难怪我始终不理解using和using namespace的区别!但是作为基类的成员难道子类不能从继承的渠道为什么非要使用using来引进呢?struct所有成员不都是公开的吗?当我尝试把using Ts::operator()...;注释掉后我得到了编译错误error: request for member ‘operator()’ is ambiguous看来这里并非访问空间的问题,而是explicitly使用的问题吧?这个错误很难看明白,但是最起码它的性质我有所明白了。
  3. 那么对于这个所谓的explicit deduction guide的理解是什么呢? 在半年以前我看到它我认为它就是为了那个constructor来定义,那时候我从来没有想过为什么不直接定义呢?比如在overloaded的定义里加一行代码不是更加的省事吗? overloaded(Ts...){}; 现在我也不能否认这个做法也许就是一个艺术的问题,对于一个ctor其实你当然可以定义它,但是空空如也的定义有什么意义呢?尤其是模板类不使用根本不产生代码,那么另一个图景就是你指示编译器这个ctor的deduction guide,就是把签名告诉编译器,这两条路似乎没有什么区别吧?
  4. 当然我现在能够体会如果没有这个超级铉酷的overloaded的话,要写一个处理所有variant里类型的方法的话就只能写上一系列的检查当前参数类型的if-else,当然这些是使用了constexpr的编译期的代码工作,可是代码就很不elegant了。
  5. 过了半年我就都忘光了等于要重新再复习一遍我当初绞尽脑汁的东西,比如当时我是怎么写一个自动产生自然数的向量的呢? 首先写一个模板的lambda就让我感到有些陌生,其次这个lambda的实现是另一个familiar template lambda 这里对于使用index_sequence的技巧我也忘了,这个是从模板参数转化到实参的关键一步,我总是很模糊,在最外面的一层模板参数里你有的是模板参数就是一个数字N,要把它转换成一个数列,你需要嵌套一个模板使用模板参数里的parameter pack,就是<size_t...Seq>,而作为实参使用的是一个所谓的特殊类型就是index_sequence,它是跨越型参和实参的桥梁,而真正的实参变量seq我们根本就没有用,因为在函数实现里我们完全可以使用index_sequence里的这个看似是型参的一部分的数字Seq...,想想看它的大表哥integer_sequence是怎么定义的,就是一个模板类,
    The class template std::integer_sequence represents a compile-time sequence of integers. When used as an argument to a function template, the parameter pack Ints can be deduced and used in pack expansion.
    作为模板类型,我们当然可以使用它的模板参数了,因为这个参数不是类型而是一个整型。然后是我们要帮助编译器认识到这个lambda的返回值是一个vector,否则当我们return {Seq...};的时候编译器会想当然的认为我们返回的是一个initializer_list,这个比vector更贴近编译器的想法。随后作为familiar template lambda的精髓就是立刻把make_index_sequence作为参数传给我们刚刚声明的这个lambda。我们之所以需要使用这个familiar template lambda的根本原因就在于从一个模板参数的一个整数转换为一个自然数列需要内嵌一个模板函数,只有使用make_index_sequence作为实参才能达到这个目的。 到此结束了吗?我又一次卡在了怎么调用模板lambda了,过了几个月之后我几乎忘记了作为lambda的模板参数你是针对这个所谓的lambda的唯一成员operator()的所谓成员函数模板,所以,现在就不难理解调用必须是这样子的

五月二十九日 等待变化等待机会

  1. 最近放出的历史旧账肯定是有原因的,那是1958年台湾海峡最紧张的时期美国曾经打算用核武器来制服中国,这个也许比1969年苏联对中国威胁使用核武器的风险更大?在当前台湾海峡军事压力日益增大的大背景下,美国放出这种风声不管是官方还是民间都是有用心的,无非是挑事,讹诈,给某些人打气,给某些人脸色看,或者做给某些人看,不一而足。胡锡进呼吁增加东风41也无可厚非,否则对方给你脸色看你不看这样不好。
  2. 既生喻儿何生亮?variant vs any
  3. ,这里的解释真的是很透彻,总结一下就是
  4. 标准库里常常看到类似的make_xxx的helper函数,比如make_qny或者这个make_optional,而它们的作用通常都是类似一个ctor的包装,比如我们要创建一个存储字符abc的向量 或者是5个2 使用optional或者用它的成员函数value或者使用那个比较fancy的操作符* 我把这个例子添加到了cppreference里了。
  5. 偶然看到关于string literal的解说,这个领域的确是很麻烦的,这可能是预处理一个很让人头疼的地方之一吧?比如这个不小心就会看走眼 它代表了两个字符,一个是Hexdecimal 0xA,一个是ascii B
  6. 我没有想到variant的继承类不支持visit,这个的确是比较高深,我看不懂错误出在哪里,这个是c++23的内容了。

五月三十日 等待变化等待机会

  1. 很多时候我觉得也许是我的英文能力影响了我看代码的能力,比如这个例子的英文注释写的一清二楚,我却误解了,钻进了牛角尖,也许是眼花也许了是先入为主,我居然死命去看那个(int, char)的ctor。
    
    	std::string s3{0x61, 'a'}; // initializer-list ctor is preferred to (int, char)
    

五月三十一日 等待变化等待机会

  1. 关于list initialization可能是c++11里最重大的改进之一吧?我曾经读到过一篇小博文在抱怨为什么c++有这么多种初始化方式,而且让人困扰的混乱,就比如到底是使用ctor还是所谓i的list initialization就有天差地别的不同,昨天我已经遭遇过这个混乱的痛苦了,很多时候都是不经意间的若即若离,比如string{'a','b','c','d','e'}肯定是调用string的这个constructor是确定无疑的,那么根据这里关于list_initialization的解释它应该属于这一个情况的第一种:
    Otherwise, the constructors of T are considered, in two phases:
    • All constructors that take std::initializer_list as the only argument, or as the first argument if the remaining arguments have default values, are examined, and matched by overload resolution against a single argument of type std::initializer_list
    • If the previous stage does not produce a match, all constructors of T participate in overload resolution against the set of arguments that consists of the elements of the braced-init-list, with the restriction that only non-narrowing conversions are allowed. If this stage produces an explicit constructor as the best match for a copy-list-initialization, compilation fails (note, in simple copy-initialization, explicit constructors are not considered at all).
  2. 根据以上的提法我试图来营造一个有ambiguous的情况 这里我们如果这么调用它的ctor是不会有歧义的 它的输出结果让我吃了一惊因为编译器偏好那个没有经过转换的模板ctor!
    second
    hello world
    second
    abcde
    
    05.08.2021,我过了三个月回头来看花了好久才明白是怎么回事,这里的关键是我不熟悉string的新版的ctor的一个形式就是参数是initializer_list的情况!
    因为我们是显式的调用了ctor而不是list_initialization,所以我本来预计这个会有歧义,可是居然没有! 它应该是上述情况的第二种就是进入了overload resolution阶段,首先参数'n','o','a', 'm','b','i','g','u','o','u','s'被作为string的参数成为initializaer_list<char> 而真正有歧义的是这样子的 其实很简单其实不简单因为我们显式的要求调用ctor而不是list_initialization,而参数本身{'a','m','b','i','g','u','o','u','s'}强调了它是一个initializer_list,它被convert成了一个string那么这样子首选的ctor当然是第一个,那么这样子有什么歧义呢?没有想到的是编译器认为还有第二种可能,我一开始没有想到,后来就想明白了,既然编译器能够接受MyStruct{'n','o','a', 'm','b','i','g','u','o','u','s'}那么在考虑overload conversion的时候它就也是一个选项,也就是说编译器认为copy constructor是另一个选项!换言之{'a','m','b','i','g','u','o','u','s'}已经被当作ctor创建了一个对象然后作为copy constructor的参数了!这个脑回路太长了吧! 05.08.2021但是当我声明强制禁止copy ctor的时候这个错误依然不能解决,这个难道是GCC的一个bug吗?事实上msvc根本就不认为这里有什么歧义,压根没有GCC的脑回路长,完全没有想到这里会牵扯到copy ctor。还是clang最好,因为它的错误信息是比较简单明了的,也是指出了copy ctor也是候选人,并且我试图delete它也是解决不了问题的。 这里的转换相当的复杂,比如一开始我认为这个无法解释因为{'a','m','b','i','g','u','o','u','s'}是否会顺理成章的解释为string呢?编译器否认了,因为作为一个无名的这么一个braced-init-list编译器拒绝做任何的猜测,所以,从上下文来看就做出了。
  3. 这些文档是如此的高深莫测,精微奥妙难以理解,甚至我们不知道它的出处,应该是c++的各种文档里提炼出来的吧?可是这么多种的情况判断这个已经是一个完整的算法了。这里最大的问题是怎么验证呢?即便尝试使用-fdump-tree-gimple编译选项输出的如同天书一般。 比如我硬生生要尝试看看这种无厘头是怎么被解释的 这个纯粹是臆想出的,一个类的ctor的参数是同类组成的initializer_list并且把它存在一个向量里,那么没有default ctor的情况下怎么初始化个体呢?这种情况下肯定只能使用braced-init-list来做list-initialization吧? 这个初始化和这个形式的ctor是等效的吗? MyStruct my({MyStruct{}, MyStruct{}, MyStruct{}});因为{}强迫它是一个initializer_list才成立? 我不是很确定,但是大差不差吧? 但是这样子却是不行的MyStruct my(MyStruct{}, MyStruct{}, MyStruct{});,因为很明显的没有相对应的ctor。initializer_list不等同于模板的parameter pack那样可以接受任意多的参数,必须要加{}

六月一日 等待变化等待机会

  1. 关于SFINAE的一个基本常识是你要给它一个出路,换言之就是一定要给编译器一个永远能够保证正确的last resort。否则,替换失败就不再是安静的走开,而是把错误浮出水面了,就达不到静静的选择一个无奈的选择。比如在这个try_add_lvalue_reference的一个实现范例里,为什么要提供一个人畜无害的永远正确的形似费话的选项呢? 原因是我们真正需要检验的这个选项有可能失败 有什么样的类型的引用类型会失败呢?换言之什么类型是不存在引用类型的类型呢?一个答案是void,这是一个热搜的题目,这里是一个比较不错的答案。总结来说就是void是一个good for nothing的类型,你不能合法的把它转换为其他任何有用的类型,当然这个是相对于c++程序员的,对于暴力的c程序员眼里一切都是浮云,因为一切都是一个地址而已,所以,一切都可以用一个void来代表,如果一个void代表不了,那么就用两个void代表:void**,也就是指针的指针。而在c++里,void也不能是一个函数指针,这里指的不是函数的返回类型,而是说在c++里几乎任何类型都支持一个称之为call operator就是anytype(),这个可以是类型的ctor,也可以是它的所谓的operator()但是void类型是一个例外这里是信口开河了,事实上编译器对于这样的语法是支持的void();它大体上相当于noop;之类的费话一样,似乎汇编里还有对应的指令吧?我不确定,总之这个是可以的。但是编译器不能接受的是声明一个变量的类型为void,即这个语法是报错的:void var;
    那么void是百无一用吗?它可以作为反面教材,帮助你判断它是否代表一个合法的类型,同理可证的推出来void&可以帮助你判断这个类型是否可以有引用类型。回到最初的try_add_lvalue_reference,它实际上是一个有声明无实现的函数,我们仅仅指明它的返回值类型,而使用函数的返回值类型是metaprogramming的一个利器,很多难以实现的类型判断都要么是检验它作为函数返回类型或者参数类型的技巧来判断的。所以这个技巧就是我们要调用我们定义的模板try_add_lvalue_reference(0)以便明确指示编译器我们要用更加speicialization的形式,也就是参数类型明确是int,只有当它返回类型是非法的void&时候,编译器无声的放弃转而使用那个永远正确的万能形式。所以,明白了这一点才能看懂它的使用 T不是void类型的时候我们的参数0强调我们优先使用了添加引用类型作为函数返回值,注意decltype对于一个函数来说永远指的是它的返回值。所以,我们得到了T的引用类型,可是当T是void的时候,我们只能得到它本身,所以,通过判断add_lvalue_reference的类型是否是void就可以了。这里有一个细节就是add_lvalue_reference使用了继承的做法而try_add_lvalue_reference指示的返回值类型是type_identity,我一开始脑子蒙了没有注意到这一点一直在怀疑为什么没有重定义type,因为这个是在type_identity里定义了!

    这里之所以记录下来因为例子给出的实现远远比std的实现来的简洁,我不知道是否是完全正确,抑或是版本兼容等复杂问题,总之这个样本实现比实际代码要清晰简洁的多。

  2. 对于所谓的user defined literal实际上是一个很新奇的东西,我几乎没有用过,第一次尝试就闹笑话了,我理所当然的认为最普通的整数是可以的,其实不然,即便是整型你要假定这个参数是可以适合所有类型的整型的,所以,就只有unsigned long long int是唯一的选择了,我是看了这个表才领悟的,这么做肯定是效率的考量了,省的那么多种函数重载。 这个网页的几个例子关于ud的名字是否可以大小写似乎不太准确,我做了一点修改希望不要贻笑大方误人子弟。
  3. 我一直有一个错误的概念认为metaprogramming是一个运行期免费的效率倍增器,我错误的认为所有的计算都是在编译期完成了,只要冠以constexpr的名义的话,似乎天上掉下了大馅饼,直到使用gdb跟踪了才意识到宾非如此,比如这个在chrono定义的系统定义的literal 对于这个__check_overflow我抱以很大希望认为是编译期检查的,有些简单的overflow是可以靠类型判断的,不是吗?实际上不完全是。看代码到这里我才意识到我的误解,对于chrono有两重的意思,如果是简单的代表时间的一天12/24小时的那个类似enum的当然可以简单的使用enum类型转换是否成功来检验,我几乎可以确定如果声明为constexpr是完全可以在编译期做类似一个meta utility function比如is_within_hourly_range之类的吧,应该不难。可是chrono另一重的含义是时长,也就是duration这个是几乎所有整数都可以代表的吧?(我模糊印象中因为最小的纳秒nano second是用整型表示所以最大的可能的分钟小时天等等就有一个天然的可能的上限,所以,这个检查就不太可能做到那么容易了,我不确定是否可以做到编译期的类型转换就能行? 我再浏览了一下代码我的感觉这个边际检查的确是编译期实现的,而且不是我所天真的以为的所谓的enum类型转换的思路,是实实在在的整数的范围的检查,相当的复杂,但是我至少可以放心的是大部分的计算的确是编译期完成的,不过运行期并非完全没有操作,因为总要返回一个结果吧,只不过计算都是编译期做的。这里代码看不懂的是overflow的检查是否仅仅是通常的整数类型的检查还是有针对小时分钟天的范围检查呢?似乎没有吧。这个太繁琐了还是留给程序员自己做吧?

六月二日 等待变化等待机会

  1. 对于to_chars这个函数我感到很陌生,记忆力几乎都完全丧失了,肯定是看过的,但是为什么几乎没有印象呢?为什么要有这个替代sprintf的东西呢?应该是完全性吧,buffer overflow的问题是严重嗯的,它是否能够阻止呢?不过这一点我似乎不是很确定是否安全性的问题是sscanf才存在呢?对于输出似乎没有什么危险吧?总之它的古怪在于使用起来很别扭,比如这个例子你要检查返回值需要使用所谓的structured capture,并且检查所谓的error condition,这又是一个我感到陌生的东西,不抛出异常也许是一个优点吧?我对于noexcept的使用不太理解,是否这个函数本身不需要声明还是我自己包装的函数来声明我noexcept呢?现在关心起这个是因为昨天看了-fdump-tree-gimple里到处都是try-catch的之类的。总之,判断无错误的方式是ec == std::errc()
  2. 我几乎没有用过to_string听说它是新的推荐的方式,以前都是直接使用stringstream,似乎效率有些不高因为至少写代码就很烦每次要再返回一个string,可是现在尝试使用to_string也让人很意外,比如浮点数后面无意义的一些0打印的时候到底是选择输出还是不输出呢?比如这里提到的输出形式和ostream不同就让人对于它退避三舍。我觉得还是使用古老的ostream方式吧,因为to_chars也是一个让人感到鸡肋的事情,据说是效率很高,可是大多数情况下值的吗?
  3. 我快要被to_chars气晕了,我因为随便尝试一个浮点数的转换结果反反复复都在说函数ambiguous,我一直没有敢想这是因为标准库没有实现浮点数的原因,因为这个简直难以想象,而且cppreference也信誓旦旦的列出了那些函数,难道是只有vc++才实现的东西?我唯一能猜想的这个因为是从微软推过来的,linux阵营的人对此不热心结果成了烂尾工程?总之我瞎折腾居然没有想到是这个原因!
  4. 我找到了这篇博客看来是比较完整的总结了一下 加上这个to_chars做一个了断吧!作者说
    • non-throwing
    • non-allocating
    • no locale support
    • memory safety
    • error reporting gives additional information about the conversion outcome
    • bound checked
    • explicit round-trip guarantees - you can use to_chars and from_chars to convert the number back and forth, and it will give you the exact binary representations. This is not guaranteed by other routines like printf/sscanf/itoa, etc.
    做后一个to_charsfrom_chars能够做到互为逆函数也许是实现的难点吧?我怀疑这个是gcc没有实现的一个原因,应该不是我想像的实现者的偏见的因素,我这个人太主观了。
  5. 这个网站这是一位前辈的个人博客网站,他有一个新的网址https://www.cppstories.com看样子有很多的不错的博文,回头我要多看看。这一篇题目似乎不错,我现在有点累了,记下来吧。

六月三日 等待变化等待机会

  1. 我常常感叹学了差不多二十年的c++依然每天都发现自己仿佛刚刚开始学习,因为今天我又一次被打脸了,我真的不明白for-loop的语法,因为新版的for-range的语法我还真的是第一次看到过。
    for ( init-statementoptopt for-range-declaration : for-range-initializer ) statement
    我以前有时候会怀念旧版的counter,总在想两个如果能够结合就好了,因为总要额外在loop之外声明一个循环变量,现在才明白这个是可以在init-statement里声明的。看来不知道语法只是照猫画虎的模仿是不够的。 作为对比这个是我们从小就熟知的古老的for-loop
    for ( init-statement conditionopt ; expressionopt ) statement
    其实我还是没有真正的理解这个语法,究竟 for-range-declaration : for-range-initializer代表什么呢? 首先for-range-declaration还没有什么特别的,因为就是一个变量的声明,基本上你都可以想像的出来的,当然实际上也可以很复杂,比如究竟要怎么声明所谓的declarator,但是最起码的今天我意识到 for-range-initializer可以是一个braced-init-list比如我从来没有想到过循环可以这么写 还有多少其他的可能呢?这个例子实际上告诉了你一部分吧?
  2. 对于这个c++11的最基本的for-loop我感到很惭愧因为自从c++98之后我一直认为c++已经停止发展了,直到c++17之后我才恍然大悟似的发现新大陆,但是这个不能成为理由,总之我确实感到陌生,比如最基本的如何使用range-based-for-loop来打印一个普通的array呢?这个是最最基本的问题,已经反反复复的出现,首先是c/c++都不主张把数组当作值来传递,传统的c程序员把一切都只是当作指针也没有这种烦恼,但是如果要保留数组的长度只有当作引用,可是参数里固定的体现数组的长度作为类型的话基本没有人愿意再使用了,所以,古老的方法是模板参数,当我使用的时候我又担心我必须要繁琐的传递模板参数因为如果是这样子就没有意义了,所幸的是编译器能够正确推导类型。 如果是从一个如花岗岩一样僵化的c++98语法的脑袋来看的话很多都是幸福的,因为你有各种各样的新的玩意,比如braced-init-list可以自动替你初始化数组,这个在古老的c++98里就是不可能的,至于说模板能否自动推理类型我也不是很肯定。总之,现在的人生活的如此幸福却完全感觉不到。
  3. 我是在阅读这篇精彩的关于dynamic polymorphism博文时候发现我的无知的。 首先,它对于我关于variant/visit的理解再次的加深,之前我已经能够感受到这是一种新兴的类似v-table的方式,但是结合实际的例子的解说能够更加深刻的理解。这个是我摘录的优缺点的比较:

    Advantages of std::variant polymoprhism

    • Value semantics, no dynamic allocation
    • Easy to add a new “method”, you have to implement a new callable structure. No need to change the implementation of classes
    • There’s no need for a base class, classes can be unrelated
    • Duck typing: while virtual functions need to have the same signatures, it’s not the case when you call functions from the visitor. They might have a different number of argument, return types, etc. So that gives extra flexibility.

    Disadvantages of std::variant polymorphism

    • You need to know all the types upfront, at compile time. This forbids designs such as plugin system. It’s also hard to add new types, as that means changing the type of the variant and all the visitors.
    • Might waste memory, as std::variant has the size which is the max size of the supported types. So if one type is 10 bytes, and another is 100 bytes, then each variant is at least 100 bytes. So potentially you lose 90 bytes.
    • Duck typing: it’s an advantage and also disadvantage, depending on the rules you need to enforce the functions and types.
    • Each operation requires to write a separate visitor. Organising them might sometimes be an issue.
    • Passing parameters is not as easy as with regular functions as std::visit doesn’t have any interface for it.
    这其中的duck type我还是一知半解,它指的是对象不要求有一致的接口方法和继承关系吗?我却一直是往模板的类型隐藏方面去想。也许不是一个概念吧?
  4. 这篇博文推荐了一个视频是关于具体运用variant/visit的。还是很多细节需要实践。
  5. 我在想能不能用代号来设定状态机呢?比如我其实设定一个模型压根不需要实现具体逻辑,如果要描述一个状态机可以用这个来简单表示 这里我们仅仅把函数的返回值做一个提示而已,这个是证明这么做是合法的 这里可以再特殊化一下少打几个字母

六月四日 等待变化等待机会

  1. 每天都像一个九岁小孩子一样的感觉,首先昨天对于大侠的博文一点也看不懂,他的视频在cppcon我看过一些印象很深刻,让人觉得无限敬仰,而大侠的博客是这两天才看到的,可惜几乎一点都看不懂,这个实在是让人太沮丧了。
  2. 然后早晨起来看到这个ADL让我感到脊背发凉,我真的有系统性的学习过c++吗?这个例子让人胆战心惊,因为参数推导所用的函数所在的名字空间这种机制我还是第一次听说,虽然看起来顺理成章,但是其中的奥秘不少,在那个例子里我也再一次的加深印象就是平常天天使用的endl是一个函数而不是一个所谓的enum之类的,因为每天我都遇到的简单如boolalpha是一个类似的函数而我常常却从形态上模糊的以为它和std::ios_base::boolalpha这些flag使用相似,以至于我看到这样使用endl会感到惊讶 endl(std::cout);,而这样子使用的一个好处是你不必指明std::endl的名字空间而编译器会根据参数cout的名字空间std自动去寻找相应的函数。而反过来却不行,因为这个会有编译错误,因为endl是一个函数的参数,编译器并不会为了一个参数而大动干戈去寻找函数所定义的namespace里是否有这么一个变量名,这显然是不经济的,因为一般来说程序里的变量远远多于函数,解决函数名字的机会远远小于解决变量的机会。那么这个函数的原型是什么呢?我应该能够猜到因为参数是一个函数指针 所以,逻辑上是清晰的,编译器首先要解决的是函数,在找到函数的前提下是不会费力的去解决参数的,为了解决函数,编译器会根据参数所在的namespace费力的寻找一下,如果在global namespace找不到的情况下。 作为对照重复理解注释里的解释,std::cout作为参数在编译器在global namespace解决operator <<失败后会尝试根据这个参数的名字空间std来解决。这里的细节我看的头晕眼花,显然超出了我的能力了。 而这里的notes里提到的unqualified function lookup是有些帮助的,其中有些是只有c++20才有实现的,这里的复杂程度就高了,虽然理解上不是很困难,对于namespace相关的编译错误会有帮助,个人感觉namespace有关的编译错误是最头疼的因为几乎没有什么提示。
  3. 这里的解释让我进一步理解什么是qualified lookup
    Name lookup is the procedure by which a name, when encountered in a program, is associated with the declaration that introduced it.

    For example, to compile std::cout << std::endl;, the compiler performs:

    • unqualified name lookup for the name std, which finds the declaration of namespace std in the header <iostream>
    • qualified name lookup for the name cout, which finds a variable declaration in the namespace std
    • qualified name lookup for the name endl, which finds a function template declaration in the namespace std
    • both argument-dependent lookup for the name operator << which finds multiple function template declarations in the namespace std and qualified name lookup for the name std::ostream::operator<< which finds multiple member function declarations in class std::ostream
    看来函数的搜索是从参数开始的吗?我以为这可能是因为是operator<<造成的这个顺序吧?接下去的说明是
    For function and function template names, name lookup can associate multiple declarations with the same name, and may obtain additional declarations from argument-dependent lookup. Template argument deduction may also apply, and the set of declarations is passed to overload resolution, which selects the declaration that will be used. Member access rules, if applicable, are considered only after name lookup and overload resolution.
    所以,对于函数的解决是相当的复杂的运用了这么多的技术,能写出这样的技术档案的大牛很可能是实现者吧? 对于函数名以外的搜索似乎简单一些吧?
    For all other names (variables, namespaces, classes, etc), name lookup must produce a single declaration in order for the program to compile. Lookup for a id in a scope finds all declarations of that name, with one exception, known as the "struct hack" or "type/non-type hiding": Within the same scope, some occurrences of a id may refer to a declaration of a class/struct/union/enum that is not a typedef, while all other occurrences of the same name either all refer to the same variable, non-static data member (since C++14), or enumerator, or they all refer to possibly overloaded function or function template names. In this case, there is no error, but the type name is hidden from lookup (the code must use elaborated type specifier to access it).
    看到这里我才明确什么是qualified name lookup
    If the name appears immediately to the right of the scope resolution operator :: or possibly after :: followed by the disambiguating keyword template.
    所以,所谓的qualified identifier包含了
    • class member (including static and non-static functions, types, templates, etc)
    • namespace member (including another namespace)
    • enumerator
  4. 看到这些unicode character居然可以被包含在id里,我感到震惊,是我理解错误吗?难道这个就是传说中的某些中国所谓的自主创新的倡导的中文编码吗? 你觉得这个代码编译不行吗?这么做的好处是代码完全中文化可以保证以中文为母语的程序员的工作岗位,这个制度一旦在一个公司确立可以很大程度上确保自有知识产权的可靠性,因为这么大量的代码如果几乎都是以中文来命名变量与函数的话,非中文程序员读代码将是一件非常困难的事情,这无形中就是一个知识产权的额外的保护
    Code points Description Characters
    U+00A8 DIARESIS ¨
    U+00AA FEMININE ORDINAL INDICATOR ª
    U+00AD SOFT HYPHEN ­
    U+00AF MACRON ¯
    U+00B2 - U+00B5 SUPERSCRIPT TWO - MICRO SIGN ²³´µ
    U+00B7 - U+00BA MIDDLE DOT - MASCULINE ORDINAL INDICATOR ·¸¹º
    U+00BC - U+00BE VULGAR FRACTION ONE QUARTER - VULGAR FRACTION THREE QUARTERS ¼½¾
    U+00C0 - U+00D6 LATIN CAPITAL LETTER A WITH GRAVE - LATIN CAPITAL LETTER O WITH DIAERESIS ÀÁÂ...ÔÕÖ
    U+00D8 - U+00F6 LATIN CAPITAL LETTER O WITH STROKE - LATIN SMALL LETTER O WITH DIAERESIS ØÙÚ...ôõö
    U+00F8 - U+167F LATIN SMALL LETTER O WITH STROKE - CANADIAN SYLLABICS BLACKFOOT W øùú...ᙽᙾᙿ
    U+1681 - U+180D OGHAM LETTER BEITH - MONGOLIAN FREE VARIATION SELECTOR THREE ᚁᚂᚃ...᠋᠌᠍
    U+180F - U+1FFF SYRIAC LETTER BETH - GREEK DASIA ᠏ܒܓ...´῾🿾
    U+200B - U+200D ZERO WIDTH SPACE - ZERO WIDTH JOINER ​‌‍
    U+202A - U+202E LEFT-TO-RIGHT EMBEDDING - RIGHT-TO-LEFT OVERRIDE
    U+203F - U+2040 UNDERTIE - CHARACTER TIE ‿⁀
    U+2054 INVERTED UNDERTIE
    U+2060 - U+218F WORD JOINER - TURNED DIGIT THREE ...↉↊↋
    U+2460 - U+24FF CIRCLED DIGIT ONE - NEGATIVE CIRCLED DIGIT ZERO ①②③...⓽⓾⓿
    U+2776 - U+2793 DINGBAT NEGATIVE CIRCLED DIGIT ONE - DINGBAT NEGATIVE CIRCLED SANS-SERIF NUMBER TEN ❶❷❸...➑➒➓
    U+2C00 - U+2DFF GLAGOLITIC CAPITAL LETTER AZU - COMBINING CYRILLIC LETTER IOTIFIED BIG YUS ⰀⰁⰂ...
    U+2E80 - U+2FFF CJK RADICAL REPEAT - IDEOGRAPHIC DESCRIPTION CHARACTER OVERLAID ⺀⺁⺂...⿹⿺⿻
    U+3004 - U+3007 JAPANESE INDUSTRIAL STANDARD SYMBOL - IDEOGRAPHIC NUMBER ZERO 〄々〆〇
    U+3021 - U+302F HANGZHOU NUMERAL ONE - HANGUL DOUBLE DOT TONE MARK 〡〢〣...
    U+3031 - U+D7FF VERTICAL KANA REPEAT MARK - HANGUL JONGSEONG PHIEUPH-THIEUTH ...
    U+F900 - U+FD3D CJK COMPATIBILITY IDEOGRAPH-F900 - ARABIC LIGATURE ALEF WITH FATHATAN ISOLATED FORM 豈更車...ﴻﴼﴽ
    U+FD40 - U+FDCF ARABIC LIGATURE TEH WITH JEEM WITH MEEM INITIAL FORM -
    ARABIC LIGATURE NOON WITH JEEM WITH YEH FINAL FORM
    U+FDF0 - U+FE44 ARABIC LIGATURE SALLA USED AS KORANIC STOP SIGN ISOLATED FORM -
    PRESENTATION FORM FOR VERTICAL RIGHT WHITE CORNER BRACKET
    ...﹂﹃﹄
    U+FE47 - U+FFFD PRESENTATION FORM FOR VERTICAL LEFT SQUARE BRACKET - REPLACEMENT CHARACTER ﹇﹈﹉...�
    U+10000 - U+1FFFD LINEAR B SYLLABLE B008 A - CHEESE WEDGE (U+1F9C0)
    U+20000 - U+2FFFD <CJK Ideograph Extension B, First> - CJK COMPATIBILITY IDEOGRAPH-2FA1D (U+2FA1D)
    U+30000 - U+3FFFD
    U+40000 - U+4FFFD
    U+50000 - U+5FFFD
    U+60000 - U+6FFFD
    U+70000 - U+7FFFD
    U+80000 - U+8FFFD
    U+90000 - U+9FFFD
    U+A0000 - U+AFFFD
    U+B0000 - U+BFFFD
    U+C0000 - U+CFFFD
    U+D0000 - U+DFFFD
    U+E0000 - U+EFFFD LANGUAGE TAG (U+E0001) - VARIATION SELECTOR-256 (U+E01EF)
  5. 这位大牛的一个明显的爱好是各种古老的文字游戏,这个我也是不知怎么回答才好,英语的修饰词和c++的修饰词对于语法语义的理解是可以类比的吧?

六月五日 等待变化等待机会

  1. 我本来以为美国公布59家涉军企业是延续了所谓的瞎胡闹的王八拳战术,也就是漫无目的把非军工企业活生生的逼上梁山转型为军工企业,现在看起来还是有一定针对性的,具体看来就是阻止真正的军民融合战略造成军是军,民是民的条块分割状况,这个战术有一点点像是当年日军围困八路军的所谓囚笼战术,那么打破这个囚笼是否需要发动一场百团大战的战役呢?
  2. 美国股票市场上有一些所谓的理性投资,价值投资的老古董,其实都是打着稳健投资的金字招牌吃老本,他们僵化陈腐靠着祖上的余荫冠冕堂皇的收取大机构的大笔资金,因循守旧的占据着先到先得的有利地位,平常不出彩,只有在金融危机爆发的时候才熠熠生辉,重复着那句老话:你看看,我早就说过。。。但是现如今美国金融当局把这个冒险惩罚机制打破了,人为的阻止了冒险失败的代价导致后进少壮派更加的激进与有恃无恐,而且无限的量化宽松导致无限的弹药,就好像一个赌场的赌注上不封顶一样,开过赌场的老板们一定知道这个是要出事的,因为十赌九输不是输在赌场老板作弊而是输在赌场赌注有上限,阻止了赌徒的孤注一掷,而且也阻止了赌徒输急了眼的无限打白条,事实上赌场里只要不清帐赌徒的无限打白条是终究能翻本的。美联储坏了规矩总有一天要自己遭殃的,只是时间早晚而已。说不定就在明天,谁知道呢?反正到时候我会说:你看看,我早就说过。。。
  3. 网上对于胡锡进呼吁增加核弹头数量的看法都是很肤浅的,或者说只知其一不知其二,防卫自身安全固然是他主张的理由之一,但是更加深层次的问题是只有这样才能让美国接受中国的崛起,也只有这样才能让美国坐回到谈判桌前认认真真的和中国探讨世界新秩序的安排。当前世界的秩序归根结底是二战结果的体现,那么在美国军方眼里从来就不承认中国在二战中的作用,即便是俄罗斯在前苏联解体后也不再被当作是起源于战胜国的超级大国地位了,那么要结束中美之间无意义的摩擦的根本解决方案就是确立中国超级大国地位的军事实力,这个才是增加核弹头数目的用意,这个也是美国一批前军官和国会议员呼吁拜登和中俄展开军控谈判的原因,因为这个世界上其实只有军人和负责任的政治家才真正明白和平的价值。

六月六日 等待变化等待机会

  1. 这篇娓娓道来的故事《C++ Lambda Story》

    From [expr.prim.lambda#2]:

    The evaluation of a lambda-expression results in a prvalue temporary . This temporary is called the closure object.

    And from [expr.prim.lambda#3]:

    The type of the lambda-expression (which is also the type of the closure object) is a unique, unnamed non-union class type — called the closure type.

    这个是我所不知道的,居然要防止你复用吗?目的是什么?难道是只能当作rvalue来使用?

    What's more [expr.prim.lambda]:

    The closure type associated with a lambda-expression has a deleted ([dcl.fct.def.delete]) default constructor and a deleted copy assignment operator.

    And an important quote:

    The C++ closures do not extend the lifetimes of the captured references.

  2. 这一点是很有教育意义的,我并不知道原来capture是只能automatic storage的!其次就是global根本不需要capture因为它们是global本来在任何代码中都能使用,所以,根本不需要capture。这就是其中的逻辑。
  3. lambda里的mutable要怎么用呢?如果capture是by value[=]的话必须使用mutable如果有修改变量的操作的话。 这里我觉得似乎没有什么意义,虽然mutable允许了local++;但因为捕捉是by value的,所以,local的值没有改变。 接下去我觉得这个似乎是一个bug。如果我直接使用lambda而不是声明一个变量的话,我是可以这样子的使用的 但是如果我有改变变量的操作的话我必须加上mutable,可是问题是加在哪里呢?结果是当我省略了代表参数的()不论我把mutable写在哪里都是错: 现在我知道这个形式的正式名称(IIFE - Immediately Invoked Function Expression) [=]mutable{local++;}mutable()mutable; 换言之,这个mutable关键字必须在代表参数的()之后才符合语法,但是对于根本不需要参数的省略()的lambda的形式你是无法添加这个关键字的。所以,只能是这样子
  4. 而这一点也很重要对于local static variable其实和global是等价的。首先,不能explicit capture因为它们不是automatic storage;其次,不管怎样lambda都可以自由的修改使用它们并不需要mutable的关键字,同时[=]capture by value不适用它们。
  5. 如果要在类内部定义lambda的话是要隐含的捕捉this指针的,所以,以下编译是会出错的,如果没有[=]或者[&]捕捉的话,但是前者会有编译器警告warning: implicit capture of ‘this’ via ‘[=]’ is deprecated in C++20 [-Wdeprecated],后者没有,但是不管如何都避免不了内存的错误。作者给出了c++14的解决方案,就是使用initializer: [s=s]
  6. 关于moveable-only的capture,作者似乎欲言又止,让我困惑了好久。既然这个是非法的 那么难道没有解决方案吗?我想了一下才找到,我怀疑这个是新的feature,否则作者不会想不到吧?的确是草率了,作者当然给出了答案,我还没有看完文章就发评论太肤浅了!不过有一点我说对了这个是c++14的new feature,这个叫做Captures With an Initializer 首先,uniqute_ptr你只能move,那么你不妨声明临时变量在capture里做一个move的动作。
  7. 作者的小品文(IIFE for Complex Initialization)真不少,先记下来以后再看吧。
  8. 这里其实挺重要的,我只是还没有完全体会到。

    The closure type for a lambda-expression with no lambda-capture has a public non-virtual non-explicit const conversion function to pointer to function having the same parameter and return types as the closure type’s function call operator. The value returned by this conversion function shall be the address of a function that, when invoked, has the same effect as invoking the closure type’s function call operator.

    这个例子是相当的有深度 首先我们必须要定义baz的conversion operator能够允许转化为函数指针类型,其次才是定义了call operator能够调用,而这里返回的是类的成员函数指针确实是让人有些担忧,所以,才需要把call定义为static的,否则我以为你是没办法不使用bind的,因为this指针必须要bind才行啊。我觉得这里的技巧就是你不是简单的传递函数指针f_ptr类型,而是实际上使用了conversion operator才能顺利的把结构baz的实例作为参数传递了,这个是比使用繁琐的bind-this来的高明的多的做法!
  9. 这个是效率的提高!很显然的第二个更好!
  10. 这里是很典型的capture member variable,你甚至完全不需要重新命名你的initializer,这个真好。
  11. 这个细节是我没有注意到的!对于一个map的类型的不精确导致了额外的一个copy!
  12. 这篇博文我几乎是一口气看完的,现在回过头来看这个古董年代的问题,才叫人无语:

    Basically, in C++9803 you couldn't instantiate a template with a local type.

  13. 我以前看过这个关于string literal的问题,现在更加的印象深刻了,以后要自觉的使用它这里要再强调一点就是这个函数的核心是什么: std::string operator""s(const char *str, std::size_t len);会返回一个returns std::string{str, len},而这里的核心就是它会使用字符串的长度而不是依赖于null-terminated-string的结尾来返回字符串。
  14. 这里有一个小小的关于eclipse console输出的问题。这个例子里的std::string s2 = "abc\0\0def"s;如果要显示在console上必须把Windows/Preference/Run/Debug/Console/Interpret ASCII control characters打钩才行。

六月七日 等待变化等待机会

  1. 昨天的lambda的故事是c++98一直到c++14,今天的是c++17的lambda新特点。 这里对于constexpr的总结简明扼要,因为我以前看了一眼这个标准答案就头晕了。

    To recall, in C++17 a constexpr function has the following rules:

    • it shall not be virtual;
      • its return type shall be a literal type;
      • each of its parameter types shall be a literal type;
      • its function-body shall be = delete, = default, or a compound-statement that does not contain
        • an asm-definition,
        • a goto statement,
        • an identifier label,
        • a try-block, or
        • a definition of a variable of non-literal type or of static or thread storage duration or for which no initialization is performed.
  2. 作者提出了一个值得注意的议题就是lambda对于constexpr的变量是不需要capture的,这个似乎是很顺理成章的,就好像全局变量也不需要capture一样啊。另一种解释就是说lambda只能capture automatic storage的变量。相应的在一个constexpr的lambda里也是不允许capture一个non-constexpr的变量来参与计算的即便是不参与计算也不允许capture!
  3. 其实这个是我的一个盲点或者说因为没有足够的实践导致的没有意识,就是说如果一个类里的成员函数返回的是一个lambda,而它又调用了类的成员函数的问题,这个时候如果你不经意的使用capture by value [=]会怎么样呢? 首先这个代码编译是会出错的。 为什么呢?因为我们的lambda没有捕捉到this指针所以调用print是非法的。怎么改正呢?给你6个选择:
    1. 捕捉指针:[this]
    2. 捕捉引用:[&]
    3. 捕捉值:[=]
    4. 初始化一个this变量:[self=this]{self->print();};
    5. 初始化一个类的对象:[self=*this]{self.print();};
    6. 采用c++17新feature直接捕捉*this[*this]
    那么对于这样子的调用会有什么问题呢? 以上编译都没有问题(除了第三个捕捉[=]会有警告说这个c++20会deprecated),这里凡是捕捉指针(1,2,3)都是不对的会有内存错误。第四个初始化的做法的优点是兼容性,因为第五个需要c++17编译器的支持才行。 但是假如我们偷懒一下采用这个调用方式似乎是可以避免错误。 Baz{"hello"}.foo()(); 这样子的话以上所有的做法都是成立的,为什么呢?我的理解是也许是这里的传入参数类似于rvalue-reference,就是this指针的传入是move的形式,所以,我们得到的类的this指针是那个临时变量本身的,相对应的如果我们使用auto f的话它毕竟是一个named variable,f里面保存的this指针是一个临时变量Baz{}的无效的指针了。
  4. 怎样把一件简单的事情做的复杂到让其他人看不懂其实是一件很容易的事情。比如
  5. 我翻看以前的笔记这个做对照。这里面的跃进还是很大的,比如为什么仅仅声明一个lambda的形态而不需要实例就可以实现compare,我看map的ctor最接近的就是这个形式: 因为这个是对应于 但是当我省略了ctor的参数f,编译器照样接受了,就是说编译器能够自己根据类型来产生一个实例?我以为这个可能不是那么简单吧? 这个编译也是没有问题的,但是当运行添加元素就会崩溃,这个说明了什么呢?map的代码逻辑是给你灵活的添加comp的实例是不会检查的,因为像你仅仅声明一个函数类型并把它当作型参传入模板参数是无法解决实例比较函数空指针的问题,那么为什么作为空类型的lambda的decltype就能够被编译器采用呢?我想这个绝对不是自然而然的,只有lambda的特殊处理才能行,因为通常的lambda就已经包含实现了,否则也不叫做lambda了,因为lambda几乎就是无名的防止了声明一个lambda而没有实现的空类型,因为i大家都是使用auto获得的无名类型。所以,针对lambda的特殊实现也许是很简单的,就是基于lambda类型的声明处的地址就是实例地址吧?大概就是这个思想吧? 顺便说一下,lambda是和函数指针兼容的这一点相当的给力,因为你可以fix以上这个
  6. 这里的这个题目相当的厉害:设计一个模板lambda它能够接受任何类型的vector,比如vector<int>, vector<string>等等 结果我自然的就是落入圈套是这样子的 这样子不是说不行,但是它完全失去了使用lambda的乐趣,因为你调用的时候必须要痛苦的使用lambda的模板参数,而这个是很丑陋的 因为这个lambda完全失去了自己deduct类型的能力,比如foo(vector<int>{});这个调用是不被承认的。那么怎么办呢?答案出奇的简单而我却没有想到: 你如此的大胆的说出你的需求,c++20完全理解你的要求,这不是真正的有求必应吗?
  7. 看到例子里使用的这个神奇的wandbox,从原理上来看似乎也不复杂,无非就是部署了各个版本的gcc吧?但实际上是这么简单的吗?这里是源代码,我尝试了一下。它使用的gRPC,这个是什么呢?总之在我看来它和https://godbolt.org/各有千秋吧?其实我根本就没有用过,总之,前者似乎更加简单明了,后者是给专业玩家用的。反正是节省了大家编译gcc的痛苦。
  8. 在stackoverflow上问的第一个问题
  9. 这是一个如此简单的问题,但是难道没有人被困扰吗?肯定是有人回答过了也许我也看到过,只是我的记忆太差了忘了,或者潜意识留下了痕迹就以为自己也能想出来,不过这个的确是受高人的指点得来的。设计一个lambda返回std::array<T,N>,这个lambda使用的时候避免使用模板参数,因为这个调用方式不是我们需要的: auto ary4=createArray.operator()<string, 5>(); 这个是我在众位高手指导下的结果 08.06.2021这个我今天早上总结了一下就是原本要达到一个模板函数的功能却不一定要真的需要像模板函数一样的使用而是通过参数来推导模板函数的模板参数。

六月八日 等待变化等待机会

  1. 我想比较一下这两个lambda的区别,在我看来两者似乎是等价的,不过我想确认一下。 不过我意外的发现c++17的语法不支持static_assert操作于非constexpr的参数,就是说c++17编译最后一行static_assert有错误,c++20才支持。 但是我还是意外的发现我对于代码的实质缺乏概念,首先,以上代码是不产生obj code原因是没有任何的输出或者是对外界的改变,形同虚设。其次,static_assert是没有对应obj code的因为这个意义上来说和assert类似的。后来我添加了诸如简单的循环只能证明两个lambda本身没有区别因为在编译器眼中foo(1,2,3,4)或者bar(1,2,3,4)和声明一个array并使用aggregate initialization是等效的,而且也不存在于copy的问题,因为肯定会使用RVO来优化了。所以,我的结论是两个也许有编译时间的差异,但是对于运行来说是等价的。
  2. 遇到这个问题你要怎么解决呢?作者的用意当然是在推销lambda因为这个是它绝佳的应用场景,可是针对这么一个常见的问题如果不用lambda的通常的解决办法是什么呢? 首先问题的核心是foo是经过重载的两个函数的名, 把它作为参数代入for_each作为函数指针编译器抱怨无法解决是哪一个 这个是有一些违背直觉,解决不就是在多个candidate之间挑选最合适的吗?事实上如果我传递给你的functor或者函数指针非常明确的就是只接受一个int为参数的函数指针编译器会屁颠屁颠的通过编译的,不是吗?你不妨删除那个多余的重载参数float的foo,编译肯定没有问题,很明显的没有歧义啊。但是这个肯定不是我们想要的解决办法,难道等我们的vector是float的再删掉int不成?那么我强制的类型转换告诉编译器行不行呢?所以我尝试使用 结果得到了这个错误:error: overloaded function with no contextual type information,这是什么意思呢?这里的解释相当的到位:
    • Compiler does not know, which overload of cb to chose.
    • Even if it did know, it would not convert from void* to, say void(*)(int), without any explicit conversion.
    所以,这里有两层意思,首先是虽然你强制转换了类型提醒了编译器,但是编译器懵懵懂懂的说我你让我重新翻译这个foo指针但是我从一开始就不知道它是哪一个。其次,就算我知道它是哪一个你要是不明确的强制转换我是不会帮你做的。这个实在是一个奇怪的行为。因为古老的c-style的强制转换是没有问题的: 所以,问题出在reinterpret_cast身上。看这里的说明我是一头雾水。 当然我尝试使用static_cast是成功的因为它和c-style-cast几乎是等效的吧?这里的著名的比较所有六种类型的cast的帖子绝对值得收藏,可是要真正理解却很难。

    What are the proper uses of:

    • static_cast
    • dynamic_cast
    • const_cast
    • reinterpret_cast
    • C-style cast (type)value
    • Function-style cast type(value)

    How does one decide which to use in which specific cases?

    static_cast is the first cast you should attempt to use. It does things like implicit conversions between types (such as int to float, or pointer to void*), and it can also call explicit conversion functions (or implicit ones). In many cases, explicitly stating static_cast isn't necessary, but it's important to note that the T(something) syntax is equivalent to (T)something and should be avoided (more on that later). A T(something, something_else) is safe, however, and guaranteed to call the constructor.

    static_cast can also cast through inheritance hierarchies. It is unnecessary when casting upwards (towards a base class), but when casting downwards it can be used as long as it doesn't cast through virtual inheritance. It does not do checking, however, and it is undefined behavior to static_cast down a hierarchy to a type that isn't actually the type of the object.


    const_cast can be used to remove or add const to a variable; no other C++ cast is capable of removing it (not even reinterpret_cast). It is important to note that modifying a formerly const value is only undefined if the original variable is const; if you use it to take the const off a reference to something that wasn't declared with const, it is safe. This can be useful when overloading member functions based on const, for instance. It can also be used to add const to an object, such as to call a member function overload.

    const_cast also works similarly on volatile, though that's less common.


    dynamic_cast is exclusively used for handling polymorphism. You can cast a pointer or reference to any polymorphic type to any other class type (a polymorphic type has at least one virtual function, declared or inherited). You can use it for more than just casting downwards – you can cast sideways or even up another chain. The dynamic_cast will seek out the desired object and return it if possible. If it can't, it will return nullptr in the case of a pointer, or throw std::bad_cast in the case of a reference.

    dynamic_cast has some limitations, though. It doesn't work if there are multiple objects of the same type in the inheritance hierarchy (the so-called 'dreaded diamond') and you aren't using virtual inheritance. It also can only go through public inheritance - it will always fail to travel through protected or private inheritance. This is rarely an issue, however, as such forms of inheritance are rare.


    reinterpret_cast is the most dangerous cast, and should be used very sparingly. It turns one type directly into another — such as casting the value from one pointer to another, or storing a pointer in an int, or all sorts of other nasty things. Largely, the only guarantee you get with reinterpret_cast is that normally if you cast the result back to the original type, you will get the exact same value (but not if the intermediate type is smaller than the original type). There are a number of conversions that reinterpret_cast cannot do, too. It's used primarily for particularly weird conversions and bit manipulations, like turning a raw data stream into actual data, or storing data in the low bits of a pointer to aligned data.


    C-style cast and function-style cast are casts using (type)object or type(object), respectively, and are functionally equivalent. They are defined as the first of the following which succeeds:

    • const_cast
    • static_cast (though ignoring access restrictions)
    • static_cast (see above), then const_cast
    • reinterpret_cast
    • reinterpret_cast, then const_cast

    It can therefore be used as a replacement for other casts in some instances, but can be extremely dangerous because of the ability to devolve into a reinterpret_cast, and the latter should be preferred when explicit casting is needed, unless you are sure static_cast will succeed or reinterpret_cast will fail. Even then, consider the longer, more explicit option.

    C-style casts also ignore access control when performing a static_cast, which means that they have the ability to perform an operation that no other cast can. This is mostly a kludge, though, and in my mind is just another reason to avoid C-style casts.

    为什么reinterpret_cast不成功反而static_cast可以呢?从逻辑上看照理说应该是前者更加的肆无忌惮更加接近无所不能的c-style-cast吗?我的直觉解释就是编译器希望你来告诉它而不是它告诉你吧?顺便说一下,声明一个中间变量效果等同于避免了cast,当然代价是运行期多了一个临时变量,增加了一两个汇编指令吧?
  3. 当你看到这里的这个无比复杂的宏是否和我一样的惊呆了呢? 这个万能的宏是在干什么?
    We create a generic lambda and then forward all the arguments we get. To define it correctly we need to specify noexcept and return type. That’s why we have to duplicate the calling code - to get the proper types.
    为什么需要duplicate calling code?这个是lambda的语法吗?我的感觉它属于lambda-specifiers注意到它既可以出现在参数之前也可以出现在参数之后。
    
    lambda-declarator:
    	lambda-specifiers
    	( parameter-declaration-clause ) lambda-specifiers requires-clauseopt 
    
    而这个lambda-specifiers其实是很复杂的一个non-terminal
    
     lambda-specifiers:
    	decl-specifier-seqopt noexcept-specifieropt attribute-specifier-seqopt trailing-return-typeopt 
    
    其实我最大的疑惑来源于为什么需要这个因为在我看来我指明函数的返回值就够了何必多此一举?我把这一行去掉这个lambda也是正确的,不过我以为这个一定是一个优化只不过我对于noexcept还很陌生,难道我不能在lambda的代码部分声明它吗?

六月九日 等待变化等待机会

  1. 每隔几个月我就把之前学到的忘得干干净净。比如我一直在纠结于模板lambda,可是难道generic lambda做不到模板的效果吗?也许压根就没有必要使用模板?比如我们并不知道有多少种参数,那么使用auto难道不能解决吗? 而使用tuple的apply里的lambda的定义本身不是问题,但是我忘记了apply不是一个循环过程,更像是一个fold operation。所以应该要这样子 注意到fold的operator需要使用挂号(),前面。顺便练习一下string的UD(operator ""s,否则字符串里的null会把它。
  2. 这个小品文使用lambda来做复杂变量或者常量的初始化。
  3. 关于这个我认为是有深度的例子我想改进一下,结果不成功。 我的思路是这样子的对于任意一个类的各个成员方法,我都可以把它们作为一个通用调用的函数指针来设计一个简单的转化operator,那么我可以任意去像调用方法一样的调用任何一个类的对象。比如所有的类都实现一个方法而它的函数签名是类似的比如using IntMethod=int(*)(int); 那么我定义每个类的转化方法是返回一个lambda通过捕捉*this来调用具体的成员函数 如果以上可以成立的话我就可以任意的调用任意的实现该方法的类比如 可惜这个想法不成立,因为A lambda can only be converted to a function pointer if it does not capture. 这里是说明
    The closure type for a lambda-expression with no lambda-capture has a public non-virtual non-explicit const conversion function to pointer to function having the same parameter and return types as the closure type’s function call operator. The value returned by this conversion function shall be the address of a function that, when invoked, has the same effect as invoking the closure type’s function call operator.
    所以我无法返回lambda作为函数指针,可是我又不能直接返回成员函数指针因为它的签名不一样,所以之前作者的例子是返回一个static方法指针,这就是我一直想避免的地方,看来是不太可能的,因为核心的问题就是怎么传递this指针的问题,这个无法回避。难道我可以使用band吗? 结果我花了快一个多小时才搞明白几个小问题:
    • 首先成员函数取地址必须是所谓的its operand is a qualified-id not enclosed in parentheses也就是说有类的名字空间
      A pointer to member is only formed when an explicit & is used and its operand is a qualified-id not enclosed in parentheses. [ Note: That is, the expression &(qualified-id), where the qualified-id is enclosed in parentheses, does not form an expression of type “pointer to member”. Neither does qualified-id, because there is no implicit conversion from a qualified-id for a non-static member function to the type “pointer to member function” as there is from an lvalue of function type to the type “pointer to function” ([conv.func]). Nor is &unqualified-id a pointer to member, even within the scope of the unqualified-id's class. — end note ]
    • 其次这个是一个不可能的任务就是说即便使用了bind也不可能把一个成员函数转换成一个函数指针,两者是不兼容的。这一点我是始终卡壳没有理解!就是说我始终有一个错误观念认为我通过bind可以把一个成员函数伪装成一个自由函数,这个是从使用的无差别上来看的,但是从编译器的角度来看两者的类型依然是不兼容的。这个道理其实很容易理解,因为如果不是这样子的话何必要发明std::function呢?
    • 所以明白了只有std::function是一个可以兼容普通函数指针和成员函数指针的道理我的代码改了一下子: 这里我定义了operator IntMethodfunction它不像函数指针那么原始,同时也去除了lambda不允许有capture的限制,至于使用就很简单的了 这里你可以把callIntMethod定义一个自由函数就更简单了,我是纯粹为了少打几个字而已。

六月十日 等待变化等待机会

  1. 这个小品文里对于我最有价值的是让我意识到了还有第三种方式来使用lambda就是使用std::invoke
  2. 这篇小品文是lambda的故事之一,我又一次看到了那个神奇的万能类,再次摘抄加深记忆一下: 我再次试图理解它:它定义的就是一个继承了所有输入参数的模板子类,同时把它的所有父类的operator()都引入,这个似乎是继承类默认没有明确引入父类方法的问题吧?我记不清了要去查看之前的笔记。第二行应该是编译器支持的问题?
  3. 但是我的思路又被打断了因为我又去捉摸那个有趣但是无用的东东:首先我定义一个无穷多的模板标志类作为实验对象。 然后定义一个双重的lambda用来产生一个这个标志类的tuple,这里最伤脑筋的地方是make_tuple的参数是什么?我脑子里又像短路一样希望先使用foldoperation来展开参数然后再传递给make_tuple,全然忘记了函数参数本身就能够对于参数进行展开,哪怕参数是模板参数,我以前对于这一点不肯定结果饶了一个大圈子在Struct的ctor里定义实参让它通过ctor来展开,现在看来完全不需要,根本不需要引入ctor的参数。其次当然是返回值的指示也很重要吧?现在看来使用了make_tuple就根本没这个必要了,我又不能使用aggregate initialization没有必要指示了。使用IIFE让代码写的更紧凑以便别人看不懂? 这里沿用了前两天我学到的小技巧就是对于模板lambda使用参数来避免对于模板的调用方式,因为通过参数让编译器自己推导出模板参数,典型的就是这些编译器的玩具integral_constantinteger_sequence它们的目的都是如此。这个lambda的结果是什么呢? 那么这么一个怪物要怎么使用它呢?对于tuple的apply方法我始终会和variant的visit方法产生莫名其妙的联想,其实两者唯一的联系就是它们都很神奇,除此之外完全不同! 这个神奇的无用的玩具究竟能做什么呢?
  4. 我以为这个玩具可能更加的复杂一些,就是如何用以上的标志类去创建一个包含它们的variant的一个vector呢?比如这个类型当N=3时 这个lambda会不会让人看的头晕呢? 这里值得注意的是我又一次的错误的使用了fold operation我也不是很确定也许是comma operation吧?,这个根本就是不对的,我需要的就是简单的braced-list-initialization ,相对照的这个是错误的 这个纯粹像是初始化最后一个元素的comma operator其结果就是返回一个长度为1的数组,其元素是最后一个也就是包含实际元素为Struct<2>的一个variant而已! 让我们做一下鉴定如何? 如果我们visit会有什么结果呢? 注意这里的visit的访问函数没有使用模板或者那个神奇的overload范式,原因当然是我们的处理比较的简单因为我们访问的是一致的一个共同的成员变量m_id,如果不是这样子的话那就要使用那个神奇的overload来实现动态多态了。 结果当然是这样子的0,1,2, 不过话说回来了,是否一定需要使用那个复杂的overload呢?比如我使用一个简单的lambda不行吗? 注意这里使用decay_t的必要性因为我们实在是无法预料这个参数到底是什么const还是reference等等。

六月十一日 等待变化等待机会

  1. 这里的小品文列举了使用lambda的大好处,
    1. Lambdas Make Code More Readable
    2. Lambdas Improve Locality of the Code
    3. Lambdas Allow to Store State Easily
    4. Lambdas Allow Several Overloads in the Same Place
    5. Lambdas Get Better with Each Revision of C++!
    6. Bonus: Lambdas Compile 6.6x Faster than std::bind
    这里有一个小小的关于c++20的改进Lambdas in unevaluated contexts - you can now create a map or a set and use a lambda as a predicate.这句话的理解上我出了偏差,这个到底是什么意思呢?最后通过实验我再次证明我的记忆是对的,在map或者set里predicate是一个类型而不是指的实例这一点也许有争议但是我绝的意思对就可以,就是说我们指的是set/map的类型的模板参数,而不是说ctor里传入的用于比较的functor,因为如果是后者根本就不存在什么unevaluate的意思,它是ctor的实参是一定是一个实际的运行期使用的代码,根本不搭界。所以,这个一定是指的是模板参数部分。那么如果要让一个lambda成为unevaluated,那么当然是使用decltype,这里我一直有一个模糊认识就是说我以为一个lambda如果声明为constexpr就可以看作是编译期可用,似乎就是unevaluate的,这简直是毫无道理,因为归根结底这里需要的是一个type,其他都是废话。
  2. 之前的这个关于overload函数传入for_each无法解决的问题,我却忽略了最普通的解决办法就是lambda的auto的威力,就是说这个函数指针有overload造成不能编译实际上是早期for_each之类实现的某些限制吧,当你使用lambda把它包裹一层就没有了。这里重复一下作者的例子来加深记忆。 这里的编译是没有任何问题的,所以从这里来看使用lambda远远优越于传统的使用各种复杂的adaptor加上稀奇古怪的操作符的组合,去年我在这里花了好些时间来锻炼脑筋组合各种各样的adaptor,真的不需要啊。当然啊从代码复用的角度看那些成熟的adaptor是值得推荐的,不过lessor/greater之类的如果复杂一些的组合就头疼了。
  3. 我估计这个可能是mpl的一个基本功,可是我对于partial specialization的理解总是不清楚,究竟怎么才是正确的做法我始终是一团浆糊,比如怎么实现一个最最简单的indexof的小函数呢?就是取得一个integer_sequence的指定index的值呢?这个似乎是太简单了吧?我想了好久最后还是参考源代码make_integer_sequence的具体实现的代码才做出一个很简陋的东西。 首先我希望得到的结果就是这个样子的 就是说这个getIndex应该返回3。 怎么实现的呢?我参考了utility里的一个内部函数_Build_index_tuple的做法,首先,我决定只是针对最原始的数列而不是integer_sequence来操作,因为我遇到模板参数的小困难,这里是定义 我知道对于数组越界有更漂亮的SFINAE的技巧,可惜我不知道怎么做就只能让他报错,不过效果上也没有什么差别。 然后这个递归定义就是核心,我为了这个小东西想了好久。这里算不算specialization呢? 然后这里是收获结果,这里我还在犹豫value这个返回值能否变成static的,其实这个想法是荒谬i的,一点意义也没有。 这样子使用是不行的,因为我要用的类型是integer_sequence,所以这里加了一个小lambda来包装一下,这里我再一次为了lambda避免使用模板而转实参。 我非常肯定mpl之类的应该有现成的实现,可是没有找到。
  4. 网上搜到的解放军陆军资料
    战区机关驻地集团军番号军部驻地
    东部战区机关驻福建福州陆军第七十一集团军军部驻江苏徐州
    陆军第七十二集团军军部驻浙江湖州
    陆军第七十三集团军军部驻福建厦门
    南部战区机关驻广西南宁陆军第七十四集团军军部驻广东惠州
    陆军第七十五集团军军部驻云南昆明
    西部战区机关驻甘肃兰州陆军第七十六集团军军部驻青海西宁
    陆军第七十七集团军军部驻四川成都
    北部战区机关驻山东济南陆军第七十八集团军军部驻黑龙江哈尔滨
    陆军第七十九集团军军部驻辽宁辽阳
    陆军第八十集团军军部驻山东潍坊
    中部战区机关驻河北石家庄陆军第八十一集团军军部驻河北张家口
    陆军第八十二集团军军部驻河北保定
    陆军第八十三集团军军部驻河南新乡

    2017年组建的陆军第七十一集团军下辖:

    • 重型合成第二旅
    • 重型合成第三十五旅
    • 重型合成第一六〇旅
    • 中型合成第一七八旅
    • 轻型合成第一七九旅
    • 重型合成第二三五旅
    • 陆航第七十一旅
    • 炮兵第七十一旅
    • 防空第七十一旅
    • 特战第七十一旅
    • 工化第七十一旅
    • 勤务支援第七十一旅

    2017年组建的陆军第七十二集团军下辖:

    • 两栖合成第五旅 (05两栖突击炮+05两栖步战车)
    • 重型合成第十旅 (96A坦克+04/04A 步战车)
    • 中型合成第三十四旅
    • 中型合成第八十五旅
    • 轻型合成第九十旅
    • 两栖合成第一二四旅(05两栖突击炮+05两栖步战车)
    • 特战第七十二旅
    • 炮兵第七十二旅
    • 防空第七十二旅
    • 陆航第七十二旅
    • 工化第七十二旅
    • 勤务支援第七十二旅

    2017年组建的陆军第七十三集团军下辖:

    • 轻型合成第三旅
    • 两栖合成第十四旅
    • 重型合成第八十六旅(96系列坦克 +04)
    • 两栖合成第九十一旅
    • 轻型合成第九十二旅
    • 中型合成第一四五旅
    • 特战第七十三旅
    • 陆航第七十三旅
    • 炮兵第七十三旅
    • 防空第七十三旅
    • 工化第七十三旅
    • 勤务支援第七十三旅

    2017年陆军第七十四集团军组建后,下辖:

    • 两栖合成第一旅
    • 重型合成第十六旅(96+86A)
    • 两栖合成第一二五旅
    • 轻型合成第一三二旅
    • 中型合成第一五四旅
    • 轻型合成第一六三旅
    • 特战第七十四旅
    • 陆航第七十四旅
    • 炮兵第七十四旅
    • 防空第七十四旅
    • 工程防化第七十四旅
    • 勤务支援第七十四旅

    2017年组建的陆军第七十五集团军下辖:

    • 重型合成第三十一旅
    • 山地合成第三十二旅
    • 轻型合成第三十七旅
    • 轻型合成第四十二旅
    • 中型合成第一二二旅
    • 重型合成第一二三旅
    • 空中突击第一二一旅
    • 特战第七十五旅
    • 炮兵第七十五旅
    • 防空第七十五旅
    • 工程防化第七十五旅
    • 勤务支援第七十五旅

    2017年陆军第七十六集团军组建后,下辖:

    • 重型合成第十二旅 (96A+04A)
    • 重型合成第十七旅(99+04A)
    • 轻型合成第五十六旅
    • 重型合成第六十二旅(99A+04A)
    • 中型合成第一四九旅
    • 轻型合成第一八二旅
    • 特战第七十六旅
    • 陆航第七十六旅
    • 炮兵第七十六旅
    • 防空第七十六旅
    • 工程防化第七十六旅
    • 勤务支援第七十六旅

    2017年陆军第七十六集团军组建后,下辖:

    • 重型合成第十二旅 96A+04A)
    • 重型合成第十七旅(99+04A)
    • 轻型合成第五十六旅
    • 重型合成第六十二旅(99A+04A)
    • 中型合成第一四九旅
    • 轻型合成第一八二旅
    • 特战第七十六旅
    • 陆航第七十六旅
    • 炮兵第七十六旅
    • 防空第七十六旅
    • 工程防化第七十六旅
    • 勤务支援第七十六旅

    2017年陆军第七十七集团军组建后,下辖:

    • 中型合成第三十九旅
    • 山地合成第四十旅
    • 轻型合成第五十五旅
    • 重型合成第一三九旅
    • 轻型合成第一五〇旅
    • 中型合成第一八一旅
    • 陆航第七十七旅
    • 特战第七十七旅
    • 炮兵第七十七旅
    • 防空第七十七旅
    • 工程防化第七十七旅
    • 勤务支援第七十七旅

    2017年陆军第七十八集团军组建后,下辖:

    • 重型合成第八旅
    • 轻型合成第四十八旅
    • 重型合成第六十八旅
    • 中型合成第一一五旅
    • 重型合成第二〇二旅
    • 重型合成第二〇四旅
    • 特战第七十八旅
    • 陆航第七十八旅
    • 炮兵第七十八旅
    • 防空第七十八旅
    • 工程防化第七十八旅
    • 勤务支援第七十八旅

    2017年陆军第七十九集团军组建后,下辖:

    • 中型合成第四十六旅
    • 重型合成第一一六旅
    • 轻型合成第一一九旅
    • 重型合成第一九〇旅
    • 轻型合成第一九一旅
    • 中型合成第二〇〇旅
    • 陆航第七十九旅
    • 特战第七十九旅
    • 炮兵第七十九旅
    • 防空第七十九旅
    • 工化第七十九旅
    • 勤务支援第七十九旅

    2017年组建后,陆军第八十集团军下辖:

    • 轻型合成第四十七旅
    • 重型合成第六十九旅
    • 中型合成第一一八旅
    • 轻型合成第一三八旅
    • 轻型合成第一九九旅
    • 中型合成第二〇三旅
    • 陆航第八十旅
    • 特战第八十旅
    • 炮兵第八十旅
    • 工化第八十旅
    • 防空第八十旅
    • 勤务支援第八十旅

    2017年组建的陆军第八十一集团军下辖:

    • 重型合成第七旅
    • 轻型合成第七十旅
    • 中型合成第一六二旅
    • 中型合成第一八九旅
    • 重型合成第一九四旅
    • 重型合成第一九五旅(96A+04A)
    • 特战第八十一旅
    • 炮兵第八十一旅
    • 防空第八十一旅
    • 工化第八十一旅
    • 陆航第八十一旅
    • 勤务支援第八十一旅

    2017年组建的陆军第八十二集团军下辖:

    • 重型合成第六旅
    • 轻型合成第八十旅
    • 重型合成第一一二旅
    • 中型合成第一二七旅
    • 重型合成第一五一旅
    • 重型合成第一八八旅
    • 轻型合成第一九六旅
    • 特战第八十二旅
    • 炮兵第八十二旅
    • 防空第八十二旅
    • 工兵第八十二旅(以平战结合形式承担国家地震灾害紧急救援队任务)
    • 防化第八十二旅
    • 陆航第八十二旅
    • 勤务支援第八十二旅

    2017年组建的陆军第八十三集团军下辖:

    • 重型合成第十一旅(96系列+86)
    • 中型合成第五十八旅
    • 中型合成第六十旅
    • 中型合成第一一三旅
    • 中型合成第一三一旅
    • 中型合成第一九三旅
    • 空中突击第一六一旅
    • 特战第八十三旅
    • 炮兵第八十三旅
    • 防空第八十三旅
    • 工化第八十三旅
    • 勤务支援第八十三旅

六月十二日 等待变化等待机会

  1. 之前我抱怨过为什么需要使用noexcept的复杂形态定义返回值,这篇文章是之前那篇的一个来源和基础,它说的理由是SFINAE-friendly,这个让我难以理解,这个是什么意思呢?文章的结尾提出的问题其实才更有深度,就是说对于初级甚至中级水平的c++程序员这都可能是个问题:怎么传递overloaded函数指针给所谓的高阶函数(higher-order function),作者提出的两个原则我是赞同的,我们定义的overloaded不管是functor也好还是什么要满足本身调用的时候直接传递不同的参数而不会有歧义,其次作为参数传递的时候不应该强迫添加模板参数。这个才是对于使用者的友好态度。但是要做到这两点还真的不太容易。作者使用复杂的宏是我不能接受的,但是使用static_const这种我也不是很赞同,我模模糊糊的能够理解在头文件里定义为static是各自包含的获得独立拷贝,这个是基本常识,但是这个违反了ODR是我没有想到的,实践中也许没有什么大问题,但是作者说这个是UB(undefined behaviour)这个就让我开始不安了。这个就问题比较大了。尤其作者说当实现一个头文件为library这会是问题。
  2. 我还从来没有意识到你可以这样定义functor:
    A functor is a mapping between categories. Given two categories, C and D, a functor F maps objects in C to objects in D — it’s a function on objects.
  3. 我竟然一开始没有意识到这个同步的问题是什么,真的是老糊涂了,这个就是哪怕再简单的整数加减也不止一条指令啊!对于这一类问题最简单的解决方案我以为还是使用atomic_ref代价最小最经济适用,修改的成本是最低的,而且我认为它能够直接利用有些处理器的内在的指令也许和原来的普通指令执行效率差不多吧?使用任何其他同步机制都是高射炮打蚊子式的小题大作了。
  4. 对于std::atomic_ref的一知半解害死人,这个也是因为cppreference对于这个专题缺乏例子,让我茫然因为源代码是看不到的,这个应该是平台相关所以是需要library实现的,不是头文件那么简单,所以,我就不理解它的原理了。对于普通的基本类型这个没有什么好说的,但是如果是一个用户定义的较复杂的类型呢?我可以领悟到它支持简单的所谓triviallycopyable的概念,所以,我还专门去做一个检验是否支持这里的Counter是我定义的一个简单的结构,如果我故意定义数据结构导致没有指针长度对齐,这个显然就不行了,我的意思是不一定是所谓指针长度,而是处理器最基本的load/store的单位长度对齐吧?总之,这个没有什么问题很好理解,但是我要怎么实现这个结构的一个加加减减来展示它的作用呢?我当然可以去实现这个结构Counter本身的操作符operator++(int)(注意是postfix++因为有一个操作符类型int)然后使用atomic_ref的方法compare_exchange_weak(localCounter, localCounter--);可是编译可以链接会失败抱怨load/store没有实现,我一开始还以为这个是不被接受的后来才明白这个是需要动态库-latomic链接的。但是对于怎么使用fetch_add我依然不知道怎么办,同样的compare_exchange_weak似乎和我设想的不一样,肯定是我理解错了。当然我也找到一个例子就是说绕开自定义类型这个坑,因为假如我们还是瞄准基本的类型的话,我还是可以直接对于基本类型操作,比如 但是怎么解决自定义类型呢?我要再找找。太累了歇一下吧。
  5. 找到一个例子,其实也很简单,当然使用operator++/--之类的做法我还是没有想法。
  6. 很久以前我认为我明白什么是bit field 可是一到实际就露馅了,对于这个例子它的sizeof(obj)我居然以为是4!为什么呢?我只是数int出现了几次却没有意识到编译器是不会浪费多余的内存空间的。学到了一个英文词<straddle跨界。另一点今天学到的是sizeof这个操作符是针对byte的也就是说你要是针对bit field的话比如sizeof(obj.b)是要报错的error: invalid application of ‘sizeof’ to a bit-field
  7. 可是我为什么要关心这种bit field之类的令人头疼的问题呢?这个是平台相关的正如有人指出的big/little/endian也是非常不同的,所以这里的例子使用int也是不现实的,但是难道使用char吗?那也不可能啊,原因是我对于atomic_ref实际上是不理解的,它紧紧依赖于内存分布这一点我是刚刚才意识到的比如你要怎么改变一个int的值呢?我得到的印象是这个操作是在bits基础上的,也许这个是处理器指令的要求吧,毕竟如果你用普通的load/store指令改变一个整数的值如果是atomic的话何必还需要专门的指令呢?就是因为通常的指令改变一个整数的值需要的指令不止一个吧?其实我对于这个说法现在又开始怀疑了。主要是一个c/c++的赋值语句包含了超过一条的汇编指令,但是真正的改变一个内存的操作本身还是一个指令而已。但是关键的是实现compare_exchange_strong是否可以用一条指令实现我不确定,以前操作系统课堂里教授说是有这种指令,如果是的话肯定是内存访问大小是有一个基本的限制,操作元应该就是只有基本数据类型吧?,可是我看的头疼的厉害,而且脖子肩膀腰背哪里都疼得厉害。出去散步一下吧。

六月十四日 等待变化等待机会

  1. 昨天被病痛折磨了一天只能休息。关于之前我对于atomic_ref的compare_exchange_weak的疑惑其实很简单是我的理解有偏差。核心的问题就是一个就是这个函数的两个操作数能不能指向一个?比如我们定义一个简单的结构 现在我们希望把这个全局变量 递增100次。 这样子会成功吗?现在看起来是明显的不可能,因为localCounterlocalCounter+1实际上是指向同一个对象。这个其实很好理解,函数compare_exchange_weak首先要把atomic_ref的值取到所谓的expected然后再和desired做比较,如果不同就更新,所以比较这两个操作元不允许是同一个地址。所以,Counter的成员函数要这么定义 这里是wandbox.org的运行结果,感觉还是godbolt.org做的要专业

六月十五日 等待变化等待机会

  1. 我原本是福尔摩斯系列的粉丝,(绝对不是什么美国搞笑的所谓的福尔摩斯,而是正宗的大英帝国最辉煌时代的严肃的与罪犯做斗争的那个专家)然后,最近我看了《大侦探波洛》系列,其中的海量的故事当然是对于福尔摩斯系列太少剧集的巨大补充,然而更重要的是它所描绘的维多利亚时代的大英帝国的全景图画,不论是各种当时最先进的飞机汽车录音机,还是精美的瓷器服饰各种富丽堂皇的建筑,奢侈优越的生活方式,总而言之是一幅活得立体的英国版《清明上河图》。其中有一些小小的部分和今天的美国有些可对照的部分,就是都发生在世界霸权帝国由盛而衰的繁荣顶点下滑的阶段。一般人总认为正午十二点是一天的最高温度的时点,可是很多人忘记了气温最高的时点往往发生在下午两点,因为虽然十二点以后地面能从太阳接受到的热量已经开始减少,可是由于惯性使然导致地面总热量的积累依然是在继续使得温度继续缓慢上升直至开始下降,所以,帝国的鼎盛时期往往发生并不是发生在它最顶点的时刻,而是往往在它已经开始从顶点滑落衰落的延后,正如美国的最顶点已经过去一段时间了,可是现在却是它真正的顶峰。今天的美国和当年的大英帝国相类似的地方就是都是一个不断创造奇迹的时代,当年的大英帝国不断的在它日不落的殖民地描绘各种充满传奇的铁路建设计划来吸引英国本土的资本来追逐,今天美国不断上演传奇的科技奇迹来吸引全世界的资本来追捧,但是无论多么豪华奢侈的盛宴总有落幕的那一天,只不过鼎盛总是出现在顶峰之后一点点而已。

六月十六日 等待变化等待机会

  1. 顶着病痛。这个是一个非常的tricky的问题,我之前没有非常的明白,最近看到了这个但是理解不透,因为太复杂,就是怎样解决一个操作符重载的问题,我之前已经看到了所谓的依据操作元的定义所在的名字空间来解决操作符重载或者说函数的搜索。这里就遇到了这个问题,这个编译好费了我好几个小时的尝试,当然其中有很多部分。
  2. 首先我们看看这个lambda的问题,它的参数是一系列的integer_sequence,然后我使用了fold expression的第一种形式,那么这个要求我们实现类似于这个操作符<<对于integer_sequence的重载。 很自然的我把这个重载定义在我自己的名字空间里因为我不想污染global namespace。注意这里有c++17/20里面强大的两个parameter pack,这两个Ints1Ints2同时出现在模板参数里在老版本的c++里是不可想像的! 那么我现在要调用这个lambda就出现了编译器反复抱怨我定义的操作符重载找不到的问题,直到我把它放在了和调用的lambda相同的名字空间才行,这个原则就是因为integer_sequence定义在std的名字空间里,作为编译器它解决操作符是依赖于参数的定义空间来查找的,当他找到一系列的操作符<<的重载之后就一个个来解决,失败以后是不会在去我定义的名字空间来查找的。这个是操作符重载时候的一个大问题,尽量不要对全局或者std名字空间类型做这个事情! 这个是完整的lambda的定义,我为了偷懒都是用现学现卖的模式,就是马上定义马上调用,这个大大增加了调试的难度! 这里关于parameter pack是一个再次训练的好机会,首先第一重的lambda是把一个整数模板参数转化为一个自动生成的integer_sequence,第二重是把这一个从0到N-1的integer_sequence变成N个长度为1的integer_sequence然后再把这些使用fold operation。这里需要注意的任何一次的parameter pack都不要有comma(,),因为编译器实在是太聪明了,比如我们把一个integer_seqeunce变成N个integer_sequence这个是函数的参数自动展开,我一开始担心这个模板参数不懂的这么做,真的是太出乎意料了。当然了要打印结果我们还是需要简单的重载一下 这里(os<<...<<Ints);使用的依然是fold operation的第四种形式,我觉的有的人喜欢使用comma operation,这个也的确不错(os<<Ints<<",",...);它的好处是可以插入一个分隔符","所以,这个也许是更好的选择,但是切勿不要混淆这两种截然不同的操作!

六月十八日 等待变化等待机会

  1. 纵观这两年香港台湾的乱像用矫情来形容是太温文尔雅了,能不能说未富先婊呢?也许可以创造一个英文短语叫做get bitchy before getting richy
  2. 这个可以说是我的一个呕心沥血的努力,对于我来说是非常难的一个实验,中间来回了好几次。可是我觉得这个是一个非常常用的东西,比如,给你一个集合要求你产生它们所有元素的组合的集合,当然最最抽象的是我们不针对这个任意集合而是使用自然数产生数字的组合,那么从这个数字的序列就可以获得集合。怎么实现呢?

六月十九日 等待变化等待机会

  1. 我不知道为什么总是把applyvisit两个风马牛不相及的东西联系在一起,这个也许是心理学的问题也许是别的什么的。其实也很简单的总结一下。首先apply是针对tuple的访问方法,visit则是针对variant,其次两者期待的参数完全不是一回事,apply实际上是一个把tuple里所有的参数都作为参数的一个函数,而visit则是一个针对variant里所有参数都需要专门去重载的多个函数,也就是说前者的参数个数是tuple_size,而后者的参数个数是1。而对于tuple和variant的理解最重要的是前者是一个编译期的型参组成的tuple,而后者variant更加是一个运行期的东西, 不过我说这一点的时候自己也怀疑因为variant实际上也是一个编译期就确定的安全union,不存在运行期来匹配动态多态的问题,两者在这一点是一样的都是编译期就明确的。 与之对应的是visit的函数实际上是一个多重重载的函数。这里的典型做法是设计一个overload万能结构继承自各种各样的lambda来使用,但是这个做法太繁琐了,大多数情况使用lambda的auto就能解决。
  2. 关于variant有一个小插曲就是如果你认为你的variant的类型可以重复的话你会有很多的麻烦事情,比如依赖于类型来赋值或者ctor的都会报出莫名其妙的错误,就是说让你想不到错误的原因是什么,错误只是告诉你类型转换失败,但是不会告诉你原因是你的重复的类型导致失败,如果强制使用in_place_index_t来指定的话你可以暂时不出问题,但是我想其后的使用不能使用自动的operator=会很头疼的。

六月二十日 等待变化等待机会

  1. 你想过没有是否应用一个工具比制造这个工具还要复杂呢?如果是这样子的话到底人类和猩猩哪一个进化程度更高呢?我想把昨天呕心沥血的所有子集的抽象集合应用到实际的集合中来,比如我昨天已经产生了一个万能的tuple of tuple of integer_sequence21.06.2021我看来是老糊涂了因为这个主意是之前失败的想法,实际上是tuple of sequence,那么现在我给定一个具体的tuple of objects,你来把这个集合的index来映射成实际的子集作为应用。 也就是实现了这么一个函数,t是我们昨天产生的那个抽象出来的index tuple,而模板参数targetTuple是一个具体的实例,我们要返回一个具体的实例的所有子集,实现所谓的映射。 这里的思路和昨天有些不同,首先,我不愿意再去依赖于操作符重载,因为这个对于任意模板参数Tuple是不现实的,我们不可能事先就要求每一个使用者去做某种奇奇怪怪的造作符重载,而且也没有这个必要,我们已经有了现成的抽象的下标index集合了唯一要做的就是去应用它到这个用户提供的targetTuple上就行了,不过要注意我们的这个抽象的t是一个包含了tuple的tuple。我们不仅需要模拟apply的做法使用一个index_sequence把这个tuple中的每一个tuple都取出来还要在内部的无名lambda里去应用每一个包含的index_sequence于用户提供的targetTuple上然后在make_tuple的参数里应用parameter pack expansion使得我们返回的也是tuple of tuple。我觉得这个代码的可读性是及其差的,几乎没有打算让初学者理解。
  2. 怎么产生抽象的这个t呢?我重复使用昨天的代码只不过优化了一行代码,这个优化在generator函数里我省略了一重lambda。并且我不想再打印这个庞然大物了所以也没有了重载ostream的部分。
  3. 怎么产生一个目标tuple来实验呢?我沿用了以前的这个通用结构的做法。使用模板类的好处是我可以自动产生它,所以为了显示重载了输出操作符。
  4. 怎么自动产生模板类的呢?这个实际上是很简单的,我也作了一个小测试,就是首先生成一个这个模板类的tuple,然后使用apply来打印输出,这里使用comma operator为了能够打印一个间隔符,当然应用了以上定义的ostream操作符的重载。 结果是很简单的
  5. 最后的一步是我们把以上串起来来应用,并且打印输出最后的结果 但是这个输出其实还是挺麻烦的我被纠缠了好久,就是怎么重载的问题,这个是正确的做法,但是效果不是很好因为在以上的这个apply里我使用\n来代表一个子集的结束,然后在下面这个打印每一个子集的内容的时候我不知道要怎么做才好,之所以这么啰嗦就是tuple of tuple,我们不能简单的使用一个重载来达到双重tuple的输出,我只能允许对于一个tuple的重载,这个地方我脑子不是很清楚,我不明白为什么不行,也许是可以的,只是我有些糊涂了。 这个输出结果还是比较的庞大的 这就是一个早上的辛苦,感觉真的很伤脑筋啊!
  6. 这个我看不懂为什么string literal不能作为template argument呢?

    From the c++11 standard §14.3.2.1

    Template non-type arguments

    A template-argument for a non-type, non-template template-parameter shall be one of:

    1. for a non-type template-parameter of integral or enumeration type, a converted constant expression (5.19) of the type of the template-parameter; or
    2. the name of a non-type template-parameter; or
    3. a constant expression (5.19) that designates the address of an object with static storage duration and external or internal linkage or a function with external or internal linkage, including function templates and function template-ids but excluding non-static class members, expressed (ignoring parentheses) as & id-expression, except that the & may be omitted if the name refers to a function or array and shall be omitted if the corresponding template-parameter is a reference; or
    4. a constant expression that evaluates to a null pointer value (4.10); or
    5. a constant expression that evaluates to a null member pointer value (4.11); or
    6. a pointer to member expressed as described in 5.3.1; or
    7. an address constant expression of type std::nullptr_t.
    我只看懂了这个结论。
    Note: A string literal (2.14.5) does not satisfy the requirements of any of these categories and thus is not an acceptable template-argument.
  7. 我又忘了user defined literal的参数有固定要求这一点

六月二十一日 等待变化等待机会

  1. 事实上你可以简单的描述一个充满各种各样的integer_sequence的tuple的,就是说根本不可能使用template template,因为它不是我所想像的,很简单的,必须把tuple里的类型都当作是不透明的类型,只有在具体处理每个tuple元素的时候才能使用integer_sequence模板,这个也是大多数类或者函数的做法。 者是一种酷炫的写法,因为首先这个tuple<integer_sequence<int,Ints...>...>是不可能直接使用模板来表达的,也没有意义因为你没有办法直接处理tuple的元素,你需要借助或者apply或者自己模仿它使用get<int>来展开parameter pack,还有就是使用fold expression或者comma expression,那么对于fold expression限制是很多的因为它们只接受为数有限的一些二元操作符,这就意味著你必须去重载这些操作符,而且有于fold的四种不同形式导致你很有可能会重载好几个类型。那么comma expression相对来说就灵活多了,不一定是操作符,函数也可以,但是归根结底这个有一些副作用比较的微妙。说到函数我还是不十分清楚,因为它是函数参数的parameter pack expansion非常的微妙,我还吃不透,比如使用get可以在参数中展开,这个是因为什么原理呢?应该是function argument list,那么它和comma expression展开的结果是完全两回事,前者是把所有的parameter pack作为一个函数的所有参数来展开,而后者则是把一个操作逐次作用在parameter pack的每一个元素上。这是截然不同的处理方式,而我头脑却总是不清楚。至于说apply可以想像成是一个回调函数来处理,但是前提是你的回调函数要能够支撑接受所有的parater pack展开作为参数。这个要求其实太高了,你能想像谁回去定义一个能够处理所有的tuple元素作为参数的函数呢?所以归根结底还是我对于这个貌似恐怖的parameter pack感到不知所措造成的。
    以上的这个lambda我使用了comma expression,而要它要求initialization部分包含在挂号里,所以,不得已我只好把整个定义的无名的lambda也包含在挂号里了。
  2. 你有没有过一个冲动去实现一个类似于make_index_sequence一样的make_character_sequence呢? 这里我限制模板参数不能超过26,同时只能是小写字母,我试图增加一个default参数来控制大小写,但是requires就不能使用auto来推理返回类型了。于是我只能把这个默认参数设成模板参数。这个是测试的结果 当然这里是需要重载操作符的 很明显的这里使用了comma expression。

六月二十二日 等待变化等待机会

  1. 碰巧搜索到这个经典的例子,这个本是作者的对于未来c++新feature的proposal,它是为了解决lambda里capture对付parameter pack的初始化的问题。我其实完全没有意识到存在这种问题因为我压根没有用到这个。简单来说就是lambda的传入参数初始化参数如果是parameter pack的话,也就是说如何capture paramter pack。其实这里我开始怀疑为什么我不能直接传入参数呢?反正是一个无名的lambda呀。 比如作者希望达到的解决方案是这样子的,这个是现有的编译器所不能支持的。
    这个是目前编译器所能支持的变通的办法传统的做法是把参数组成一个tuple然后再capture,然后再使用apply解开tuple,这个确实是比较的繁琐,当然这个是一个很好的技巧,在我看来tuple应该是运行期无差别的吧?
    这个是作者提议编译器增加的功能
    这个是我觉得实际程序员会做的,因为真的有必要capture吗?使用参数难道会增加成本吗?也许吧?如果说使用rvalue-reference比capture-initialization有更高的成本的话,我就完全同意作者的提议
  2. 找到了,因为我早上在幻想能不能使用fold对付两个parameter pack,看来语法上是不可能的。
    An expression of the form (e1 op1 ... op2 e2) where op1 and op2 are fold-operators is called a binary fold. In a binary fold, op1 and op2 shall be the same fold-operator, and either e1 shall contain an unexpanded pack or e2 shall contain an unexpanded pack, but not both就是说fold不可能使用在两个parameter pack上!.
    If e2 contains an unexpanded pack, the expression is called a binary left fold.
    If e1 contains an unexpanded pack, the expression is called a binary right fold.
  3. 那么是否真的就是无解了呢?我们不是刚刚学习了tuple的技巧了吗?所以对于这个模板我们是可以运用tuple来化解的。 也就是说我们先把第一个parameter pack化解为一个tuple来运用function argument lists,这里我们依然需要去重载操作符* 这样子的关于两个integer_sequence的相乘像不像是两个向量相乘呢?鉴于此我决定遵循向量相乘的规则实际上我不确定这个是事实,因为在我看来我只不过是把第一个向量的所有元素对于第二个向量做scalor,得到了个数为第一个向量的长度的一组向量,每个向量的长度和第二个向量相同,这个操作当然比真正意义上的dot/cross product要容易实现的多了。,产生的tuple的元素个数是第一个sequence的个数,也就是说 这个结果应该是 而不是 这个结果是否更正确呢?当然这个要求以上的代码改动一下 当然为了输出这个结果我需要重载输出操作符 注意这里是有一个小小的递归,因为tuple里是又一个tuple,所以,我不需要重载两次的,后果也就是我无法控制怎么打印分隔符",",我无法区分我是在打印子tuple还是tuple里的元素,结果都是,
  4. 怎么分析S3下载统计情况,我现在还看不进去,以后再说吧?
  5. 我突然心血来潮想到这个幼稚的问题:operator as template parameter?看来是不可能的。我之所以想到这个问题是因为我发现了一个新的途经来实现类似于vector/matrix操作。 注意这里的两个tuple需要长度一致,因为我们需要使用get方法配合操作符,这里虽然两个都是parameter pack,但是我们只需要一个...。这里和fold expression差别很大的。 结果是这样子的{5,7,9,11,} 当然需要重载输出操作符
  6. 关于打印tuple我终于找到了正确的做法 这样子的输出就好看多了,因为解决了在结尾不多打印那个","的问题。 {{1,2,3},{4,5,6},{7,8,9}}

六月二十三日 等待变化等待机会

  1. 我终于改进了一点点,就是实现了一个如何使用integer_sequence来模拟向量dot production的第一步。 结果就是{4,10,18}。 这里的核心就是直接使用integer_sequence无法直接使用fold expression,无奈我只能先把两个sequence都转成tuple然后利用tuple的get函数来利用function argument list传递回make_tuple,也就是说要完整实现dot production只要再实现一个tuple的所有项相加的小函数就可以了,我本来以为可以轻松再使用一个小lambda直接实现,结果遇到一些小障碍,所以,不妨单独再实现,毕竟它是一个相对独立的有用的功能。
  2. 的确这样一个简单的tuple_sum是应该单独写一个小函数的,可惜的是我想不出有什么操作符可以使用做sum的。 结果当然是32了,这样子就是一个完整的dot production了。
  3. 简单看了一下tuple,感觉我的印象是错误的,它并不是一个纯粹的编译期对象,也就是说我以前一直把它当作是mpl里的非存储的东西,现在我开始怀疑。不过我还是不确定,就是说虽然它有所谓的allocator之类的并不一定代表它们是运行期的分配,也许。。。总之,我现在感到比较的不确定。
  4. 实现cross production就要复杂了,我决定不再使用integer_sequence,因为它只是作为一个范例而已,实际应用中类型肯定不只是integer必须支持double,所以,只有tuple才可以。 这个看上去相当的复杂,其实无非就是把第一个tuple拆分对于第二个tuple乘积,返回增值的tuple,最后再统一使用tuple_cat来串起来。其实并没有那么的复杂。 这个是检验的结果{4,5,6,8,10,12,12,15,18}
    90

六月二十四日 等待变化等待机会

  1. 这个其实并不是我想要的,我想我还是先实现一个全排列的功能再说吧? 结果实在是差强人意
    {12,hello,0.67}
    {hello,0.67}
    {0.67}
    {}
  2. 要走的路很长,我决定一步一步来,第一步是做一个小函数就是把一个数列分成两部分,也就是类似于split的方法,把其中一个元素拿出来把其他的元素组成一个sequence或者是tuple,我后来觉得使用tuple是过于昂贵了,所以使用sequence。我觉得我最大的误区就是以为tuple是mpl里的sequence,不是的,这个实际上是pair的近亲,感觉它实际上是类似于fusion里的混合了型参和实参的异形数组,所以,它不是完全免费的。 为了输出打印我不得不重载sequence,这里我本来使用comma expression,本来可以直接使用index_sequence,但是因为要保证打印","不在最后一个,所以无奈之下又把sequence转成tuple。 但是如果我们认为pair是tuple的一个特例,那么我直接实现tuple的输出是否就可以代替pair呢?显然不行的,因为pair和tuple虽然都支持get方法,可是它们的模板参数形式不同,而且从类型的角度来看两者根本就是不兼容的类型,所以,不得已我必须实现pair 这个是测试的结果
    {0,{1,2,3,4}}
    {1,{0,2,3,4}}
    {2,{0,1,3,4}}
    {3,{0,1,2,4}}
    {4,{0,1,2,3}}
    
  3. 你可以从index_sequence转换成一个tuple,可是你能逆转它吗?显然似乎是不行的。以下这个是编译不成功的 编译器抱怨说
  4. 真正的编译期编程必须要使用类型而不是实参,因此经典的实现是计算类型而不是计算实例!我再一次的练习了一下这个经典的例子,我需要分割一个index_sequence,把其中的后半部分按照给定的长度去头的sequence返回。 这个声明其实很重要,一方面它提供了一个使用requires关键字的机会,另一方面它是具体specialization的前置声明。具体的实现我们通常都是使用递归继承,我觉得这个做法的原理就在于我们要产生新的类型,而定义新的类型的方式就是继承。 那么递归的结束条件是什么呢?只能是一个特殊的类型,而这个类型的标志是它的模板参数为0 注意我们这里得到的是一个类型,所以使用实例的时候要注意 这个结果就是一些数列
    {1,6,4,5,3,6}
    {6,4,5,3,6}
    {4,5,3,6}
    {5,3,6}
    {3,6}
    {6}
    
  5. 我想当然的想要做一个类似的sequence_split_head就是得到前面部分的sequence,可是语法上似乎不可能。 编译器抱怨说...必须是在参数的结尾才行

六月二十五日 等待变化等待机会

  1. 想了差不多一天感觉使用结构类型推理似乎是无解,于是只能寻求使用递归函数,我不确定这个算不算是编译期进行的计算,也许我可以加上constexpr,总之这个算是可以吧? 这个是函数的调用原型,给定一个下标返回包含这个下标的元素的数列的前头部分。我把requires限制写在了实现函数sequence_split_head_impl里。 这个是实际的递归实现函数,总的来说使用函数比使用结构的难读是天壤之别。 这个是测试 结果如下
    {1}
    {1,6}
    {1,6,4}
    {1,6,4,5}
    {1,6,4,5,3}
    {1,6,4,5,3,6}
    

六月二十六日 等待变化等待机会

  1. 我耗费了快两天了但是permutation始终就是不成功,很多次仿佛就在眼前但是就是够不着,有几条路似乎就差一步结果还是失败,也许是什么地方不对,也许是根本不可能。总之,我退而求其次打算先产生最通用的permutation的index_sequence,按照next_permutation的原理来产生这个算法太复杂了,单单实现一个index_sequence的搜索上升下降就有多么的麻烦。我搜到了一个monster,看来有很多大牛做好了这些工具,先看看再说吧?
  2. 在看大师们的方案之前我还想再尝试一下,一点感触就是metaprogramming可以立刻发现你代码的边边角角的问题,这个在普通程序里的边缘条件的错误需要运行期使用边界条件测试才能发现的现象在这里不存在因为编译期就相当于你的运行期,而且很多的模板是自动展开把你想不到的边界条件都编译了一遍,这个也许是编译风格决定的吧?总之这里几乎眼里不揉沙子丝毫也马虎不得,没有什么99.9...%的正确,只能是100%或者0%。这个真的是脑体操啊,总之,头疼啊。
  3. 还有一个有趣的现象在平常我们可以自由使用的conditional operator变得有时候不能使用,原因是我们通常的操作元必须是同类型或者兼容的,可是在auto和元编程的世界里这个不一定成立,所以,你不得不使用大量的if constexpr让代码看上去很繁琐。
  4. 花了整整一天在修改最后终于实现了一个全排列,但是这个实在是太夸张了吧,为什么这么复杂!而且非常的不实用因为模板是要全部产生的,一旦数列长度超过5就超过900模板栈的深度。这也就是为什么大家都是采用next_permutation的这个方法,我打算明天再参考前辈们的实现。我这个简直就是二战德国元首拍脑门想出来的超级鼠式坦克歼击车

六月二十七日 等待变化等待机会

  1. 关于Conditional Operator会使得两个操作元类型保持一致的要点我添加了两个简单的例证。这个是我看到评论说那些从标准拷贝来的说明太干巴巴的启示。
  2. 看见这位高人的座用名,这个并不算是合法的lambda,也许将来是吧?我不确定。但是这个的确是可以的。
  3. 有人在评论指出是否和其他相类似的库的效率的比较。原来这个http://metaben.ch/是这么一个benchmark的平台,其中的库有brigand(这个有点老了是c++11的时代没有怎么更新了)kvasir(这个也是四五年没有更新了)这个是完全不同的领域,说明是说嵌入式的吧?,而我只找到mp11(这个已经是boost的一部分)它和mpl的关系是怎样的呢?正规的meta function的定义都是基于class的那种用constexpr的函数是非法的吧?A metafunction is a class template or a template alias whose parameters are all types 对于mp11的说明里一个结构里的模板成员函数和一个模板结构的一个成员这两者之间究竟有和使用的玄妙呢? A quoted metafunction is a class with a public metafunction member called fn
  4. 我找到了cppcon上我喜爱的一个大师关于这几个库的评价,看他的例子我是既惊又佩,就是五体投地也不夸张。我现在还只能一个字符一个字符的读,而且手机屏幕太小我必须反反复复的来来回回的看才能看懂一行,第一遍是语法,第二遍才开始试着理解用意,总之,看了前面忘记后边。才看了一个开头我就困了,不过现在看明白了一个大的图景,如果不使用库的话的确自己写的代码是挺多的,而使用库的话,似乎mp11的代码最简洁,似乎是这样子吧?这个Arthur O'Dwyer大师是我的一个指路明灯。

六月二十八日 等待变化等待机会

  1. 大师的手技是值得学习的,因为不依赖于任何现有的库完全凭借标准库来实现那都是多年积累的功力,是最值得仔细临摹观赏的。我决定再次领会记录。

六月二十九日 等待变化等待机会

  1. 昨天卡在template template,我算是明白了这个不是一时半会能够掌握的,只能一步一步来,学习的过程改正了一个例子,因为作者想要说明一些要点生生造了一个例子根本不可能编译,这个是我的改正,虽然能够编译但是已经无法达到作者原来要表达的意思了。 核心要点就是模板成员函数的实现必须和声明的模板参数保持一致,不是说你认为它是模板参数的符号就可以随意代替,编译器在看到它的声明后这个代号就是它的签名的一部分不能再改变了。当然作者的表达的意思是对的,只不过例子不够恰当,不可能编译。 作为对照,作为非模板成员函数你在实现的时候,模板的参数名字可以任意改变不受限制,可是以上的模板类的模板成员函数的模板参数名字却似乎成为一个固定符号成为函数签名的一部分在实现部分不能任意变换,我不知道这个是否是标准的一部分还是编译器实现的限制?果然是的,似乎目前只有clang支持这个语法,gcc/msvc都不支持,我只好羞愧的把我的拙劣的修改撤销了。
  2. 翻看defects report,这个最基本的例子说明了模板参数的模板参数是无名的,它只代表模板参数的模样我不知道它究竟有何必要列名? 注意这个X<int> x;最能说明问题,就是这里T只是作为一个placeholder的角色,本身不能参与到具体的类型的声明计算。所以根本没有必要出现命名。
  3. 这个问题似乎已经解决了?可是我连问题都看不懂。
  4. 这个报告我也看不大懂,这个语法上是没有问题的 但是结果是它是一个递归定义,编译器进入一个死循环900次报错了,就是说语法上没有问题,但是到底A是class还是template呢?如果是class的话,那么这个断言应该是成立的吧? 可是这个导致了递归说明A是template,那么这个问题就是被解决成了template了?这里引用标准
    Within the scope of a class template, when the name of the template is neither qualified nor followed by <, it is equivalent to the name of the template followed by the template-parameters enclosed in <>.
    我的理解是模板类。复习一下,所谓qualified意味着有scope::操作符。

    Qualified identifiers

    A qualified id-expression is an unqualified id-expression prepended by a scope resolution operator ::, and optionally, a sequence of enumeration(since C++11), class or namespace names or decltype expressions(since C++11) separated by scope resolution operators.

    但是如果改动一下,这个是成立的,它说明了什么呢? 指针不是递归定义,这个即便不是模板类也是如此,那么现在这个和模板就无关了吗?这个是否说明了这里的A成了类而不是模板类呢?我都要吐了。
  5. 学习concept,这个在模板类里的应用往往是定义一个类型然后作为模板参数的类型来限制参数类型的种类。而concept本身是需要返回值为bool类型的,比如这个是我第一次尝试使用concept定义一个模板类A 的模板参数类型不能是普通简单类型或者union,换言之它的模板参数必须是一个类 一种使用concept的场景是使用alias 它的效果是我们限制了这个情况B<int> b;而这个声明是合法的
  6. 这个是什么意思呢?我看不太懂,总之这个是编译不过的 必须要添加一个T::template才行, 这个也是我第一次有点明白这种::template是什么意思了,我试着解释,首先我们要把A的模板参数U具体化为一个成员变量以便来测试 这里已经透露出了U就是一个模板类,那么不妨把它实例化为模板参数是int。那么接下去我们怎么实例化这个A呢? 比如我需要这么一个A<B> a;我要怎么定义这个B呢?经过反复尝试编译器提示我这个B它期待着一个模板类叫做X所以它是这样子的 那么怎么检验呢?
  7. 委员会是支持这个建议的,那么它的意义是什么呢?我不太理解,不过我做了一个尝试 注意A<int>是等价的。
  8. 安能辨我是雌雄?一开始我自然的认为这个编译会有歧义错误 结果没有!这个说明编译器已经采用了建议。我为了能够分辨到底哪一个specialization被使用了,把原来例子的type改动成了integral_constant,所以这个说明了第二个形式被采用了 这个是非常的微妙的结果。我已经要吐出来了。出去走走吧。
  9. 关于template template的恶补暂时告一段落,理论总是要和实践相结合的,而问题导向是学习的一个抓手,我的目的就是要解决读码过程中的障碍。
  10. 现学现卖模仿大师的编程风格实现一个简单的cat以便连接任意多的index_sequence,这个是一个很简单的功能可以有很多种实现路径,而且归根结底大师的这个算法本质上很像是fold expression里的unary right fold。 这个模板的默认值很重要,没有它们编译器不知道我在做specialization 这个基本上模仿了大师的做法,几乎就是照抄,可是抄一遍也是一种学习,你不抄你都不知道你会忽略什么,所以伟大领袖当初鼓励文革期间考试作弊也是有道理的,考试不会的时候抄一遍比平常学的更扎实! 当然了为了输出打印还是需要重载操作符,同时要注意我的重载不可能在名字空间里找到,因为index_sequence是在std namespace里的,所以,根据参数依赖搜索原则编译器只会在std namespace里查找就不会正确的打印了。 输出是{1,2,3,4,5,6,7}

六月三十日 等待变化等待机会

  1. 模仿大师的作品临摹了一遍花了差不多两天时间,发现其实我还是一知半解照猫画虎亦步亦趋邯郸学步如履薄冰胆战心惊之后还是心有馀悸惴惴不安诚惶诚恐。 这里面遇到很多很浅薄的问题都是属于基本问题而我并没有想清楚,最最致命的就是对于partial specialization的理解不透,导致对于模板如何使用index_sequence作为模板参数一直想不通,当然这不是我一个人的问题我肯定是反反复复的查询这一个问题了吧因为stackoverflow我已经点过赞了。,和这个问题相关的就是我纠结于我的排序函数的模板参数到底是接受一个index_sequence还是接受一个数列,显然后者是无法操作作为递归来传递参数的,前者作为类型是可以自由传递而不产生运行期耗费的,这一点的理解我是最近才有更深的体会,但是我对于怎样传递一个有着多个index_sequence的模板参数的元函数依然不明所以,苦苦想了很久似乎可以使用template template来表达,但是始终就像水中捞月触手可得却又得而复失。总之,最后决定嵌套一层把多个index_sequence先合并作为模板参数是比较简单的。这个是和大师的情况略有不同的地方,还有就是大师使用的是一个虚拟的容器或者神器typelist是一个类型来承接所有的类而我使用标准的index_sequence那么更容易,反而让我对于大师使用所谓的as的alias感到迷惑,因为在我看来大师的typelist的alias似乎多此一举那个难道不是类型本身吗? 这个是我不能理解的,但是我的感觉这个是大师为了各个库转换的一个接口吧?因为显然的对于手工打造这个不是必须的。 然后就是无数的试错,因为我对于何时使用typename重新定义nested type不是很有把握,这个滥用之后编译器反而会抱怨,必须精准必要。而我因为简化了使用index_sequence导致很多不需要template template降低了难度实际上很多细节就没有办法理解了,尤其是使用partial specialization的方法就更加的模糊了。
  2. 外面的雨下的好大,天凉好个天。

七月一日 等待变化等待机会

  1. 我发现阅读这些报告的例子就已经足够令人震撼的了,因为这些例子往往是如此的深邃与精挑细选以至于思想的震撼对于越是初级玩家越是震撼强烈。这个是关于函数签名的解析原理,这个需要仔细的体会

    According to 9.3.4.6 [dcl.fct] paragraph 5,

    The type of a function is determined using the following rules. The type of each parameter (including function parameter packs) is determined from its own decl-specifier-seq and declarator也就是说函数的签名只决定于这两样,这个是小学生都明白的道理,不是吗?. After determining the type of each parameter, any parameter of type “array of T” or “function returning T为什么要单单提这两样呢?因为函数参数传递数组是不允许的,只能传递指针,这个是尽人皆知的,这里是再次提醒。至于说函数参数是一个函数表达式这个需要计算它的返回值类型,可是这里的细节是如果函数返回值类型是传值还是传引用还是传指针,这个是回答,一派胡言!人家说的是参数是一个函数的话要把参数改成是一个函数指针。这个例子说明了一切!本来参数f是一个函数,但是签名必须改成指针! 这个是我从来没有想到过的问题! is adjusted to be “pointer to T” or “pointer to function returning T,” respectively. After producing the list of parameter types, any top-level cv-qualifiers modifying a parameter type are deleted when forming the function type. The resulting list of transformed parameter types and the presence or absence of the ellipsis or a function parameter pack is the function's parameter-type-list. [Note: This transformation does not affect the types of the parameters. For example, int(*)(const int p, decltype(p)*) and int(*)(int, const int*) are identical types. —end note]
  2. 你生活的每一天都是幸福的偶然
    “你很快就会知道一切的,所有人都会知道。汪教授,你的人生中有重大的变故吗?这变故突然完全改变了你的生活,对你来说,世界在一夜之间变得完全不同。”
    
    “没有。”
    
    “那你的生活是一种偶然,世界有这么多变幻莫测的因素,你的人生却没什么变故。”
    
    “大部分人都是这样嘛。”
    
    “那大部分人的人生都是偶然。”
    
    “可多少代人都是这么平淡地过来的。”
    
    “都是偶然。”
    
    “是的,整个人类历史也是偶然,从石器时代到今天都没什么重大变故,真幸运。但既然是幸运,总有结束的一天;现在我告诉你,结束了,做好思想准备吧。”
    

七月二日 等待变化等待机会

  1. 关于昨天那个不可描述的模板的问题,作者想表达的是什么我至今没有理解,他引用的相对照的例子我在体会这个是一个典型的教科书式的SFINAE的例子:首先一个名如其人的alias是使用第一个参数first_of 然后关键的关键是在返回值上做文章 因为它的核心就是编译器要替换模板参数的时候生成函数返回值,虽然函数返回值不是函数重载所必须的,但是它是函数签名的一部分,这里我就觉得有些矛盾,具体细节想不清楚。 这个是与之对照的万能的fallback 那么对于 为什么第一个f偏好第一个模板呢?很显然的是因为函数参数起了决定作用,因为函数首先使用参数类型匹配来决定最佳的选择。而模板X能够让函数返回值成立,但是第二个f很显然的试图追随同样原则,但是它的返回值不成立,编译器默默的吞下苦果无声的退而求其次使用了万能的第二个f,这个就是整个SFINAE的过程。但是作者的核心问题是什么呢?我现在总算明白了一点昨天的<不可描述的问题是Non-type_template_parameter 也就是说我昨天糊涂的没有注意到第二个模板参数并不是一个类型参数,而是non-typed。而我明白这一点后居然发现了如何描述这个不可描述的类型是什么: 这个是否是很神奇?同样的另外一个例子它是这样子的 这个是不是很让人惊讶一个变量的类型居然依赖于它传入参数的变量的id,这个说明我是看不太懂,也许说的就是这个意思吧?
    An id-expression naming a non-type template-parameter of class type T denotes a static storage duration object of type const T, known as a template parameter object, whose value is that of the corresponding template argument after it has been converted to the type of the template-parameter.
    我想最关键的说明就是这句An id-expression naming a non-type template-parameter of class type T,这个就是这个static_assert成立的来源。我现在也开始领悟到作者的问题是什么,就是说为什么对于这个non-type template parameter没有经历类似的SFINAE的过程呢?很显然的int::size是不成立的,但是这个并不影响结果,似乎是模板参数先转换类型确定Tint之后顺理成章的认定第二个non-type的就是合法的T*或者说int*而完全不用考虑int::size不成立的问题。
  2. 看来我的理解力是有问题,对于common_type本意是寻找两个类型的共通点得到的类型,我却理解成了大众广泛接受的类型。
  3. 长久以来我对于void_t始终不理解,当初看cppcon的视频里原作者的解释博得满堂喝彩,而我却不知所以然,然后在阅读例子的时候我对于这个例子百思不得其解,最后才意识到它也是一个non-type template parameter!所以从这点来看它和前面的例子里non-type template parameter是相似的,这里的转换是非常的tricky,判断先后是一个黑盒子。 这里的void_t实际上是我最大的理解障碍,为什么要把它定义为void的别名呢? 这里我本来以为我发现了第二个形式的std::void_t是一个non-type template parameter,也许是specialization的关键,现在看来似乎是无关的?
  4. 《三体》问题的核心是这样子的:当一个文明实体内部某些成员对于本文明内部环境的某些不满达到一定程度时,以至于绝望地认为本文明内部无法解决这些问题并寄希望于寻找外部文明来解决本文明内部的问题,而这个外部文明的真实意愿仅仅是出于自身发展利益的目的对于该文明施行殖民式的毁灭所引发的文明间的生死冲突。
    这种现象在很多文明的很多时期是普遍存在的。在之前的中国内部各种矛盾日益尖锐的一个时期,很多所谓的前卫知识分子以社会公益为己任希望引进美欧文明来解决中国本地的社会问题,但是事实上美国为代表的外部文明的真正目的却仅仅是为了维持其自身的既得利益而意图彻底消除威胁自身的潜在对手的潜力。这个现象像极了地球三体组织(ETO)意图引进三体文明来拯救地球文明的初衷却达到了引狼入室的效果一样,作为三体文明争取生存空间是唯一重要的内容,其他是否消灭曾经客观上帮助它们达到这一目的的ETO以及人类文明的利益与诉求都是不重要的。可怜的公知团体中有很多成员也许都怀着一些天真的期待认为文明进化的更高阶段意味着道德水平的更高阶段,因此像当然的认为出于食物链高端的美欧社会应该出于更高的道德高地来帮助发展中国家来改善其生活状况,却不知争取更大的生存空间始终是围绕着任何发展阶段的文明的最核心的内容,在生存第一的目的面前,所谓的道德也是作为约束限制麻痹对手的工具。

七月三日 等待变化等待机会

  1. 模板的一个要义就是检验是否是ill-formatted,就是依赖于编译器的能否编译来实现的,这个思路我还是没有根深蒂固的建立起来。比如对于enable_if的理解就是不够深刻。 这个和昨天的void_t很类似的都是依赖于有了default parameter,然后通过模板参数的specialization得到一个检验是否ill-format的机会。在默认情况下不定义这个成员类型type导致使用的代码里出现了未定义的成员type而引发编译失败。 这里我们真的关心enable_if_t的类型是否为void吗?其实是无所谓的,重要的是is_integral的返回值value如果是false的话enable_if_t将使用不存在的type这个member从而引发编译错误,这个是我们唯一关心的:如果成立编译没有问题,否则编译失败,这是一条原则。与之对比的是is_integral之类的实现就很容易了,它们都是从integral_constant的specialization继承来的,这个给了后继实现的开放空间,也很简单与清晰。不过我把两个风马牛不相及的模板拿来比较实在是脑回路太长了。
  2. 但是最有趣的还是关于模板的语法的歧义,这一点在c++98的程序员几乎是不会遇到的,因为那个时候程序员根本没有过过一天幸福日子,总是小心翼翼,而今天就会为了幸福付出代价。下面这个是比较让人脑子一嗡的编译错误: 这个情况就是当你对于这类模板的使用不熟悉的时候照搬才最痛苦,你的精力主要都集中在对于这个使用的语法是否合法的时候却没有想到这个是又一个编译器语法分析产生歧义的错误。因为核心的问题是你忘记了添加一个空格bool>=true中间,之所以我会对于这么简单的语法错误不敏感的一个原因就在于对于无名模板参数的不习惯。难道这样子的模板不是很有意义吗? 因为我内心对于这种定义是感到不可思议的,为什么会有这个定义的可能呢?如果不能使用何必要定义呢?实际上c++的一个基本原则就是你不能阻止程序员拿枪打自己的脚,这个就是编程的自由。自由不等同于自律,也不需要自律,你想干什么都行,这个就是所谓的程序正义,欧美的律师都是编程的好手,能够把现行法律体系玩的滴流转需要的就是这种敢于冒天下之大不韪的离经叛道,不计较社会成本的个性自由。这个在和平年代都是社会的引领精英为人津津乐道,一旦战争爆发或者大灾大难来临,等着排队枪毙的就是这类人。
  3. 三体文明用来阻遏地球文明的三个计划早已被美帝文明用于遏制中华文明的战略手段:
  4. 我一开始并没有领悟到这种unnamed enum type的用意,作为一个无名的enum类型它的用意就是要让名字空间以外的用户无法使用。于是作为模板参数来使用的时候你不能写出这个模板函数的类型。但是看样子我实际上还是没有领会,这个是不能声明作为成员变量的,因为 myenum这个enum的类型名字是必须的,不能声明一个无名的enum class。 但是这里我搞糊涂的是关于enum class和enum之间的区别。比如我可以声明一个无名的enum,但是不能是enum class因为编译器要抱怨error: unnamed scoped enum is not allowed 然后我可以定义一个模板类 注意成员变量是不能声明为constexpr的,除非是static的,成员只能声明为const,只有成员函数才能声明为constexpr 注意这里我有意把ctor也定义为constexpr是为了能够使用static_assert 但是当我理所当然的以为我可以这样子断言t1的类型的时候: 发现编译错误, 原因是enum_1并不是一个模板类型参数,而是一个值,但是它的类型因为是匿名的enum,所以我无法命名它的类型。这里我常常会潜意识里认为enum的值好像是不同的类型一样,可是实际上不是的。另一个很有意思的观察就是t1,t2,t3的类型似乎是一样的 你居然可以使用类型Struct来声明,可是当你以为这个就是t1的类型的时候,这个不成立的断言又浇了你一盆冷水 因为编译器抱怨Struct没有模板参数,编译失败。

七月四日 等待变化等待机会

  1. 原来scoped和unspcodedenum declaration有着本质的区别。
    There are two distinct kinds of enumerations: unscoped enumeration (declared with the enum-key enum) and scoped enumeration (declared with the enum-key enum class or enum struct).
    也就是说如果我声明为enum class/struct,那么它就应该是所谓的scoped enumeration。所以,我遇到的问题和non-typed template parameter是两个截然不同的问题。我遇到的是一个typed template parameter,只不过它的enum type是一个匿名的。
  2. 再一点就是scoped enum从语法出发就不允许省略名字,而且我看到一个更加令人惊奇的东西就是第三种的所谓的opaque enum declaration for a scoped enumeration whose underlying type is int 比如 这个就是它的典型的应用就是它给了你一个enum的class,根本就是一个标志类比如你在dispatch的时候可以作为case来使用的标识。当然这个用途的确不大,我们能够使用其他的做法达到同样目的还能够应用index,它能吗?因为它就是一个int的别名吧,但是它是一个类,你不能给它赋任何值,纯粹的无意义的东西。
  3. 我每每遇到specialization的情况总是感到很模糊,应该好好学习一下定义,而阅读枯燥的定义不如看例子来的明晰: 这个是一个很好的说明:首先,文档里提到所谓的specialization就是以template<>领头的我不知道这个是不是仅仅适用于一个模板参数的情况。 其次,这个是一个技巧就是说模板参数和实参的类型一致可以在specialization的时候甚至省略掉模板参数对比一下这个是你通常需要写的 可是我们在以上省略了<int>直接使用参数类型就让编译器推理得到模板参数的类型void g(int){}当然前提是我们需要这个空模板template<>来提示这个是specialization。 但是这个情况如果你为了偷懒把返回值改成auto的话编译器就会误解了: 这里一定要给出明确的返回值int
  4. 还有一个有趣的事情就是关于函数类型typedef int (funct)(); 这个是可以用作模板参数的,比如 可是任何一个函数是不能直接返回另一个函数的:比如这个就是非法的funct f(){return funct{};},编译器会抱怨error: 'f' declared as function returning a function 所以,基于这个原因你只能返回函数指针typedef int (*functPtr)();然后才可以定义functPtr f(){return functPtr{};} 所以,这里对于这样子的模板的错误不是模板specialization本身的错误而是任何函数都不能返回函数类型的返回值:
  5. 我觉得这个gcc的格言似乎是有某种含义的吧?

    Sun Jul 4 2021 20:54:39 UTC

    In a world without Borders and Fences who needs Windows and Gates?

    很明显的GCC是一个反对微软的政治组织?哈哈,在独立日贴这样一个snapshot是有意义的因为GNU的宗旨就在于摆脱垄断与独裁争取自由与独立。
  6. 我觉得这个应该是一个gcc的bug,但是我不确定的是这个是否被suspended不打算解决呢? 在1001. Parameter type adjustment in dependent parameter types 里的这个例子编译器对于添加了const之后就不能辨认出模板函数的specialization了 编译器的错误是说不能发现f<int>是什么模板函数:does not match any template declaration。如果没有const的话, 这个是可以编译的,原因应该是挺复杂的,这个在相关的bug的解释相当的复杂,我只能理解就是模板参数已经被当作了指针int*,但是。。。函数原型是数组。。。 总而言之,这个问题的状态是
    Drafting: Informal consensus has been reached in the working group and is described in rough terms in a Tentative Resolution, although precise wording for the change is not yet available.
    那么是否是gcc要等待结果再去修正呢?可能是吧?

七月五日 等待变化等待机会

  1. 关于昨天的问题的一个补充就是说,对于类型的转化尤其是数组需要使用所谓的decay, 也就是说要把数组类型转化为指针类型
    If T names the type "array of U" or "reference to array of U", the member typedef type is U*.
    同时添加cv这类的const不能简单的添加const,而是要使用add_const 换言之 但是当我尝试这样子对于模板原型的时候失败了,就是说这些type_traits是针对实际类型对于模板来说它们更像是一个变化的形式,实际上编译器在解决的过程中就是在使用类似的过程吧?总而言之,我觉得这个是编译器的实现问题,而gcc在等待委员会的决定。那么clang和其他编译器如何呢?clang居然已经解决了这个问题!MSVC也没有问题!看来gcc实在是老了!
  2. 这个是所谓的debuggingGCC,但是我没有那个野心,那个对于我来说还是太超前了吧?实际上我还是有野心的,而且使用gdb根本就是任何一个程序员的本能,即便是gcc本身的debug过程也没有那么的特殊,它并不是什么神秘的特殊程序,我也没有遵循这个指示去作因为我看不大懂它的意思,何况我遇到的模板的问题根本就是parser的问题要简单的多得多,我之前已经有了尝试cpp/cc1/cc1plus的经验可以直接绕过gcc/g++这一层可以稍微的减少一点工作量,当然我很快就定位到了parser.c这个巨大的文件,可是你要再精准定位出错的方位还花了我一两个小时,这个一点不夸张,因为你要在那些一个函数就是几千行代码里找到报错的具体函数还只能依靠二分法一步一步减少搜索范围最后定位在了这个函数determine_specialization这个函数名字看着多么亲切,这个不就是我魂牵梦绕的如何断定specialization的实现代码吗?我能够看得懂吗?而在这个之前我在几个超大函数的泥潭里跋涉了上千行的n+return,比如这个让人费解它的名字的函数grokdeclarator有差不多四千行三千行代码!天哪!一个函数而已,不知道有多少个if/else也不知道有多少个错误返回,我根本就没有耐心看其中的代码因为也看不懂,只是机械的敲回车键一路跟踪下来,程序员的确是一个体力活,手指头都敲麻木了。我也不想再看了,反正知道出错的函数了下次定位就简单了。休息一下吧。
  3. 我没有指望自己能够真的debug这个问题,但是很大的收获是终于知道gcc模板的代码在哪里了,而且再也没有对于模板有那么神秘莫测的感觉了,其实gcc的模板parsing代码挺丑陋的,现在看起来应该没有什么特别的地方无非是很复杂而已。
  4. 计谋是弱者才必需的,这一点和仁慈是弱势一方期盼的道理是一样的。这个正如在战场上遵守日内瓦公约关于人道的对待战俘是一样的道理,因为持这种理念的一方是从己方有可能是失败一方的想法出发的,而作为绝对强势的一方是不需要保留战俘的。
  5. 《三体》里最有远见的人物是谁?是章北海

    章北海与父亲的重要对话一共有三句:

    父:要多想。

    海:想了以后呢?

    父:北海,我只能告诉你在那以前要多想。

    这三句对话出现在《黑暗森林》前半部分,而在本书后半部分,章北海率领自然选择号逃亡后与亚洲舰队司令的第一次通讯之中,他才将这三句对话发生的背景——即他们父子坚定不移的逃亡主义思想之来源做出了说明。

    父亲和北海从三体危机一开始就在思考这场战争最基本的问题,后来在章父身边出现了同样在思考战争基本的 “未来史学派” ,早在太空军建立之前,早在面壁计划开始之前,这群人就对人类的未来进行了详细周密的推演,他们不但预言了大低谷,预言了人类文明的复兴,也预言了末日之战中人类将一败涂地。

    如果说章北海是自己给自己烙下了另一种形式的“思想钢印”,那么他对战败的预测和逃亡的信念,就是源自未来史学派内部的研究和讨论。

    要多想

    意思就是要比别的人想的更多,更全面,不但要知道人类必定失败,更要想出应对最坏情况的详细计划并且提前部署实施。

    在对话之前,父子两人都知道彼此对这场战争的看法,两人都坚信人类必败,那么注定失败的战争应该怎么打?无非两种情况,一种是无谓的牺牲,那么父亲的话应该是“要勇敢”或者“别多想”。而“要多想”,就是另一种选择:准备逃亡,保存火种!所以章北海在听到这三个字的时候,心中很感慨,父亲和自己不只对战争的预测一致,对应对战争的想法也一致,父亲也是逃亡主义者。

    但他不知道在逃亡之后应该怎么办。于是又问了“想了以后呢?”

    章父回答:“北海,我只能告诉你在那以前要多想。”

    “在那以前”这四个字可以把时间线分为三段,“那”代表的是逃亡机会出现的时段,在这个时段之前要多想。而在这个时段之中和这之后则不用多想,因为机会出现的瞬间需要果断不能多想,之后则是人类进入新的状态,星舰文明诞生谁也不知道会有什么事情发生,想了也没用。

    这三句对话,是章父对北海的最后指导,也决定了章北海对待未来的态度发展为坚定的逃亡主义。

    后文北海的一切行为和遭遇都是以这次对话为基点展开的,他的周密计划和果断的出逃为人类星舰文明的开始提供了宝贵机会。也正是因为对逃亡成功之后的事情缺少计划(并非没有预测,只是没有行动计划),导致自然选择号没有能够在黑暗战役的残酷对决中胜出,但是对于北海来说,这件事并不重要。他继承父亲志愿,唯一坚守的目标已经达成,人类文明的火种已经保存下来,所以对他来说,谁是最后的幸存者,“都一样”。

    (书中还着重描述了在逃亡之后章北海精神面貌的巨大变化,由一个紧绷着的弦放松下来,结合他早已对黑暗战役中星舰之间的猜疑链有所预估,却拖延了一个月还迟迟没有动手,这和逃亡成功之前的果决形成了鲜明对比,说明比起人类的延续,自身个体的存亡在北海心中分量要轻得多。)

    。。。

    在对话结束后,章北海还有一段内心独白:“爸爸,我们想的一样,这是我的幸运,我不会给您带来荣耀,但会让您安息。”

    有着无与伦比的远见卓识,刚毅绝伦的坚强信念,加上过人的胆魄,高超的智慧,以及那一点人性的善良。

    章北海,无愧是三体这套书中最闪耀的“人类四杰”之一。

  6. 我喜欢丘处机的这首诗:

    自古中秋月最明, 凉风届后夜弥清。 一天气象沉银汉, 四海鱼龙耀水清。


七月六日 等待变化等待机会

  1. 这个问题对我来说很难理解: Function partial specialization is not yet allowed as per the standard. In the example, you are actually overloading & not specializing the max<T1,T2> function.这里面的原因我不想追究,我只关心平常模板函数如果我不使用specialization的语法的话是不是就是说编译器自己从实参类型推导模板参数类型?所以,从这个角度来看就是函数重载?
  2. 我花了好几天才终于看懂了一点这个问题,说明说的很清楚但是我却读不懂其中的英文,正如同他指出的标准里规定
    The type of a function is determined using the following rules.
    The type of each parameter (including function parameter packs) is determined from its own parameter-declaration ([dcl.decl]). After determining the type of each parameter, any parameter of type “array of T” or of function type T is adjusted to be “pointer to T”. After producing the list of parameter types, any top-level cv-qualifiers modifying a parameter type are deleted when forming the function type. The resulting list of transformed parameter types and the presence or absence of the ellipsis or a function parameter pack is the function's parameter-type-list.
    这里翻译成中文就是函数的签名取决于函数的参数组,那么参数的类型的决定却有一个转换的过程,就是top-level cv-qualifiers modifying a parameter type deleted这里指的是最上层的那些const被最后去除了,也就是说函数的签名居然不包含那些最上层的const/volatile等等。这个本身就让我有些意外,这个难道会导致函数的类型签名不需要const之类的吗?这是不是违反常理呢? 但是这个却是事实: 同时更让人困惑的是这个过程并不会改变参数的类型?(这句话我就觉得别扭) 而上面的这个core 1001的问题也和这个有着密切的关系,作者认为标准含糊不清,但是后来的讨论又似乎是说这个是The CWG agreed that this behavior is intrinsic to the different ways cv-qualification applies to array types and non-array types. 由此我得出的猜测是这个现象只有数组才会有? 比如如果不是数组类型根本就不会有任何的问题。注意到标准提到函数转为函数指针和数组转为指针的过程都发生 ,但是即便是我们把上面的例子改为函数类型也没有问题,这个做正了问题的核心是数组的某些和cv-qualifier加载相关的特殊性的问题: 我花了两天时间debug但是依然没有很多的头绪,我现在走出了最最原始的一步就是在gcc的pt.c里determine_specialization对于要比较的两个函数声明的参数类型,其实很原始的warning打印的参数不是printf而是自成一派,比如 而我还没有找到这个%E是在哪里定义的,很多地方打印函数声明又使用了别的%qD之类的,我还要学习,不过这个过程却很简单,因为我之前编译过了debug版本的gcc,现在任何小小的修改只是编译一个文件是非常快的一个过程,所以,这个给了我一个很好的探索的机会。
  3. 这个说明TREE_LISTTREE_VEC解决了很多疑问!我现在遇到的函数参数是前者是一个链表,而类的继承似乎是后者是一个vector。单单打印就是头疼因为那些花狸狐哨的pretty_printer的选项是没有用的,gcc的diagnostic会直接根据TREE_CODE的返回类型自动选择dump的小函数,但是对于这两个我难道要自己寻找所有元素吗?这个真的是很麻烦因为那些个gimple tree看起来是有几百上千个成员,这个实在是很不好办。
  4. 不要依赖于文档了因为这个gcc的内部文档是给程序员看的,最好的文档就是代码,前提是你知道在哪里!关于树tree要去看cp-tree.def里面有详尽的解释!

七月七日 等待变化等待机会

  1. 茫茫然就明白了一件事,就是函数的声明是decl是<void f(const int*), 所以,很明显的参数是已经从数组转化为指针了,而所谓的template_id实际上就是函数名字,当然对于模板函数是要连同模板参数的f<int>, 确定是否是特殊化的目的就是在一系列函数中比对函数签名, 这个一系列函数是这么获得的fns = TREE_OPERAND (template_id, 0);,这个得到的是一个所谓的TREE_LIST,可以通过lkp_iterator的iterator来访问。对于函数来说都是获取它们的参数,就是TYPE_ARG_TYPES,得到的是一个TREE_LIST 那么怎么比较就是在这个所谓的compparms,它就是使用TREE_CHAIN一层一层的直接比较type是否一致。注意这里获取一个TREE_LIST的实际上的指针要使用TREE_VALUE,然后你就可以使用TREE_CODE得到它的类型了,这里我对于TREE_TYPE还不是很理解。至于说要怎么打印各个不同的类型就要参考error.c里面的代码了。这个实在是一个非常复杂的领域,相比较而言其他很多软件似乎都是小儿科了。我可以直接使用warning来打印tree因为在diagnostic会自己依赖不同类型漂亮输出。我现在就打算在compparms函数里面对面的输出参数来比较看看为什么不符合。不成功因为这里面远比我想的要复杂,因为TREE_CODE判断的不是所谓的tcc(tree code class),还要使用TREE_CODE_CLASS来包。。。总之,思路是有了,无非就是打印这个还是比较容易的。休整一下再说吧?
  2. 总之问题是很复杂的,因为在比较两个函数的参数签名的时候,使用的是所谓的TYPE_CANONICAL,而这要求它们的指针是同样的,这一点我觉得根本做不到吧?我的猜测是要把函数参数化简产生的一个通用的签名,那么模板参数中间也许有些什么地方不对吧?因为我在检查比较的参数签名的时候类型都是整数指针类型,但是比较的时候要看他们的TYPE_CANONICAL是否一致结果就失败了。
    
    (gdb) p TREE_CODE(TYPE_CANONICAL(TREE_VALUE(t1)))
    $102 = POINTER_TYPE
    (gdb) p TREE_CODE(TREE_TYPE(TYPE_CANONICAL(TREE_VALUE(t1))))
    $104 = INTEGER_TYPE
    (gdb) p TREE_CODE(TYPE_CANONICAL(TREE_VALUE(t2)))
    $105 = POINTER_TYPE
    (gdb) p TREE_CODE(TREE_TYPE(TYPE_CANONICAL(TREE_VALUE(t2))))
    $106 = INTEGER_TYPE
    
    对于函数类型的比较这个还不够吗?就是说数组转为canonical的时候也许有了什么偏差吧?
  3. 还有一个让我印象深刻的地方就是现在的编译器的确非常的强大,似乎能够对于宏有很好的检查,比如我要打印tree,但是如果宏返回的不是合法的地址,编译器直接就发现了,而不是运行期的崩溃,所以,在compparms里我不得不做了这些检查:
    
    if (TREE_TYPE(TREE_VALUE(t1))!=NULL_TREE && TREE_CODE(TREE_TYPE(TREE_VALUE(t1))) !=VOID_TYPE)
    	  warning(0, "t1: %qD", TREE_TYPE(TREE_VALUE(t1)));
    if (TREE_TYPE(TREE_VALUE(t2))!=NULL_TREE && TREE_CODE(TREE_TYPE(TREE_VALUE(t2))) !=VOID_TYPE)
         warning(0, "t2: %qD", TREE_TYPE(TREE_VALUE(t2)));
    
    当然这个基本上毫无意义因为warning是用来打印函数声明之类的,对于具体类型我不知道要怎么打印,但是这个似乎不重要因为我在gdb里就看到它们的类型是一样的。。。
  4. 《三体》里狙杀罗辑的基因导弹所采用的思路和当前的新冠疫情如出一辙,都有可能是某个神秘组织研发出来的针对某个特定的人的基因或者某一类人的基因的导弹,因为对于大多数人都是轻感冒的症状,可是对于某些特定基因类型的人群则是致命的。如果我们现在能够把这一年多来死亡的人群做一个全面的扫描也许就能够发现其中的端倪。在已经死去的几百万人类里也许就有罗辑。ETO今天为了杀死那个躲在地球的某个不为人知的地下的罗辑不惜杀死几百万人也要达成目的。为了杀死第四个面壁者,不惜消灭几百万人,而对于未达目的不择手段的ETO来说杀死这么多人一点问题都没有,他们担心的不是杀死更多的人而是担心三体要杀死罗辑的目的会暴露,所以不得不把杀死罗辑掩盖在杀死几百万以至于上千万人里。

七月八日 等待变化等待机会

  1. 也许c++最复杂也是最精华的部分在于模板,这个是我认为区别于其他大部分编程语言的部分吧?至少我google了java是没有的,似乎大部分的语言也都没有吧?反正这个对于编译器值作者是一个终极的挑战。而跟踪在gdb里我看到无数的千行以上的函数,整个pt.c有将近3万行代码,我想能读懂其中的注释就是专家了。eclipse的很多功能在这种超大文件下是不工作的。总之我是怀疑在解析模板参数的时候遇到模板类的成员数组作为模板函数的实参的时候计算canonical type的时候有误,这个是我的猜想,要在gdb里证实我的身体估计都顶不住了。先休息一下吧?
  2. 美国:给文明以岁月;
    中国:给岁月以文明;
    
  3. 在《三体》的构建的文明体系中,所有地球大国都衰落了,而一个个军事帝国崛起了,可以想像的是强大的美军各个司令部各自独立成为一个个雄霸一方的独立王国。各个司令部垄断了本地区的资源作为其维持统治的经济基础,比如美军各个司令部分别控制着所在大洲的各种资源。
  4. 中美博弈从现有状况来看似乎毫无悬念,因为时间在中国一方,如果按部就班中国将在不远的将来不战而胜并且将把优势不断拉大以至于美国最终在这场不会发生的决战中自生自灭。然而,这个前提是美国没有突然获得跨越性技术的假设上,在二战中如果美国的对手比如德国或者日本突然获得核武器技术将直接扭转战局,这就是技术爆炸的威胁。如何防止美国独霸新兴的改变游戏规则的新技术是这场决战的关键!
  5. 我只能说这个是绝对的异常的复杂的过程,首先单单是确定模板参数的过程就是如此复杂的过程,对于所谓的真正复杂的parameter pack我连看都不干看!其间模板都有可能是多重的,更不要说夹杂着specialization要去补全推测模板参数,这一点一开始就把我搞糊涂了,我常常搞不清楚是在计算模板参数还是函数参数,因为匹配的是模板函数的specialization的话最终是要把模板声明配合模板参数来最终计算出函数的参数类型来做匹配,总之这里面递归推导化简参数的过程无比复杂,因为我的参数是一个typename得到的nested type,而typename后面的是模板类的成员类型又是使用typedef定义的新类型 所以单单处理这些不同寻常的就经历了好多重的递归,而且中间还有无数的我看不懂的注释说明处理的情况是我连想都不敢想的问题。总之结果就是compparms比较结果不一致,这个结论不奇怪,关键的是其实他们比较的两个函数参数类型的所谓的TYPE_CANONICAL地址不一样,这一点我昨天就知道了,只不过我今天确定了两个canonical虽然不同,但是都是正确的类型都是POINTER_TYPE指向一个INTEGER_CST,这是不是很有趣的现象?我其实已经明白了很可能是产生的canonical不对,但是单单跟踪到产生它的路径就是如此的漫长我都要吐了,因为你脑子要很清楚现在在干什么,可是我昨天根本没有理解到有一多半的过程是在计算模板参数的过程,所以,看的头昏脑胀,但是几天明白了一些依然是气血翻涌晕头转向,这些代码写的实在是太不规范了,因为从各种不同的缩进就知道这个在古老的使用各种vi/vim/emac时代个人设置的tabsize的不同加上多次的bugfix提交的片段就知道代码风格乱七八糟,缩进和if/else使用{}不使用毫无章法,我个人是反对风格沙皇的集权统治,但是也是被这个代码看的头疼,而且很多时候程序员为了节省几个字在switch/case里面故意不用break让他们溜到后边的case去统一处理,这个实在是所有批判编程风格的活教材,如果你对那个时代遗留的风气感到气愤想要找一个反面典型的话,gcc的代码是最好的一个例证。也难怪clang采用了完全傻瓜化的递归处理,我估计使用gimple结构来存储数据也许还不是最让人头疼的吧。。。总之,磬竹难书,真是要诉苦估计三天三夜也说不完。

    每次我发出对于gcc开发前辈的惊叹的时候脑子里就回想着这些歌词。也只有这些词句能够表达我对于这些前辈的崇敬敬仰之情:

    我们的王成,是毛泽东的战士,是顶天立地的英雄,是特殊材料制成的人。他的豪迈气概从那里来?因为他对朝鲜人民无限的爱,对侵略者切齿的恨!在我们志愿军队伍里,有千千万万个王成,这就是我们伟大祖国的骄傲和光荣!

    烽烟滚滚唱英雄,
    四面青山侧耳听,侧耳听
    晴天响雷敲金鼓,
    大海扬波作和声
    人民战士驱虎豹,
    舍生忘死保和平
    为什么战旗美如画,
    英雄的鲜血染红了它
    为什么大地春常在,
    英雄的生命开鲜花;
    
    英雄猛跳出战壕,
    一道电光裂长空,裂长空
    地陷进去独身挡,
    天塌下来只手擎
    两脚熊熊趟烈火,
    浑身闪闪披彩虹
    为什么战旗美如画,
    英雄的鲜血染红了它
    为什么大地春常在,
    英雄的生命开鲜花;
    
    一声呼叫炮声隆,
    翻江倒海天地崩,天地崩
    双手紧握爆破筒,
    怒目喷火热血涌
    敌人腐烂变泥土,
    勇士辉煌化金星
    为什么战旗美如画,
    英雄的鲜血染红了它
    为什么大地春常在,
    英雄的生命开鲜花!
    


七月九日 等待变化等待机会

  1. 不去理解gimple的各种union的tree,那是不可能理解其中的奥秘的。但是这个是很复杂的一个过程。也许任何学习探索的过程都是先从比较入手,同样的我根本就不能确定这个一定就是一个bug,那么在证伪之前首先要证明,就好比证明之前首先要证伪。

  2. 《三体》中人类四杰之一的维德并不是对三体文明最有威胁的人物,但是他在中美博弈中是对中国威胁最大的那一类人。美国现在最有优势的一个领域就是在隐蔽战线的登峰造极,与之匹配的是这一领域的执行者的执着冷静狂放不羁不达目的不择手段以及冷酷残忍的登峰造极。
  3. 我在《今日头条》看到一个古老的几乎人人皆知的加密传输方法:
    后来,有人想到了一个办法:A先把信放到箱子里后上一把锁,然后让邮差把箱子送给B。B在接到箱子后不打开,而是再上一把锁,这样箱子上就有两把锁了。B再让邮差把箱子送回给A,然后A把箱子上自己那把锁打开,让邮差再把箱子送回给B。这时箱子上只有B的一把锁了,B用自己的钥匙把箱子打开就可以了。这个过程中,邮差接触不到任何一把钥匙。
    原文是有意引出RSA的公钥私钥,但是假如我们降低一些要求能不能这样子呢?
  4. 其实应该是有一个捷径的我没有找到!在error.c里有这方面的打印选项:
    %A   function argument-list.
    %C   tree code.
    %D   declaration.
    %E   expression.
    %F   function declaration.
    %G   gcall *
    %H   type difference (from).
    %I   type difference (to).
    %K   tree
    %L   language as used in extern "lang".
    %O   binary operator.
    %P   function parameter whose position is indicated by an integer.
    %Q   assignment operator.
    %S   substitution (template + args)
    %T   type.
    %V   cv-qualifier.
    %X   exception-specification.  */
    
  5. 我决定还是先报告这个bug吧,看样子我是不大可能fix了,本来也没有指望,不过这是最好的学习机会。

七月十日 等待变化等待机会

  1. 至少我对于问题的认识又进了一步:我昨天还是稀里糊涂的bark the wrong tree,就是说这个和函数参数的处理无关,而是函数的声明部分相关,也就是说在我的这个diagnostic输出里显示的很清楚,模板函数特殊化根本就不承认有这个const参数类型,而是模板函数声明部分不应该保留const。换言之,我的这个输出warning: fn_arg_types: ‘(int*)’; decl_arg_types: ‘(const int*)’decl_arg_types指的就是。直到这一刻我才意识到我前几天的gdb根本就是浪费时间,specialization没有错,错的是声明啊!这个可真的是晴天霹雳,真好像侦探片里侦探一直在追寻谋杀嫌疑犯,最后发现受害人自己才是凶手,彷佛自导自演的一出骗局!为什么呢?因为标准要求top level cv qualifier必须去掉,而声明没有。以前我看到最早的core1001里的讨论是说这个跟数组的处理方式有关系,的确如此,因为标准对于两样东西做函数参数有特别处理:一个是数组,一个是函数。两者都必须先转化为指针,只不过后者是函数指针,那么我把代码改动成类型是一个函数: 然后同样的输出信息就是正确的了:
    warning: fn_arg_types: ‘(int (*)(int))’; decl_arg_types: ‘(int (*)(int))’
    如今不论是声明还是特殊化的参数类型都是正确的去除了const,所以,这个数组的在模板函数的声明的解析存在问题。这个是我的结论,也是几天的无数次gdb的一个原本显而易见的结论。
  2. 但是当我面对那个无比丑陋和令人不解的decl.c:grokfndecl我简直是有些崩溃了,到底是递归调用了还是怎么回事我在gdb里跟丢了。这个是常见的事情,首先gcc的代码实在是读的很困难,一个个if/else的不对齐还有无数的不同缩进让人看的实在是费劲,遇到几个超千行的函数你就糊涂了。今天才明白watch在超出上下文就,我以前以为它是使用内存地址的,现在才知道和普通的break一样是靠symbol表达式来计算的,否则干嘛会因为上下文改变就失效呢?我又饿又累还是休息一下吧?

七月十一日 等待变化等待机会

  1. 对于简单的函数声明的参数打印可以遵循这个做法,这个也是我通过gdb跟踪打印代码帮助的结果: 总之,使用这些简单的打印可以打大方便debug,省的到处打log。但是这个gdb的跟踪实在是太复杂,主要是对于代码数据结构的不熟悉,c++的模板特殊化是一个及其复杂的过程,你可以想像大多数编程语言都没有模板的特性的一个原因是它太复杂了,复杂的东西自然是非常的强大,而强大的东西有各种漏洞弱点是不足为奇的。我觉得使用gdb是一种很好的学习代码的过程,因为对于这么复杂的代码即便强大的eclipse也有很多无法解析的部分,尤其是铺天盖地的宏定义,让人实在无法简单利用查找的方法来浏览代码。这个时候gdb是唯一的向导。
  2. 《三体》里关于执剑人候选人选的章节是最能说明整个黑暗森林理论的实质。对于威慑的本质的理解决定了整个人类文明和三体文明博弈的最基础战略,三体派驻人类的大使智子要以最柔美的外形来麻痹人类世界的战略,这本身就是要作为一种公关政策来掩盖三体本质上为争取生存空间的不择手段的看似邪恶的本质,另一个小小的细节就是绝对不暴露三体人生物形态的细节而只以智子的形态来与人类交流是为了减小人类公众对于三体文明异类的印象。另一方面,威慑力的计算是无法评估民主体制下的多权力决策机制的,也就是说三体文明和人类文明都明白威慑力的实现必须用个人独裁机制实现,结合当前美国国会剥夺总统交战权的权力法案可以明白美国已经丧失了威慑力。
  3. 非常的痛苦的debug,就在于说这个是无比复杂的过程,而我又几乎是一窍不通,即便你对于标准熟悉也不一定能够明白其中的逻辑,更不要说在那个四千行三千行代码的一个函数grokdeclarator里打转转了。但是我觉得我终于开窍了,这个不是这个函数的错,它只是忠实的得到了一个函数参数的签名,所以是在比它高一级的函数调用里传递来的参数,而另一点我的观察是第一次调用的时候你看到的函数的签名是模板参数的原型,第二次才看到实例化的模板函数的参数真实参数,所以,我这才明白问题可能出在这个实例化的过程cp_parser_explicit_instantiation,这个看上去是符合逻辑的,结合代码里的注解你就明白了,通常人们说函数的声明declaration但是这里实际上指的是实例化的过程instantiation。这都是具体实现的细节,可能是不值一提吧?但是要证实这一点也不是那么容易,比如在实例化过程里难道不应该准确的保留函数的声明的参数的cv-qualifier吗?显然是的,否则参数的const和不加const难道不是代表两个函数吗?或者说overloaded吗?
  4. 我觉得我可能真的是木瓜脑袋完全不开窍!问题根本就是反过来的嘛!比如到底我理解不理解所谓的top cv qualifier的意义呢?根本就是两回事!看这个clang和类似msvc的解释就明白了!不是模板的declaration或者Iinstantiation出错了,而是实例化的时候const被丢弃了!就是说问题正好是相反!我简直是不理解这些文档在说什么! 我越来越糊涂了,这里到底在说什么?如果说函数的签名不需要参数的const,那么怎么判断函数是否重载呢?这个要怎么理解呢?难道说函数指针的类型和参数是否有const无关,但是函数重载仍然需要?这个也根本解释不通啊? 我觉得我头晕脑涨要出去呼吸一下新鲜空气了。
  5. 难道和这个有关系吗?在cp-tree.def里看到的。。。
    /* A type designated by `typename T::t'. TYPE_CONTEXT is `T', TYPE_NAME is an IDENTIFIER_NODE for `t'. If the type was named via template-id, TYPENAME_TYPE_FULLNAME will hold the TEMPLATE_ID_EXPR. TREE_TYPE is always NULL. */
    问题是在typename之前的const是否被丢掉了? 这里有一个观点是这样子的:就是说typename后面要紧跟所谓的qualified name,在我理解就是通常的scoped name。如果是这样子的话那么const只能放在typename之前了。所以,这个是没有选择的。我现在越来越感到这个问题的复杂,我开始迷茫这个问题到底是在parse的时候就出现了问题还是说在specialization的时候呢?或者问题的根源究竟是什么?

七月十二日 等待变化等待机会

  1. 总之,我是陷入了混乱之中。这里的一个细节让我百思不得其解,如果一个TREE_CODE(t)==TYPE_DECL的话,而且DECL_ORIGINAL_TYPE (t)==nullptr并且DECL_ARTIFICIAL (t) && !DECL_SELF_REFERENCE_P (t) == true那么打印它的方法是针对TREE_TYPE(t)通过TYPE_QUALS获得是否为const。 这里在使用TYPE_IDENTIFIER这个宏我遇到了困难现在看来这个问题解决了,就是下面的这个原因tree_code_type被优化了,使用TYPE_IDENTIFIER就可以直接获得类型字符串。但是我在gdb里打印不出来,也许是被优化了? 但是目前似乎这里就是我不解的地方,我的typedef T arr[3];为什么类型最后变成了T而不是T[3]或者T*之类呢?
  2. 宏TREE_CODE_CLASS用到这个数组tree_code_type但是这个数组在哪里定义的呢?而且在gdb里不能打印它的内容估计是编译优化掉了,下一次要把它包含到debug编译里。它的定义是
    const enum tree_code_class tree_code_type[] = {
    #include "all-tree.def"
    };
    可是源代码里是找不到这个文件的,它是编译时临时生成的,只有在Makefile里看到它是这么生成的
    
    s-alltree: Makefile
            rm -f tmp-all-tree.def
            echo '#include "tree.def"' > tmp-all-tree.def
            echo 'END_OF_BASE_TREE_CODES' >> tmp-all-tree.def
            echo '#include "c-family/c-common.def"' >> tmp-all-tree.def
            ltf="$(lang_tree_files)"; for f in $$ltf; do \
              echo "#include \"$$f\""; \
            done | sed 's|$(srcdir)/||' >> tmp-all-tree.def
            $(SHELL) $(srcdir)/../move-if-change tmp-all-tree.def all-tree.def
            $(STAMP) s-alltree
    
    而其中的这个变量lang_tree_files是这么定义的,看得出c++就只有cp-tree.def是有用的。
    lang_tree_files= ../../gcc-10.2.0/gcc/ada/gcc-interface/ada-tree.def ../../gcc-10.2.0/gcc/cp/cp-tree.def ../../gcc-10.2.0/gcc/d/d-tree.def ../../gcc-10.2.0/gcc/objc/objc-tree.def
    

七月十三日 等待变化等待机会

  1. 代码里和语法定义有着几乎一一对应的关系,所以要理解代码必须要从语法理解开始。目前我需要理解这样两个语法概念: decl-specifier-seqtype-specifier-seq
    1. 我们以我们的模板参数为例,这里我意识到如果从模板语法起步战线拉的就太长了,因为我目前可以先从简单入手事实上我不得不从函数的参数入手 所以,只能从函数的参数入手
    2. 
      parameter-declaration-clause:
      	parameter-declaration-listopt ...opt
      	parameter-declaration-list , ... 
      
      很明显我们选择第一式。
    3. 
      parameter-declaration-list:
      	parameter-declaration
      	parameter-declaration-list , parameter-declaration 
       
      鉴于没有逗号,,我们选择第一式
    4. 
      parameter-declaration:
      	attribute-specifier-seqopt decl-specifier-seq declarator
      	attribute-specifier-seqopt decl-specifier-seq declarator = initializer-clause
      	attribute-specifier-seqopt decl-specifier-seq abstract-declaratoropt
      	attribute-specifier-seqopt decl-specifier-seq abstract-declaratoropt = initializer-clause 
       
      同样的我们选择第原因是我们不需要declarator它是一个指针或者什么的声明形式。式,而且attribute-specifier-seq不再是可选项了。
    5. 这里首先看attribute-specifier-seq
      
      attribute-specifier-seq:
      	attribute-specifier-seqopt attribute-specifier 
       
      注意到它是左手边递归的,难道它是要求parser从右至左的解析顺序吗?这个是说明了优先级还是修饰的从属关系?总之在BNF语法里要注意左右递归的不同。
    6. 
      attribute-specifier:
      	[ [ attribute-using-prefixopt attribute-list ] ]
      	alignment-specifier 
        
      看到这里我意识到我们不需要这个atribute-specifier,因为我们没有[[]],而第二个aligment-speicifier是alignas关键字开头。我理解错误,这个 attribute-specifier-seq解析失败,回头。没有失败,因为这个本来就是opt,所以是可选的,为空而已。
    7. 回过头来继续看decl-specifier-seq
      
      decl-specifier-seq:
      	decl-specifier attribute-specifier-seqopt
      	decl-specifier decl-specifier-seq
       
      这个部分很关键,因为代码里我已经看到了相关的函数!首先一眼看到排除第一式因为我们没有attribute-specifier-seq,所以选择第因为递归需要终结者!那么这个终结就是decl-specifier它后面的attribute-specifier-seq是可选的!式。两个都要!首先是递归所以是第二式,而递归结束需要终结者所以是第一式。
    8. 
      decl-specifier:
      	storage-class-specifier
      	defining-type-specifier
      	function-specifier
      	friend
      	typedef
      	constexpr
      	consteval
      	constinit
      	inline 
       
      这里我们必须选择defining-type-specifier原因是清楚的,除掉后面的关键字和我们的const不符合以外,function-specifier指的是explicit或者virtual之类的。类似的对于storage-class-specifier还是有概念的,都是static/extern/mutable之类的。
    9. 
      defining-type-specifier:
      	type-specifier
      	class-specifier
      	enum-specifier 
       
      很明显的我们选择type-specifier
    10. 
      type-specifier:
      	simple-type-specifier
      	elaborated-type-specifier
      	typename-specifier
      	cv-qualifier
       
      这里我们首先选择了cv-qualifier但是它的上一级是递归的,接下来一轮我们选择了typename-specifier 所以,直到现在我们才第一次遇到了语法树的一个叶子因为我们终于有了第一个匹配constcv-qualifier
      
      cv-qualifier:
      	const
      	volatile 
      
    11. 我们现在进入了type-specifier type-specifier-seq的递归匹配中,下一个type-specifier我们要匹配 typename-specifier
      
      typename-specifier:
      	typename nested-name-specifier identifier
      	typename nested-name-specifier templateopt simple-template-id 
       
      这里我一开始被simple-template-id所诱惑,因为我们毕竟是一个模板类的成员类型A<T>::arr,但是如果看到simple-template-id结尾就是模板变量而已就知道我们不能选它,而是要选择第一式!
    12. 首先要明白什么是nested-name-specifier
      
      nested-name-specifier:
      	::
      	type-name ::
      	namespace-name ::
      	decltype-specifier ::
      	nested-name-specifier identifier ::
      	nested-name-specifier templateopt simple-template-id :: 
       
      原来如此,它实际上就是平常说的所谓的qualified name或者是scoped identifier之类的,这里我们选择哪一个呢?散步回来我觉得是type-name
      
      type-name:
      	class-name
      	enum-name
      	typedef-name 
       
    13. 这里的class-name
      
      class-name:
      	identifier
      	simple-template-id 
      
      这就对了,但是要选择simple-template-id于此对应的是A<T>::
      
      simple-template-id:
      	template-name < template-argument-listopt >>
      
      这里很明显的template-name就是A,而template-argument-list对应于gcc里经常出现的那个TREE_VEC就是T
    14. 现在终于可以回到之前的identifier,也就是const typename A<T>::arr
    15. 所以,到此为止我们有了一个完整的parsing的过程,以此来跟踪代码看看吧?
  2. 所以结合以上的语法分析,对应的gcc/cp函数是decl.c:grokdeclarator这个函数的参数cp_decl_specifier_seq *declspecs 这个struct cp_decl_specifier_seq对应的成员是这样子的
    1. location_t locations[ds_last];这个就是描述各种所谓的declaration sepecifiers比如ds_const这个enum cp_decl_spec是对应于const等等。这个是我要重点debug的地方,之前我在gdb已经打印觉得不太正确,接下来要确认这一点!
    2. tree type;注解里说它就是一个TYPE,我之前打印这个没有问题,因为它的类型的确是TYPE_DECL因为在模板类A里成员arr的确是typedef来的。
    3. 我在gdb里对于函数grokdeclarator传入的参数打印warning(0, "grokdeclarator:declspecs: %T", declspecs->type); 结果看到很奇怪的现象,这个函数被呼叫了至少四次这个不奇怪,因为它是一个比较通用的函数是针对所有的声明类型的处理。:在第一二次被调用的时候,这个type的打印结果是T
      
      ./1001.cpp:2:20: warning: grokdeclarator:declspecs: T
          2 |     typedef T arr[3];
            |             ^
      
      而它的类型:TREE_CODE(declspecs->type)结果是TYPE_DECL
    4. 在第三四次调用的时候,它的type是 而类型也变了:TREE_CODE(declspecs->type) 结果是TYPENAME_TYPE,我注意到这里没有const,检查locations结果: declspecs->locations[ds_const]是非零的!所以,这里parsing的结果是正确的,那么怎么转化为模板instantiation的过程可能是关键吧?
    5. 在接下来的int type_quals = get_type_quals (declspecs);代码返回结果是正确的,的确读取了const信息。毫无疑问gcc的parsing代码都没有问题,其中计算type_quals是一个非常复杂的过程,关于所谓的variant的一大堆我看不懂的代码最后也还是保留了const的属性。
    6. 接下来的这个调用过程是正确的:

      cp_parser_explicit_specialization ->cp_parser_single_declaration ->cp_parser_init_declarator ->cp_parser_declarator ->cp_parser_direct_declarator ->cp_parser_parameter_declaration_clause ->cp_parser_parameter_declaration_list ->grokdeclarator

      这里的结果是完全正确的 这里打印的aka在error.c里应该是strip_typedefs的结果,大概是把TREE_LIST用TREE_CHAIN做遍历,对于每一个TREE_VALUE针对不同的类型的TREE_TYPE做调整,这些类型包含POINTER_TYPE,REFERENCE_TYPE,等等几乎所有类型都包含。这里针对TYPENAME_TYPE的做法是TYPENAME_TYPE_FULLNAME。。。非常的复杂我看不下去了。。。 总而言之这里的const依旧保存着。也就是说模板实例化得到的参数是正确的。
    7. 这个过程太复杂了我散散步再说吧。

七月十四日 等待变化等待机会

  1. 我不确定这个函数cp_parser_single_declaration采用的是否是过时的语法?因为很明显的从其注解的语法是和我看到的最新的语法有出入。在cp_parser_decl_specifier_seq我看到了另一层的parser,是所谓的cp_parser,我一开始误以为是我之前看到的cpp_parser,对于其结果cp_token需要使用这个函数cp_lexer_print_token来输出,其实它还是依赖于最根本的cpp_token

七月十五日 等待变化等待机会

  1. 在陷入混乱以后拔出泥潭的最好方法是重新审视走过来的脚步。所以,我再次回到原点重头来过。 找到这个cp-tree.def里关于template-id的定义很有帮助。
    /* A template-id, like foo<int>. The first operand is the template. The second is NULL if there are no explicit arguments, or a TREE_VEC of arguments. The template will be a FUNCTION_DECL, TEMPLATE_DECL, or an OVERLOAD. If the template-id refers to a member template, the template may be an IDENTIFIER_NODE. */

    DEFTREECODE (TEMPLATE_ID_EXPR, "template_id_expr", tcc_expression, 2)

    我目前看到的是这样子的:
    1. determine_specialization这个函数的两个传入参数是template_id(TREE_CODE:TEMPLATE_ID_EXPR)和decl(TREE_CODE:FUNCTION_DECL)
    2. template_id不论是expression(%E)还是以declaration(%D)打印的结果都是f<int>
    3. decl以function declaration(%F)形式打印是void f(const int*)换句话说就是指的模板explicit specialization的形式,这个我称之为实例化是正确的。所以关键是和模板函数做比对看这个声明是否能够应对模板的某个实例化的形式。
    4. 从这个template_id得到的函数的overload,即fns=TREE_OPERAND (template_id, 0);得到的fns(TREE_CODE:OVERLOAD)以declaration(%D)打印得到:f(typename A<T>::arr)其中的参数的cv-qualifier也就是const丢失了!
    5. 其结果就是从这个fns通过lkp_iterator这个iterator得到的模板声明fn(TREE_CODE:TEMPLATE_DECL)自然也是不对的了,比如不论是按照function declaration(%F)还是按照type(%T)打印结果都是
    6. 由此fn_arg_types = TYPE_ARG_TYPES (TREE_TYPE (fn));而得到的模板函数的参数fn_arg_types(TREE_CODE:TREE_LIST)也是不对的了。比如按照function argument list(%A)打印的结果是(typename A<T>::arr)同样的丢失了const
    7. 但是令人诧异的是这个fn_arg_types并没有被直接使用,而是被丢弃了,它唯一的用途是用来检验在成员函数的情况下的成员函数本身的cv-qualifier是否一致,否则就跳过,我的理解就是一个快速的检验成员函数是否是const注意不是参数,而是成员函数本身! 注解里提到
      The return type, the parameter-type-list, the ref-qualifier, and the cv-qualifier-seq, but not the default arguments or the exception specification, are part of the function type.
      就是标准里的关于函数的标准定义
      D1 ( parameter-declaration-clause ) cv-qualifier-seqopt
         ref-qualifieropt noexcept-specifieropt attribute-specifier-seqopt
      也就是说函数的签名包含了D1parameter-declaration-clause cv-qualifier-seqoptref-qualifieropt。 注意这里没有提到 attribute-specifier-seqopt,这个是和函数签名无关的,它是所谓的
      
      attribute-specifier-seq:
      	attribute-specifier-seqopt attribute-specifier 	
      attribute-specifier:
      	[ [ attribute-using-prefixopt attribute-list ] ]
      	alignment-specifier 
      alignment-specifier:
      	alignas ( type-id ...opt )
      	alignas ( constant-expression ...opt ) 	
      attribute-using-prefix:
      	using attribute-namespace : 	
      
    8. 注意到这个函数参数列表被放弃了,转而使用从模板声明fn以及函数声明decl的所谓的get_bindings来获得所谓的参数targs,然后再用这个参数做所谓的替换tsubst,这个过程是如此的复杂纷繁,中间有很多重的递归,因为要解决的任务非常多,不仅仅要解决模板参数,还有模板模板参数以及他们的explicit/implicit specialization或者default argument等等,然后是函数参数的一系列替换,我前几天跟踪这个函数都要吐血了因为它有非常深的调用,直到最后我才意识到一开始传入的函数参数里压根就没有const不论你怎么替换也不可能重新冒出一个const,因为不管这个过程最终如何算法逻辑就是以你的这个参数列表和函数声明decl的参数来比较,也就是说在compparms这个参数比较的函数里看到的两个参数是想差一个const: 函数声明decl里的参数decl_arg_types按照argument-list(%A)打印(const int*),而从模板声明template_id里辗转得到的参数fn_arg_types按照argument-list(%A)打印得到的是(int*)。所以,算法找不到匹配。这个就是直接的结果。
    现在我们进一步的明确了现象,然后重新寻找原因吧

七月十八日 等待变化等待机会

  1. 俗事缠身休整了两天。回过头来看parser的过程其实看起来是比较简单的,就是在cpp lexer的基础上的高一级的cp级的parser比如这个cp_lexer_peek_token返回的所谓的cp_token能够让你知道是什么样的c++语法中的不同token类型。cp_parser这个包含了更高一级的所谓的context的信息,这是一个很复杂的体系,我也不想陷入这个gcc的实现的泥潭里,因为历史原因,gcc要维持多个语言的前端的兼容,而这个简直是一个不可能的任务,因为首先各种语言的五花八门,如果c和c++,objectiveC还有一点点关联性的话,那么Ada,Fortune之类的就完全不同了,当然不同语言使用不同的parser本身就是隔绝的谈不上互相干扰,但是从代码的规模来看是巨大的。这里可能主要的还是c,c++,objectC共同使用的部分的复杂性吧?我瞎猜的。总而言之,这个是一个很大的负担,也许clang是做了很多人想要而做不到的一件事,难道不能使用纯粹的C++的语言来实现一个c++的parser吗?boost开了一个好头,可是核心的是语法树的产生的根本原因是为了产生目标代码服务的,如果不产生目标代码压根不一定需要AST,因为仅仅实现一个语法验证器是无意义的,原因是c++并非CFG它是需要上下文解析某些ambiguity,那么要达到这个目的只能在实际产生目标代码的某些过程才能发现吧?这是我的理解,也许有些是在语法解析过程能够发现但这个也是在为了产生目标码的服务的过程,两个紧密相联。所以,没有人去做这个无意义的尝试,只有前端的parser几乎是无意义的。
  2. 我想记录下我看到的场景。这个再次提醒一下我们的代码是
    1. 其实看到c++parser的代码就很容易理解了,比如当使用cp_lexer_peek_token看到当前是一个template的keyword的时候,使用所谓的cp_lexer_peek_nth_tokenpeek再下一个token以及再下一个,如果看到的是<>,这个就说明是模板的specialization,于是就进入到了cp_parser_explicit_specialization,这里是非常的形象,要lookahead两个token才能决定,但是这个也不是最主要的,关键是同样的模板也许在不同的上下文也许语法是不同的吧?我这个仅仅是为了帮助理解context-sensitive的例子,就是说parser还有所谓的一个成员变量来描述上下文,这个才导致语法解析的复杂之处。这个是由语法本身的歧义性造成的和实现无关,哪怕是clang也会遇到同样的问题,所以,这里是实现不能回避的困难。
    2. 注解里说explicit-specialization 这里我还不能找到语法的佐证。
      
      explicit-specialization:
           template < > declaration
      
      这个实际上就是
      
      explicit-specialization:
      	template < > decl-specifier [opt] init-declarator [opt] ;
      	template < > function-definition
      	template < > explicit-specialization
      	template < > template-declaration  
      
      注意到这里template < > explicit-specialization的一个递归对应了代码里的cp_parser_explicit_specialization函数也是包含递归的,所以,这些代码如果不结合语法定义是看不懂的!
    3. 进入到函数cp_parser_explicit_specialization是依靠所谓的peek函数,等于是lookahead并没有consume这些token,这也就是为什么进入到函数以后调用
      
      cp_parser_require_keyword (parser, RID_TEMPLATE, RT_TEMPLATE);
      cp_parser_require (parser, CPP_LESS, RT_LESS);  
      cp_parser_require (parser, CPP_GREATER, RT_GREATER);
      
      的原因了因为他们是确认并且consume这些expected token了。
    4. 接下去比较复杂的情况是我们又看到一个template这样子的keyword,我对于这个语法定义不是很清楚,这里不但有可能进入递归还有其他的可能性吧?上面的这个语法挺复杂的。但是所幸的是我们不在这个分支里,因为我们是简单的模板的特殊化直接进入了cp_parser_single_declaration
    5. 下面很明显的是进入到了语法分支function-definition
    但是这个是我两天前在黑暗中摸索的过程,我现在已经有点明白了我看到的问题不在于declaration或者说函数specitialization的instantiation,因为它的参数是正确的const int*,我看到的问题是出在模板声明的参数为什么是int*?不是吗?太平洋上的台风已经形成了,但是它的起源应该在数千英里以外的某只不知名的蝴蝶在无意识的煽动了几下翅膀造成的?总之我的头脑又一次的混乱了,我需要再次理清思路,还是要再次检查来的路。这个不是在原地打转,因为每次我都看清楚一些以前不知道的足迹,这个正好像人类认识世界的过程是一个反复螺旋上升的过程,每次的旋转setback并不是back to square one,而是在提高了一个维度的情况下看待原来的二维坐标,就是说我在二维空间的坐标是回到了起点,可是我先在是站在三维空间的第三维的更高的坐标上了,也就是螺旋上升的那个高度来俯视我之前的足迹。这就是学习的过程。 吃早饭以后再说吧?
  3. 我现在有了新的认识我应该在cp_parser_translation_unit开始因为我总共就是三个语句,我应该关注在,不过我印象中parsing是没有问题的应该是在cp_parser_explicit_specialization里解析模板声明的时候的问题吧?所以,我应该继续以上的分析。
  4. 午睡后延续早上的追踪。。。
    1. 所以,我回想起这个问题不是parsing本身,因为总共三个translation unit对应三条语句,第一个是模板类 第二条是模板函数的声明 第三条才是模板特殊化的声明 我们目前在这个语句的模板特殊化的声明部分。这里的cpp_ttype是libcpp里定义的预处理类型,我几乎已经忘记了,在parser里找了好久的定义找不到! 在早上我们看到parser明确当前是explicit specialization之后,发现template < >之后没有跟着template就可以确定我们只需要parse一个简单的cp_parser_single_declaration也就是语法里的一个巨大的分支declaration,而当parser进一步看到void时候怎么决定呢?这一部分的语法我不能找到完完全全的一一对应的算法,但是总的来说parsing所谓的什么specifier总是没有错吧?总之,自动机在这里排除了模板函数声明的可能,转而尝试模板函数,所以我们现在在函数cp_parser_init_declarator
    2. 我终于在这里又一次进到那个黑洞一般的函数的噩梦grokdeclarator。现在的问题是在进入这个黑洞之前我得到的所谓的decl_specifier到底是怎样的呢?这里我的一个朴素的感悟是start_function是解析函数的一个入口,这个感想是多么的朴素啊!而那个黑洞grokdeclarator会被很多函数所调用,因为顾名思义它就是语法里的declarator啊!
    3. 我觉得这里有必要补习语法关于最最基本的id-expression,因为我遇到这个declarator-id,它是我们最通常的所谓noptr-declarator
      
      declarator:
      	ptr-declarator
      	noptr-declarator parameters-and-qualifiers trailing-return-type 		
      noptr-declarator:
      	declarator-id attribute-specifier-seqopt
      	noptr-declarator parameters-and-qualifiers
      	noptr-declarator [ constant-expressionopt ] attribute-specifier-seqopt
      	( ptr-declarator )
      declarator-id:
      	...opt id-expression 
      
      至少这里有两点值得回忆,一个就是...后面跟的是id-expression,另一个是很多指针类型的声明是在挂号()里的 这里顺便熟悉一下ptr-operator 这里注意到ptr-declarator最终还是递归回到了noptr-declarator
      
      ptr-declarator:
      	noptr-declarator
      	ptr-operator ptr-declarator 
      
      而关于这个ptr-operator有很多的思考,它究竟是什么?
      
      ptr-operator:
      	* attribute-specifier-seqopt cv-qualifier-seqopt
      	& attribute-specifier-seqopt
      	&& attribute-specifier-seqopt
      	nested-name-specifier * attribute-specifier-seqopt cv-qualifier-seq
      
    这里我感觉头脑很混乱有必要明天一步一步的用语法来解析模板函数speicialization的全过程才能理解代码的走向。原因就是一个看似简单的declarator是如此深得语法树,让我都要吐了。
  5. 另外一个思路就是重新读标准里的关于模板函数特殊化的说明例子。第一关于function template specializations里的例子模板参数推导不适用我的问题,因为我的是explicit function template specialization,我这里开始感到糊涂我到底是要看explicit specialization还是explicit instantiation?应该是前者吧?后者是在调用,而我还是在声明阶段。这个所谓的explicit instantiation declaration和explicit instantiation definition是我以前很模糊的,甚至可能从来没有遇到过的吧?这个语法看上去是比较陌生的
    
    explicit-instantiation:
    	externopt template declaration 
    
    我遇到的问题是specialization因为在template后有模板的空参数<>,这个是原则性的区别,所以不要把自己搞糊涂了。
  6. 先从模板函数的定义出发吧 A function template defines an unbounded set of related functions. 而不要小看这个定义,我居然没有这个overloading的概念,因为以下的代码让我感到震惊 你可以这样做,这个是合法的,可是这个不是推翻了使用模板的全部意义吗?你对于这样子的overloading有意义吗?你明明就是在无所不包的模板参数里包含了各种各样的overloading了何必要再来一个呢?你不能直接使用specialization达到所谓的overloading的效果吗?定义两个本质上一样的模板函数不是浪费编译器的工作吗?也许这个就是c++语法设计之初不允许禁止程序员自杀的初衷吧?也许很多语言设计之初就瞄准了特定的使用场景根本就不浪费在无意义的语法场景,把程序员的胡思乱想限制掉了,可是c++没有,也不允许。这个是所谓的绝对的自由吗?而且这个例子还强调说Such specializations are distinct functions and do not violate the one-definition rule. 我对于这两个几乎相同的模板函数分别进行specialization 然后如果我们直接调用的话它是否优先匹配specialization呢?这个答案是必然的,这个是基本的常识啊。
    
    int number=10;
    int*pNumber=&number;
    my(number);
    my(&pNumber);
    
    结果是这样子的 假如我们去除第二个specialization,那么函数my(&pNumber);要匹配谁呢?答案是它匹配参数是指针的那个,这个看上去似乎不出意外,只是这里的倾向性似乎很难界定吧?
  7. 现在翻看一个月以前的笔记完全想不起来,看来我必须记录尽可能多的细节以至于可以做到通过记录完全复原。
  8. 这里是关于explicit specialization的定义。 我还是不明白怎么使用explicit instantiation,尤其是对于模板函数来说它究竟有什么用呢?在下面这个例子里explicit template instantiation完全没有什么用处,而且不能出现在specialization的前面。 对于这个f(0);调用的结果是什么呢? 结果是普通函数胜过了specialization或者是模板函数,即void f(int)

七月十九日 等待变化等待机会

  1. 暂时的多积累一些语法基础知识,很多的语法名词在代码里都有出现,我如果能够提前对它们有一定认识的话,代码的走向就容易理解的多了。
  2. 关于多重继承我只知道一个菱形继承的谬误,那都是当年为了应付面试的一鳞半爪,在模板类的环境下这个问题就复杂的多了,其中牵涉到的类的成员的访问在模板的实例化下是相当的复杂的。我略略修改为了说明另一个问题。 在以下的两个实例化中, 作者本来是为了说明第一个实例化会引发歧义的编译错误,而我想要发现的是第二个实例化究竟返回值是什么?实际上如果明白了第一个实例化的错误就直接能够回到第二个实例后的结果是A::m的值1,因为之所以编译成功就是在模板定义阶段就锁定了访问范围直看到A的成员变量m,因此哪怕你把C的继承顺序struct C:T,A{...};改变为struct C:A,T{...};结果也是一样的。 昨天我对于模板实例化的声明感到不解,不明白有什么用,现在我的理解就是模板的声明定义实例化是三个截然不同的阶段,它们面对的语法也是有着些微的差别的,就比如以上这个例子在模板定义阶段和实例化阶段模板的访问限制范围是不同的,这个导致的后果是显著的。
  3. 对于param-list要怎么打印呢?注意它是const typename A<T>::arr
    1. 我的情况就是这个参数只有一个也就是说
      TREE_CODE(TREE_VALUE(TREE_CHAIN(params))) ==> VOID_TYPE
    2. 那么第一个也是唯一的参数的类型是什么呢?
      TREE_CODE(TREE_TYPE(TREE_VALUE(params))) ==> TYPENAME_TYPE
    3. 查看error.c里关于TYPENAME_TYPE的打印方式先要输出qualifier,也就是
      TYPE_QUALS(TREE_TYPE(TREE_VALUE(params))) ==> 1
      说明它是const
    4. 我们需要知道它的TYPE_NAME究竟是什么类型:
      TREE_CODE(TYPE_NAME(TREE_TYPE(TREE_VALUE(params)))) ==> TYPE_DECL
    5. 首先看看它是不是原生的类型:
      DECL_ORIGINAL_TYPE(TYPE_NAME(TREE_TYPE(TREE_VALUE(params)))) ==> 0x0
      说明不能用普通类型来打印。
    6. 对于这个类型有三种可能,enum:
      TYPENAME_IS_ENUM_P(TREE_TYPE(TREE_VALUE(params))) ==> 0
      , 不是的。
    7. 是类吗?
      TYPENAME_IS_CLASS_P(TREE_TYPE(TREE_VALUE(params))) ==> 0
      , 也不是
    8. 那它只能是typename
    9. 对于typename我们需要得到它的context:
      (TYPE_CONTEXT(TREE_TYPE(TREE_VALUE(params)))) ==> RECORD_TYPE
    10. 我们已经知道它不是enum,也不是union,只能是class或者struct
    11. 如果
      TYPE_LANG_SPECIFIC(TYPE_CONTEXT(TREE_TYPE(TREE_VALUE(params)))) &&  CLASSTYPE_DECLARED_CLASS(TYPE_CONTEXT(TREE_TYPE(TREE_VALUE(params))))
      为真就是class否则就是struct
    12. 这里要输出context的qualifier,同样的使用
      TYPE_QUALS(TYPE_CONTEXT(TREE_TYPE(TREE_VALUE(params)))) ==> 0
      获得值和TYPE_QUAL_CONST,TYPE_QUAL_VOLATILE等等去与操作来输出,这里是context的qualifier,和typename之前定义的const是不同的,也许问题就在这个附近?
    13. 下一步我们需要判断它的TYPE_NAME的种种类型:
       TREE_CODE(TYPE_NAME(TYPE_CONTEXT(TREE_TYPE(TREE_VALUE(params))))) ==> TYPE_DECL
      ,这里有很复杂的判断得出它是模板类型,所以还是要输出模板参数。所以,这里使用的是
      CP_DECL_CONTEXT(TYPE_NAME(TYPE_CONTEXT(TREE_TYPE(TREE_VALUE(params))))
      ,它的TREE_CODE是NAMESPACE_DECL
    14. 这里有一大堆的判断我发现
      CP_DECL_CONTEXT(TYPE_NAME(TYPE_CONTEXT(TREE_TYPE(TREE_VALUE(params)))) == global_namespace
      ,所以没有namespace的scope的打印任务。
    15. 打印模板是一个很大的任务。首先,依然要得到
      DECL_NAME(TYPE_NAME(TYPE_CONTEXT(TREE_TYPE(TREE_VALUE(params))))
      , 它的TREE_CODE是IDENTIFIER_NODE,所以使用
      IDENTIFIER_POINTER(DECL_NAME(TYPE_NAME(TYPE_CONTEXT(TREE_TYPE(TREE_VALUE(params)))) ==> A
      就得到了结构的名称。
    16. 打印模板参数需要从context得到TYPE_TEMPLATE_INFO,然后从这个info再得到TI_ARGS
    17. 算了这个函数参数的打印实在是太复杂了,我只能依赖于warning来打印了。
  4. 总之parser对于模板声明的代码是没有问题的,在typename之前的const是存在的,至于结构体模板类本身所在的context的qualifier是不存在问题的,我误解了。我对于cp_parser_parameter_declaration_clause返回的参数打印需要一点点改变,就是直接使用argument-list的%A会crash,这个我不清楚什么原因,但是我只有一个参数所以就直接打印它的那个节点:
    warning(0, "TREE_TYPE(TREE_VALUE(params)) as T: %T", TREE_TYPE(TREE_VALUE(params)));
    结果是正确的const typename A<T>::arr 所以,问题还是出在特殊化的时候的模板函数参数怎么丢掉的问题。这个一点都不意外,如果这个地方出错那就是天大的问题,全世界都要跳楼了。相当的辛苦,但是也确实是能够亲手实践获得提高。当然参数是正确的不等于装配declarator正确,应该是很接近了。
  5. 今天偶然找到我以前购买的老片子《十二只猴子》,大多数人一开始看都觉得混乱不堪,莫名其妙,因为时空反复的穿梭和疯狂的人物和任务让几乎所有的观众都不会有什么好评。但是在今天大瘟疫流行的年代人们还是这么看待这个疯狂的主题吗?疯狂是这样子的:活到未来,梦游当下,死在过去。

七月二十日 等待变化等待机会

  1. debug的应该用什么单位来表示呢?你会用还有三千行代码来标识你距离你判断的故障源头的范围或者是表示你已经debug的工作量?我反反复复都在这个巨型的函数grokdeclarator里面打转转,每次都在这个四千行三千行代码的函数里面被饶晕过去,因为它也是被很多函数调用,比如参数的入口函数cp_parser_parameter_declaration_clause也会反复调用它,因为这个declarator是语法中如此频繁重要的元素,以至于我羞愧的承认我不理解它的真实含义!我发现盯着这些语法表达式很有帮助,似乎渐渐的它们开始慢慢的进入我的脑子里,但是时间长了可能我就睡着了,或者头晕了。需要出去走走。
  2. 假如一个t的TREE_CODE是POINTER_TYPE,那么我不要去看这个t的TYPE_QUALS的值,而是要看它对应的TREE_TYPE的TYPE_QUALS值,也就是说const int*里的const修饰的是数据类型int而不是指针*本身,这个是大多数c程序员比较容易理解的。但是从语法的角度来看我始终对于int const*和int*const的区别搞不清楚。
  3. 我能不能大体上确定一个范围呢?我感到蹊跷的是这里的一个变化
    
    #3  0x00000000009fdbf0 in grokdeclarator (declarator=0x36ca7b0, declspecs=0x7fffffffda50, 
        decl_context=NORMAL, initialized=1, attrlist=0x7fffffffd858)
        at ../../gcc-10.2.0/gcc/cp/decl.c:13574
    #4  0x0000000000a0484b in start_function (declspecs=0x7fffffffda50, declarator=0x36ca8d0, 
        attrs=0x0) at ../../gcc-10.2.0/gcc/cp/decl.c:16493
    
    这个declarator的地址都变了,肯定在这个四千行三千行的代码函数里发生了某种变化,具体的变化大概就在这个范围:在13574-10905=2669行。也许玄机就在这两千多行的代码里。另一点要注意的就是这个declarator当我们做specialization的时候是使用它的所谓的unqualified-id,就是当declarator的kind是id的时候的declarator->u.id.unqualified_name。这一点让我感到比较的困惑我印象中在解析模板函数过程中得到的declarator是kind作为function的,为什么摇身一变成了id呢?

七月二十一日 等待变化等待机会

  1. 我觉得我的脑子终于有点开窍了,所谓的cp_declarator这个数据结构非常的容易理解,它就是一个链表,可以包容所有类型的declarator所以它也是一个union,理解注意到了它仅仅是一个中间结构也就是说最终它被转化成tree的存储结构,也就是说我依然还没有掌握这个寻找全局变量的思路,究竟怎么在各个translation_unit之间寻找匹配函数呢?这个机制我还是没有掌握。难道说tree本身就有机制存储链表吗?
    我只能理解它是一个巨大的union,那么难道全局变量在每个tree的链表里?不可能。
    
    type = union tree_node {
        tree_base base;
        tree_typed typed;
        tree_common common;
        tree_int_cst int_cst;
        tree_poly_int_cst poly_int_cst;
        tree_real_cst real_cst;
        tree_fixed_cst fixed_cst;
        tree_vector vector;
        tree_string string;
        tree_complex complex;
        tree_identifier identifier;
        tree_decl_minimal decl_minimal;
        tree_decl_common decl_common;
        tree_decl_with_rtl decl_with_rtl;
        tree_decl_non_common decl_non_common;
        tree_parm_decl parm_decl;
        tree_decl_with_vis decl_with_vis;
        tree_var_decl var_decl;
        tree_field_decl field_decl;
        tree_label_decl label_decl;
        tree_result_decl result_decl;
        tree_const_decl const_decl;
        tree_type_decl type_decl;
        tree_function_decl function_decl;
        tree_translation_unit_decl translation_unit_decl;
        tree_type_common type_common;
        tree_type_with_lang_specific type_with_lang_specific;
        tree_type_non_common type_non_common;
        tree_list list;
        tree_vec vec;
        tree_exp exp;
        tree_ssa_name ssa_name;
        tree_block block;
        tree_binfo binfo;
        tree_statement_list stmt_list;
        tree_constructor constructor;
        tree_omp_clause omp_clause;
        tree_optimization_option optimization;
        tree_target_option target_option;
    } *
    
    tree当然就是链表啊!问题是在parser中保存了这个链表头吗?
  2. 其实我应该要对于parser的基本逻辑有一个了解才能阅读代码。
    1. 实际上如果是在c++20以前来看translation_unit和declaration几乎就是等价的,直到c++20引进了module才使得这个层级多了不少module相关的部分。
    2. 首先最高一层的循环就是届些一个个的所谓的cp_parser_toplevel_declaration,因为c/c++的语法就是一个个的以;结尾的语句,当然这里也是cp_parser_translation_unit文件结尾的跳出点。这一层的上面就是c_parse_file,在之前的cpp实践中我已经看到过了。
    3. 我的感觉是declaration部分的语法是经历了逐渐的演变的,很多以前的框架束缚了后来者的增加部分,比如cp_parser_declaration这个函数的无数个if/else让人看的目眩,而它的dispatch函数却并没有一一对照语法表达式,很显然的是从前的语法并没有后来这么复杂,比如deduction-guide这种后来者是没有位置的这个例子并不恰当,因为我感觉我并不熟悉它的语法,实际上它是裹藏在模板分支里的,它本身并不是顶级的语法入口,所以没有必要在低级派遣中单独列出。不过我的意思原则上是对的吧?都是些大实话的唠叨。。而让人感到不快的是object-C的独特语法也掺杂在这里,这里集中体现了gcc最让人诟病的一些特征,把c++和object-C以及GNU自己的extension都大杂烩在代码,而随着c++标准的演进后来人都只在这个基础上修修补补,而最容易通过的修补方式就是考古式的修补方式,也就是模仿前人的风格尽量小的添加自己的代码并且以把自己的代码彻底融入原来的历史为最高原则,这个经年累月的沉积层等到足够厚实以后对于后来者也就成了新的历史沉积导致后来者花几倍的力气来做同样类似的添加工作,这个称之为软件开发的一个类似沉积岩的规律,所有的程序员都知道的现象,但是面对这个庞然大物没有程序员敢凭一己之力来对抗它,这才有了后来的clang的出现。不接受现实就只有出走另立山头。
    4. 在gcc的迷宫里摸索,语法书彷佛是你的指路地图,而核心的数据结构tree则是你留下的面包屑,因为你探索了进入迷宫的路,要能够安全的出迷宫自己手头没有设立一些标识是困难的,而在gdb里依靠tree的记录的信息才能让你知道你处于哪一层迷宫。
    5. 我以前一直不知道c++20是如何支持module语法的,我总以为那个打开支持module的宏之后会放开如潮水般涌出的一大堆有关module的语法代码,现在才明白在语法层面是没有人这么做的,语法代码里已经蕴含了它的语法关系,比如这个关键字export,就是这个情况,gcc的代码里已经在10.2里把这个export-declaration包含了,这个也就是我一开始对于在模板部分检查export关键字的困惑所在。这个也从侧面说明了顶级的派遣函数没有按照语法一一对应看上去减少了几个判断语句在古代是效率追求的导向,可是在如今未必是好主意,因为随着语法的演绎这种修改也许越来越麻烦。
    6. 在本世纪初的美好年代人们对于c++带来的革命还在消化阶段,也许很多人认为模仿生物进化的类的继承就足以描述整个MATRIX,也许大多数人根本没有想到c++语言还能有这么大的发展演绎空间,而那个时代编译器之间的比拼的指标是更快更兼容,为了能够在同类中脱颖而出,尽可能多的兼容古代的语言和使用尽可能小的内存和最大限度提高单个文件语法解析速度是生存的唯一指标。毕竟在那个年代很多大公司的一个很大的野心就是垄断编译器垄断计算机操作系统垄断计算机架构。于是乎,那个时代的烙印深深的打印在gcc的沉积岩代码里了。
    7. 也许不久的将来在大学计算机专业课里会出现一门选修课叫做《计算机考古学》,它并不是指借助于计算机技术来从事考古发掘工作,而是计算机自身的软件硬件的发展历史。我想如果有这样一门课的话,软件历史里gcc是绝对绕不开的一个章节,多少历史的沉积啊。
    8. 我今天的目的就是追寻函数的调用来对照语法表达式来探索迷宫的走向,但是最主要的目的是要分析面包屑撒在了什么地方,也就是说各个函数调用之后具体怎样把语法解析的结果存储起来的部分,尤其要留心我心目中期待的链表的头在哪里。

七月二十二日 等待变化等待机会

  1. 早晨起来在清凉的晨风里翻看李伟的那本模板宝典,那是一两个月前刚开了一个头的重温计划。其实哪怕看一两句话都是受益的。比如他说现实中函数重载是依靠类型来判断的,可是如果你的类型是无法分辨也想要重载那怎么办呢?通常这个是函数内部通过参数的运行期的值的不同而选择不同分支的代码,可是这个在模板元编程里使用enable_if就做到了重载相同函数签名的不同值的函数。他举了类似这个例子 这个样子的函数重载在现实编程中的应用我还一时想不出来,但是这个应该是元编程的一把利刃吧?对于元编程来说enable_if是大家司空见惯的物件,我的感慨是有些莫名,只不过我是想从函数重载能够依据参数的值做到而不是函数类型不同达到的视角出发而感慨的。那么我想做一个简单的测试,但是卡在如何在编译期产生随机数的老问题上了,于是我看到了这个比较好的解答,同样的问题大概一个月以前我google到的答案让我很不满意也许是没有耐心看明白吧?总之目前我接受这个理由和解决方案,保存一下。他的思想就是应用编译期的两个宏来产生某种随机组合,这个主意我可能上一次也看到过,但是应该是没有理解,或者还期待使用类似mt19937的高级的实现吧?
    
    #define RNG_SEED (((__TIME__[7] - '0') * 1  + (__TIME__[6] - '0') * 10  + \
                  (__TIME__[4] - '0') * 60   + (__TIME__[3] - '0') * 600 + \
                  (__TIME__[1] - '0') * 3600 + (__TIME__[0] - '0') * 36000) + \
                  (__LINE__ * 100000))
    
    这个想法我现在比较能够接受,就是说我仅仅要产生一个简单的编译期随机来做一个简单的测试,并不考虑加密解密等等的高大上,所以,够用就好。
  2. 另一方面就是我认为我这几天的debug通过gdb认识了一些gcc的代码绝对是有收益的,比如我之前对于模板特殊化始终无法把握,现在就清楚多了,利用default参数就是一个很典型的特殊化,因为只要是参数个数的不同应该就能够达到这个目的吧?另一个比较大的体会就是我以前对于SFINAE的实现机制感到很神秘,现在看来就很容易理解了,比如在模板特殊化过程中要从模板的template_id所代表的一系列声明里来寻找匹配的函数签名的过程就是我目前在debug的过程,这里对于模板实例化不成立代码是不会报错的,就是所谓的静悄悄的舍弃,除非最后没有找到任何匹配才报错,这里需要深入了解的是所谓的模板特殊化内部的所谓的partial order决定了什么样子才是更加的特殊,至少前几天我们已经知道了gcc会首先匹配非模板的普通函数,那么在不同的模板特例的排序就是我今后要注意留心的地方。

七月二十三日 等待变化等待机会

  1. 《三体》里为什么总是把人类的命运交给程心第一次她作为一个不合格的执剑人被三体人看穿直接击破了威慑战略导致地球人类被三体人奴役甚至屠杀,如果不是《蓝色空间号》奇迹般的广播三体星系坐标实现黑暗森林威慑人类几乎就灭绝了,即便如此已经造成了成百万上千万的人类的直接死亡。第二次在唯一能够挽救人类免遭降维打击的曲率光速飞船的研发时又一次大发圣母婊的妇人之仁让人类放弃了唯一的逃脱文明灭亡的机会。最后在全人类难逃灭亡命运之际她又一次无耻的乘坐秘密建造的唯一一艘曲率光速飞船逃出生天。即便是在她第一次作为不合格的执剑人葬送人类生存机会之后也是被三体人作为功臣而免于灭亡的命运。这个全人类的文明的最大罪人却每每被奉为圣母般的博爱偶像在欧美大陆执掌各个国家的命运。然后又招致这么多的怨恨?女子岂应关大计,英雄无奈怨红颜

七月二十四日 等待变化等待机会

  1. 有一度我天真的以为gcc没有什么全局变量是少有的干净,真的好幼稚啊。你看不见就代表不存在,天真是因为你的视野狭窄而已,换言之,井底之蛙都是天真的。
  2. 我对于gcc如何描述函数一无所知直到看到这个名副其实的function的数据结构觉得怎么这么直截了当呢?还有一个有意思的是为了防止后来的程序员直接对结构的成员访问设定,这里是一个简单的技巧就是把指针从lvalue变成rvalue,比如
    
    extern GTY(()) struct function *cfun;
    
    声明了一个全局变量的指针,紧接着就再用一个宏把它变成非lvalue:#define cfun (cfun + 0),这样子就逼迫程序员必须使用push_cfun or set_cfun之类的宏了。这个可真的是用心良苦啊!在遥远的古代有这么多的痛苦。顺便说一下我大概是看到了RVO的实现机制吧,就是
    
    /* Nonzero if function being compiled needs to be given an address
     where the value should be stored.  */
    unsigned int returns_struct : 1;
    
    可以想像很多看似很高级的功能在实现层面也不过就是如此,以前听说RVO是某种优化,而且需要比较新的版本的编译器才支持,看来的确是这样子的,也许就是最后化为一个比特的标志量而已,当然这个是在parsing阶段应该就要解决吧?
  3. 我刻意留心全局变量,显然的cp_parser不是一个非常巨大的存储的结构,我对于它是否能够储存所有c++的全局变量表示怀疑,那么我们先来看看这几个我感觉越来越重要的概念了。
    1. 首先是scope
      
        /* The scope in which names should be looked up.  If NULL_TREE, then
           we look up names in the scope that is currently open in the
           source program.  If non-NULL, this is either a TYPE or
           NAMESPACE_DECL for the scope in which we should look.  It can
           also be ERROR_MARK, when we've parsed a bogus scope.
      
           This value is not cleared automatically after a id is looked
           up, so we must be careful to clear it before starting a new look
           up sequence.  (If it is not cleared, then `X::Y' followed by `Z'
           will look up `Z' in the scope of `X', rather than the current
           scope.)  Unfortunately, it is difficult to tell when name lookup
           is complete, because we sometimes peek at a token, look it up,
           and then decide not to consume it.   */
        tree scope;
      
      这里揭示了变量定义的查找机制。我怀疑using就是给它提示的。
    2. 接下来的这两个的object_scope我不太理解,似乎这个是一个类似于栈的不停的变化的量?也许我的理解有问题,解释是很清楚,parser需要这个作为所谓的上下文继续解析当前的语句?
      
        /* OBJECT_SCOPE and QUALIFYING_SCOPE give the scopes in which the
           last lookup took place.  OBJECT_SCOPE is used if an expression
           like "x->y" or "x.y" was used; it gives the type of "*x" or "x",
           respectively.  QUALIFYING_SCOPE is used for an expression of the
           form "X::Y"; it refers to X.  */
        tree object_scope;
        tree qualifying_scope;
      
    3. 就是说在编译器在一开始是无法判断到底是声明还是表达式,所以要尝试一下?
      
      /* A stack of parsing contexts.  All but the bottom entry on the
      	 stack will be tentative contexts.
      
      	 We parse tentatively in order to determine which construct is in
      	 use in some situations.  For example, in order to determine
      	 whether a statement is an expression-statement or a
      	 declaration-statement we parse it tentatively as a
      	 declaration-statement.  If that fails, we then reparse the same
      	 token stream as an expression-statement.  */
      cp_parser_context *context;
      
      让我们回顾一下表达式的语法
      
      statement:
      	labeled-statement
      	attribute-specifier-seqopt expression-statement
      	attribute-specifier-seqopt compound-statement
      	attribute-specifier-seqopt selection-statement
      	attribute-specifier-seqopt iteration-statement
      	attribute-specifier-seqopt jump-statement
      	declaration-statement
      	attribute-specifier-seqopt try-block 
      
      这里的expression-statementdeclaration-statement究竟为什么会引起歧义需要尝试性的解析呢? A declaration statement introduces one or more new names into a block;
  4. 对于《头条》上引录的这篇奇文我想摘录下来。可是图片对于中文OCR我试了两三家都遇到一个奇怪的现象,就是第一二列被当作了,而第三四列被当作了第二段,这个也许是作者故意做的手脚吧?那么摆在我面前的就是这么一个小小的任务:把输入文件in1和in2的每一行合并到输出文件,比如
    
    ifstream in1("/tmp/word1-1.txt"), in2("/tmp/word1-2.txt");
    ofstream out("/tmp/word1.txt");
    
    很简单啊!
    没想到我居然啃吃啃吃如此的吃力,真的让人汗颜啊!
    1. 首先的首先,这个是纯粹中文而且是utf8编码的。所以要imbue一下。
      
      in1.imbue(std::locale("zh_CN.UTF8"));
      in2.imbue(std::locale("zh_CN.UTF8"));
      out.imbue(std::locale("zh_CN.UTF8"));
      
    2. 其次,我不能使用getline我主要喜欢这个输出是string,而不是我自己设定buffer,因为设定buffer的大小始终是一个头疼的地方,看下文,我实验了才发现我从ocr转回的文字没有换行符。
    3. 没问题,那么我使用get好了,那么读多少个character呢?这个是一行的原文 乾坤有序,宇宙无疆,数一下吧有几个字符?中文编码是三个字符一个啊,不对吗?我使用midnightcommander来数也是啊。所以,
      
      char buffer1[36], buffer2[36];
      while (in1.get(buffer1, 30) && in2.get(buffer2, 30))
      
      结果我总是发现最后的那个逗号()总是漏掉,然后再看文档才发现读的字符是30-1=29!
      basic_istream& get( char_type* s, std::streamsize count );
      ... that is, reads at most count-1 characters and stores them into character string pointed to by s
      ...
      所以上面的30应该改成31! 唉,我一直想避免自己设定这些读取的字符数,结果还是在这上面栽跟头了。
      
      while (in1.get(buffer1, 31) && in2.get(buffer2, 31)){
      	string str1(buffer1, 30), str2(buffer2, 30);
      	out<<(str1+str2)<<endl;
      }
      
  5. 后来我发现我真的是浪费时间!新浪里有老老实实的文字版
  6. 我找到一位同道也在检查重复的字。但是我发现我下载的版本和他的有出入,而且新浪博客的版本被加密打不开了,我无从考据了。我又找到了豆瓣上的版本可是我怎么数都数不到四千字啊?只有第一个版本是正确的版本,是四千字而且的确是20个重复的字但是我都查过它们都是多义字,甚至于作为外文翻译的字也勉强算吧,比如脱氧颗粒籽
  7. 那位使用R语言检验的仁兄也不错,不过我把我的输入文件这个不知道是否有侵犯中华字经的版权呢?不过我想作者既然是大学教授他们也是没有版权的。净化了一下。
    1. 首先,我假定我的文本是纯粹的utf8中文包括逗号和句号,空白字符都事先被去除了。那么我怎么描述我的中文字呢?
      
      struct Word{
      	char m_utf8[3]={};
      	Word(){}
      	constexpr Word(const char utf8[3]){
      		m_utf8[0]=utf8[0];
      		m_utf8[1]=utf8[1];
      		m_utf8[2]=utf8[2];
      	}
      	auto operator <=>(const Word& other)const{
      		if (auto cmp=m_utf8[0]<=>other.m_utf8[0]; cmp!=0){
      			return cmp;
      		}else if (auto cmp=m_utf8[1]<=>other.m_utf8[1]; cmp!=0){
      			return cmp;
      		}else{
      			return m_utf8[2]<=>other.m_utf8[2];
      		}
      	}
      	bool operator ==(const Word&other)const{
      		return *this<=>other ==0;
      	}
      };
      
      这里我总算是学习使用了一下新玩意spaceship operator,我一开始对于怎么应用它很不理解,后来大概明白了一点,我实现了这个操作符对于set/map之类的运用less/greater就足够了。
    2. 比如我使用一个set来作为字典来检查是否有重复的汉字。
      
      set<Word> s;
      ifstream in("/tmp/chinese.txt");
      Word word;
      const char CommaAry[3]={'\xEF','\xBC','\x8C'}, PeriodAry[3]={'\xE3','\x80','\x82'};
      const Word Comma(CommaAry), Period(PeriodAry);
      int counter=0;
      while (in.get(word.m_utf8, 4)){
      	if (s.contains(word)){
      		if ((Comma<=>word)!=0 && (Period<=>word)!=0){
      			string str(word.m_utf8, 3);
      			cout<<"repeating..."<<str<<endl;
      			counter++;
      		}
      	}else{
      		s.insert(word);
      	}
      }
      cout<<"repeating total:"<<counter<<endl;
      
      这里我定义的逗号Comma和句号Period作为常量来比较,当前的字,但是这里如果我直接使用==或者!=编译器不接受,这个让我感到很困惑,也许是有什么bug?总之我只能再次使用spaceship的值来判断,这个是很奇怪的。也就是如果我的代码使用
      
      if ((Comma!=word)&&(Period!=word)){..
      
      那么我必须要另外定义这个成员函数
      	
      bool operator ==(const Word&other){
      	return *this<=>other ==0;
      }
      
      这个值得我以后调查原因。
    3. 这里我得到的20个重复的汉字是藏长行率调传核乐弹陆朝校圈膀重参畜咽厦曾

七月二十五日 等待变化等待机会

  1. 昨天的故事是如果我把普通数组换成了array的话,我就没有单独实现一个操作符==或者!=的烦恼了,因为array是标准库里实现了spaceship operator的数据类型之一:
    
    struct Word{
    	array<char,3> m_utf8;
    	Word(){}
    	constexpr Word(const array<char,3>& ary):m_utf8{ary}{}
    	constexpr Word(const char utf8[3]){
    		m_utf8=std::experimental::make_array(utf8[0], utf8[1], utf8[2]);
    	}
    	auto operator<=>(const Word& other)const =default;		
    };
    const char A[3]={'a','b','c'};
    constexpr Word a(A), b({'a','b','c'});
    static_assert(a==b);
    
    1. 这里我不得不添加#include <experimental/array>因为这个make_array是一个定义在experimental名字空间的小函数,看它的实现也是一种让人心潮澎湃的激动。一开始我对于这个实现猜想没有什么了不起的,因为我本来没有打算用它,本来嘛我的代码直接就是这么写的 constexpr Word(const char utf8[3]):m_utf8{utf8[0],utf8[1],utf8[2]}{},你说这个make_array能有多大花头?何况它难道真的从效率的角度讲就能够减少几条指令啊?我这里已经就是一个array的ctor形式了说不定编译器看在constexpr的份上直接就优化了,难道你这个make_array能够做的更好?但是在看了它的实现代码之后我还是感到敬佩,并不一定就是说这个效率有多么的提高这一点我不敢肯定也许没有,但是这里的实现是经典的元编程的范例,这里要解决的关键实际上是所谓的common_type的问题,因为作为make_array的多个参数如何转化是否能够转化是取决于你给定的目的地array的类型参数的,所以这个是要提前计算的!怎么计算呢?我以前对于common_type这个类型计算工具感到有些无聊,现在才体会到它的重要。而对于这个样本实现和我看到的实际实现代码的差别的微妙之处我现在还难以领会。但是感觉有很多的学习之处比如一个简单的对于paramter pack样式的参数类型要求他们都不能是引用类型的判断使用了!__or_或者像参考代码std::conjunction,这些对于我来说都还是挺高级的。
    2. 那么怎么知道什么是已经在标准库实现的了呢?这里给出了列表: 几乎标准库里的大多数容器和数据类型都做了重载,我的理解就是他们的ordering得到了明确,这里strong_ordering/weak_ordering等等的概念我要进一步加强。
    3. 我对于c++20/17引进的这个default comparison认识不深,没有意识到它背后的巨大的更新,至少在我看来是巨大的,以前看到同事和我自己写那些无聊的一行代码式的成员函数就为了实现类直接的比较实在是要烦死了,如今编译器把你解放了!这里产生的代码不仅仅是成员函数还包括全局的操作符重载以及参数是我的理解就是rvalue的临时变量?不是吗?它的意义不仅仅是简单的实现自定义类的比较排序,更直接的是每次你要使用set/map之类排序相关的容器你被要求实现这些无聊的代码,即便是用lambda或者是functor也是繁琐,经常有编译的时候很莫名其妙的说这个是const不行之类的烦人的事情,往往这类编译错误耗费很久才明白是一个简单的const!
    4. 至少我对于c++20的这个宇宙飞船还理解不够,昨天的错误我还是没有找到答案,虽然我找到了正确的做法。
  2. 你是否如我一样的感到无语! 我期待的是array<char,3>可是编译器给你的是一个数组的指针的array!怎么办?这个是你偷懒在声明array不给出具体模板参数让编译器推理造成的,我在想实现一个小的helper来做到。。。这里我略施法术 然后这里我就实现了这么一个简单的make_array_helper能够正确的把数组类型转换了,这里我还没有想清楚怎么安全的确保是数组就只能这么暴力执法使用static_assert了。 这个是使用的场景,只不过数组作为函数参数传递的时候失去了它的本性被退化成了指针导致我的模板参数类型推导无法认出它是数组类型,我尝试使用什么forward也不行,也许是用反了?总之我只能使用模板参数来明确指示模板函数类型 我觉得这个还是很贴心的,不是吗?能不能把这个加入标准库呢?不过牵扯到使用experimental让人感到希望不大了。实际上我心目中的make_array就是这样子接受一个数组作参数然后返回array,不是吗?如果让我把一个extent_v为100的数组转化为一个array我要怎么办?难道手写一百个元素吗?也许我可以写一篇paper提个建议?不过这个要specialization模板函数make_array可能有些困难吧?我能够特例化数组类型吗?比如make_array<T []>()吗?我累了回头再想吧?

七月二十六日 等待变化等待机会

  1. 被打断了两天并非没有收获只不过是完全不同的领域,还是回到模板函数声明的解析过程中来吧。再次跟踪gdb对照语法书,有一点要注意,我的观念里总以为decl-specifier是有包括const实际上是有的!我为什么这样说呢?因为decl-specifier ==> defining-type-specifier ==> type-specifier ==> cv-qualifier ==> const 但是这个是在cp_parser_type_specifier才能看到,所以,这个就是我目前的跟踪要点。相对应的typename的出现也是相似的: ... ==> type-specifier ==> typename-specifier 然后它后面必须完成这两个nested-name-specifieridentifier 那么代码里的A<T>::对应的是type-name :: ==> class-name ==> simple-template-id ==> template-name <template-argument-listopt>
  2. cp_parser_elaborated_type_specifier我就跟丢了,因为这个太复杂了,单单语法就是很深的递归,我只有先明确了语法才能比较轻松的跟踪,这个是我这阶段的经验。
  3. 看以前的笔记我觉得这个绝对是gcc的一个缺陷,如果不算是bug的话。而且我的感觉它也许和我目前探索的问题是很接近的。我感觉这个函数可能有问题。 的确是有些问题,现在看起来是这样子的:在解析模板函数过程中,在已经解析了函数的参数以及各种specific之类的这时调用start_function中间对于declarator做了某种改动然后再返回调用start_preparsed_function。这里的逻辑我不是很清楚但是观察到的现象就是这中间在调用那个四千行三千行代码的庞然大物grokdeclarator的时候返回的declarator的参数已经失去了const。在这之前的传入的参数是一个cp_declarator,当它的kindcdk_function的时候,我使用%T来打印这个type: TREE_TYPE(TREE_VALUE(declarator->u.function.parameters))结果是const typename A<T>::arr,这个看起来是正确的,然而grokdeclarator返回的是一个函数声明我使用%F打印的结果却是void f(typename A<T>::arr),所以,可能出错的地方就是在这个四千行三千行代码的函数里。我仿佛是在走夜路鬼打墙,饶了一圈又一圈还是饶回来在这个老问题上。最终还是回避不了这个庞然大物。不过不能说我是原地打转,我至少现在是有很大提高的,我明白了在explicit specialization的之前模板函数的声明就有问题,所以,不是在模板实例化匹配的问题,而是模板函数声明在正确的解析参数之后在这个很独特的小环节里出了一些状况,从这个参数命名为decl1的相对粗糙来看应该是后来的人打得补丁,因为原本的函数start_function里面分出来的代码让我怀疑这个是因为某种原因被改过了吧?总之,经过了几个星期我依然还有四千行三千行代码的路途。不过我相信我现在和之前遇到的这个函数不是在同一个维度的,因为之前我还是稀里糊涂的在这个函数里面被缠绕到解析函数参数的大递归里不能自拔,现在应该是比较清晰的一个路径了。这个跋涉的历程实在是太困难了,走夜路鬼打墙兜圈子的原因就是你在漆黑的旷野里没有指路的明灯误打误撞,如今我有了语法书来做地图配合很多函数的语法注解多多少少能够明白很多了,而且对于打印各种tree的类型的熟悉让我更容易的理解各个语法对象在tree里的存储形式,这都是很大的进步,至少在我看来是的。明天在来过吧。

七月二十七日 等待变化等待机会

  1. 这个就是我前几天下载并整理了一下的《中华字经》文本。我为了处理方便去除了所有的空白字符包括换行符,所以,是准确的四千个汉字加上一千个标点符号,按照utf8中文编码正好是1500个字节。其中,汉字有二十个重复但是我的检验可以看作是同形不同音或者不同义吧?当然有一个我自己也觉得勉强的就是糖核酸在外文里作为外来字是否就有了不同的含义呢?
  2. 这个是一个常识
    中国国家标准总局1980年发布了《信息交换用汉字编码字符集》,1981年5月1日开始实施的一套国家标准,标准号是GB 2312—1980,简称GB2312。
    GB2312标准共收入汉字6763个和非汉字图形字符682个。整个字符集分成94个区,每区有94个位。每个区位上只有一个字符,因此可用所在的区和位来对汉字进行编码,称为区位码。
    对于人名、古汉语等方面出现的罕用字,GB 2312不能处理,这导致了后来GBK及GB 18030汉字字符集的出现。
    GBK全称《汉字内码扩展规范》(GBK即“国标”、“扩展”汉语拼音的第一个字母,英文名称:Chinese Internal Code Specification) ,中华人民共和国全国信息技术标准化技术委员会1995年12月1日制订,国家技术监督局标准化司、电子工业部科技与质量监督司1995年12月15日联合以技监标函1995 229号文件的形式,将它确定为技术规范指导性文件。
    GBK编码,是在GB2312标准基础上的内码扩展规范,使用了双字节编码方案,其编码范围从8140至FEFE(剔除xx7F),共23940个码位,共收录了21003个汉字,完全兼容GB2312标准。
  3. 我下载了不少的免费字库,怎么查看他们呢?默认的gnome-font-manager是针对拼音文字的预览因为没有几个字母需要查看,可是中文却不同,只有fontforge才能查看,我还是很钦佩的,因为它要自带所有的中文大概包含cjk中日韩的所有的glyphon吧?我出于好奇下载源码来瞅了一眼。
  4. 我已经不止一次的说过大概还有四千行三千行代码的距离了,可是前几次也许没有意识到这个巨型函数是很多递归函数的入口所以继续跟踪下去又进入了很多的螺旋,根本不止这么一个函数,所以是四千行三千行代码的好多倍。这一次呢?
  5. 我又一次在这个大迷宫里丢掉了自我,让我想起了以前的一部电影,内容也忘记了只记得片名《Lost in Translation》,其实电影的剧情我当年也没有看懂,想到它纯粹是片名,因为代码如果超过一千行就会很难控制走向,这个似乎是一个铁律,你怎么可能把这么复杂的逻辑包含在一个函数里呢?这个本来想想其实是正常的,因为一个函数的确包含了这么复杂的逻辑,但是关键的是不应该把具体的逻辑都平铺直叙的写下来,应该把你要写的表达浓缩成一个个步骤,这才是函数的初衷。它的过程肯定是这么复杂再怎么改写也不可能减少哪怕一行代码甚至更多,但是重新归纳之后应该让人明白它具体在做什么。我在敲击了几十上百下回车符之后的收获就是又回到了那个老问题:这是一个bug吗?在我们传递的declarator的参数在这个小函数grokparms里处理时候,程序员明确的遵循了c++标准里的After producing the list of parameter types, any top-level cv-qualifiers modifying a parameter type are deleted when forming the function type.要怎么理解呢?我现在有一个新的理解我以为函数的类型是一回事,它实际的参数是独立于它的,也就是说这个note说的很重要,我以前看过很多遍但是依然不能把握其中的真谛。为什么说This transformation does not affect the types of the parameters.强调这一点的用意在哪里?难道这个不是显而易见的吗?那么这个例子的理解更加的重要,我以前没有看懂,这是多么让人汗颜的事实啊,写了这么多年的函数第一次对于什么是重载开始疑惑:
    1. 我们声明了一个参数是char*的函数void f(char*);
    2. 那么对于参数是数组char[]的函数实现void f(char[]) {}当然是合法的,这个是天经地义的,但是为什么呢?你能说两个类型是一样的吗? 比如static_assert(!is_same_v<char[], char*>);,这个说明什么?如果不是因为以上的指引把参数类型从数组改成了指针怎么能够说明它是声明的实现呢?这个是要意识到的,我以前从来没有想过,通常的理解都是从类型的可转换来想的,这个也许是可能的,但是真正的原因是上面的标准
    3. 你再次定义个参数是加了const的指针const char*,这个实现void f(const char*) {} 是对于原来声明的重载,为什么?让我们大声朗读关于函数overload的定义吧:
      Overload resolution is a mechanism for selecting the best function to call given a list of expressions that are to be the arguments of the call and a set of candidate functions that can be called based on the context of the call.
      The selection criteria for the best function are the number of arguments, how well the arguments match the parameter-type-list of the candidate function, how well (for non-static member functions) the object matches the implicit object parameter, and certain other properties of the candidate function.
      这里就看出了要害,我们是把expression的实参来和函数的所谓型参来比较,那么之前说了函数的型参列表是要去除top level cv-qualifier的,那么这个是resolution而我的问题是要回到overload的定义的本源来看。
      Each of two or more entities with the same name in the same scope, which must be functions or function templates, is commonly called an “overload”.
      这里只是说了结果如果同时存在同名同的实体那么他必定是重载,可是它们为什么能够存在而不违反ODR呢?我要追寻一下关于函数的ODR是什么,现在要去买菜了。
    4. 重新理解函数。 首先,函数不是什么:
      Functions are not objects: there are no arrays of functions and functions cannot be passed by value or returned from other functions. Pointers and references to functions are allowed, and may be used where functions themselves cannot.
      不存在函数数组!我们平常用到的难道是函数指针数组?函数的引用我用过吗?
      那么函数是什么呢?
      Each function has a type, which consists of the function's return type, the types of all parameters (after array-to-pointer and function-to-pointer transformations, see parameter list) , whether the function is noexcept or not (since C++17), and, for non-static member functions, cv-qualification and ref-qualification (since C++11). Function types also have language linkage. There are no cv-qualified function types (not to be confused with the types of cv-qualified functions such as int f() const; or functions returning cv-qualified types, such as std::string const f();). Any cv-qualifier is ignored if it is added to an alias for a function type.
      这里是我们熟悉的函数重载的定义,没毛病啊!
      Multiple functions in the same scope may have the same name, as long as their parameter lists and, for non-static member functions, cv/ref (since C++11)-qualifications are different. This is known as function overloading. Function declarations that differ only in the return type and the noexcept specification (since C++17) cannot be overloaded.
      所以我的理解是函数的类型并不等同于它的参数就应该被纯化,因为计算函数类型的这个transformation过程并不影响函数的参数原本的类型,否则怎么可能又重载呢?这个是两个不同的概念吧。
    5. 我们在回头来看看函数参数的列表定义吧:
      Parameter list determines the arguments that can be specified when the function is called. It is a comma-separated list of parameter declarations, each of which has the following syntax :
      attr(optional) decl-specifier-seq declarator (1) Declares a named (formal) parameter. For the meanings of decl-specifier-seq and declarator, see declarations.
      int f(int a, int *p, int (*(*x)(double))[3]);
      attr(optional) decl-specifier-seq declarator = initializer (2) Declares a named (formal) parameter with a default value.
      int f(int a = 7, int *p = nullptr, int (*(*x)(double))[3] = nullptr);
      attr(optional) decl-specifier-seq abstract-declarator(optional) (3) Declares an unnamed parameter
      int f(int, int *, int (*(*)(double))[3]);
      attr(optional) decl-specifier-seq abstract-declarator(optional) = initializer (4) Declares an unnamed parameter with a default value
      int f(int = 7, int * = nullptr, int (*(*)(double))[3] = nullptr);
      void (5) Indicates that the function takes no parameters, it is the exact synonym for an empty parameter list: int f(void); and int f(); declare the same function. Note that the type void (possibly cv-qualified) cannot be used in a parameter list otherwise: int f(void, int); and int f(const void); are errors (although derived types, such as void* can be used). In a template, only non-dependent void type can be used (a function taking a single parameter of type T does not become a no-parameter function if instantiated with T = void).
      这里的很多东西我都是似曾相识的,因为这些天的gdb整天都在和它们打交道,现在是系统的整理思路的时机了。只有第5个的单个模板函数参数为void的时候我有些疑惑,需要做一下实验。
    6. 在重新查阅declaration我有些豁然开朗的意思,这个要归功于之前研习语法的过程,现在看这些就很清晰了,然后我也有一个自己的结论,对于const typename some-dependent-type的语法中这里的const不应该归结于top-level cv-qualifier,因为它是由declaration解决的参数类型,只要看看语法就明白它们是这个some-dependent-type的从属的type-specifiers的一部分啊!这个就是我的理由,我相信这个也是clang/msVS给出的结果的原因。
    7. 这里给出的算法应该是有权威性的吧?虽然基本是标准的引用,但是其中有些微的差异:
      The type of each function parameter in the parameter list is determined according to the following rules:
      1. First, decl-specifier-seq and the declarator are combined as in any declaration to determine the type.
      2. If the type is "array of T" or "array of unknown bound of T", it is replaced by the type "pointer to T"
      3. If the type is a function type F, it is replaced by the type "pointer to F"
      4. Top-level cv-qualifiers are dropped from the parameter type (This adjustment only affects the function type, but doesn't modify the property of the parameter: int f(const int p, decltype(p)*); and int f(int, const int*); declare the same function)
      看到没有,cppreference特意强调了这个Top-level cv-qualifiers are dropped from the parameter type的行为并不会改变参数本身的属性This adjustment only affects the function type, but doesn't modify the property of the parameter,我能够相信这个是权威的解读吗?就是说函数本身的类型虽然取决于参数但是是被净化的参数列表,但是当函数类型和它的真实的参数列表合起来以后才是作为函数重载的依据? 首先这两个函数的类型就是不同的,这个要怎么解释static_assert(!is_same_v<void(const char*), void(char*)>);,你就不能说这两个函数的参数去掉top-level cv qualifier就一样了,这个const根本就不能算什么top level cv qualifier,那么什么情况下才是top level呢?
    8. 我觉得我要好好体会这个declarator因为这些日子总是和它打交道但是真的理解它吗?相当的复杂。算了明天再看吧,我本来以为我已经比较清楚路线图了,结果发现又回到了定义的原点,甚至于我都开始怀疑这个到底是标准定义的模糊还是实现的错误的大原则问题上了。
    今天忙忙碌碌了一整天感觉是更加的混乱了。很多概念在反反复复的纠缠,解不开理还乱!
  6. 我终于知道了一点什么是top level const的概念了,这一点非常重要,否则我就还在黑暗中徘徊。 这里让我细细研读一下:
    A top-level const qualifier affects the object itself. Others are only relevant with pointers and references. They do not make the object const, and only prevent modification through a path using the pointer or reference.
    这个概念在c++标准中似乎并没有很清晰的定义
    For a type cv T, the top-level cv-qualifiers of that type are those denoted by cv.
    但是接下来的例子反而让人糊涂了
    The type corresponding to the type-id const int& has no top-level cv-qualifiers. The type corresponding to the type-id volatile int * const has the top-level cv-qualifier const. For a class type C, the type corresponding to the type-id void (C​::​* volatile)(int) const has the top-level cv-qualifier volatile.
  7. 终于还是google强大找到了这个比较专业的定义:top-Level-cv-Qualifiers-in-Function-Parameters.pdf明天再好好读一下! 这里的简单的规则就是
    例子代码const是否为top-level
    T *const p;
    T const *volatile q;the top-level cv-qualifier is volatile
    T const volatile *q;has no top-level cv-qualifiers. In this case, the cv-qualifiers const and volatile appear at the second level.
    int f(char const *p);the const qualifier is not at the top level in the parameter declaration, so it is part of the function's signature.
    int f(char *const p);the const qualifier is at the top level, so it is not part of the function’s signature. This function has the same signature as: int f(char *p);
    这个可能是今天最大的收获!

七月二十八日 等待变化等待机会

  1. 早上起来研读了一下昨天下载的这篇文章,感觉真的是胜读十年书啊!
    1. 这个top level cv qualifier是一个真真切切的实际的问题,绝对不是什么坐在象牙塔里的咬文嚼字的学究想出来的形而上学的问题。
    2. 它的根源很深也很实际,其中还牵涉到一些被很多人忽略的c与c++比较微妙的差别的地方,所以,要深刻理解问题的来龙去脉就不得不回顾c的做法,这个也是c++背负的一个十字架,因为c++继承自c就要兼容c,但是c语言是一个追求更加贴切硬件效率的语言,对于很多的处理追求的是简单,比如函数签名问题,c的处理很简单就是任何参数的cv qualifier都是照单全收的,为什么?因为c不支持重载啊,所以,没有问题!
    3. 要理解上面的结论必须回到函数传参数的方式来说才能理解,对于c语言来说就是传地址和传值两种,c++增加了传引用实际上也是传地址的包装包裹了const而已,而所有的传递方式都可以增加const这类qualifier。这些都是老生常谈尽人皆知的常识,但是对于传值是否添加const究竟对于调用者有任何区别吗?比如函数void f(int);void f(const int);显然从调用者来说是没有区别,尽管对于函数定义本身来说参数声明为const不允许本地改变是有区别的,可是从调用者来看反正都是一个拷贝,函数定义一方改变与否与几何干?这个在c语言看起来根本就不是一个问题,可以归结于个人的编程风格,有的人就是为了强调个人的理念就喜欢强调传递的这个参数原则上是不能被函数修改的,至于说本身传递的就是一个拷贝修改了对自己也无妨倒是其次,意思是传达到了而已。但是这个对于c++就不行了,为什么?因为c++支持函数重载,c语言把参数的类型无条件的接受导致函数签名不同以至于以上是两个不同的函数,那么c++按照重载的原则就允许它们重载,可是这样子的重载不是毫无意义吗?所以,c++主张这类无意义的重载不应该发生!于是这就是问题的源头。
    4. c++怎么样防止这类无意义的重载发生呢?就在于计算函数签名的时候要剔除这类影响,所以,这才有了top level cv qualifier的概念的重要性。那么我们先要复习一下最基本的概念就是在传值的过程中加不加const是无意义的,在没有指针和引用的情况下怎么加和加不加const都是耍流氓。所以,它们这种针对非指针非引用的cv qualifier都被叫做top level,因为它们修饰的就是对象本身。
    5. 其实,这个完全是倒叙,我们应该先明确有多少层的level采来讨论是不是top level,可是我想一下子就抓住为什么要这么做反而没有提及是什么这个基本问题,就是说没有指针都是一层,所以只要是有const自然就是top level,那么有了指针就分两层了,指针本身才是变量名代表的物件,而指针指向的类型反而是第二层了,因为应该头脑明确在传指针的函数里指针本身也是被当着传值来看待的,从这个意义来看char*const就明白这个const为什么被叫做top level了。要真的理解这个必须重温这两个概念: 对于指针变量的const放在类型前面还是后面是等价的,完全是习惯使然static_assert(is_same_v<const char *, char const *>); 只有当const是放在*后面修饰指针变量本身的时候才是所谓的top level。而它放在指针符号*之前任何位置都是用来修饰指针指向的数据类型的。比如这个就是例证static_assert(is_same_v<const struct D, struct D const>);,明白了这两点就理解了我们对于普通的指针变量的top level const实际上是强调我们传递的这个指针地址是不能改变的,这个和传递整数加上多余的const是一个道理,指针本身就是一个地址,你传进去一个整型数字无论加不加const都是无关痛痒的,作为c++重载是要避免的。而另一方面,这个指针变量如果const是修饰指针指向的数据类型的话,不管是const char*还是char const *都是对于调用者有很大不同的,换言之,是要作为重载的依据成为函数签名的一部分的!这个就是真正的内在逻辑。
    6. 在这里我对于参数是函数指针的情况发生了疑惑,如果函数指针就是一个指针变量的话依照刚才的原则,它的top level是怎么样子的呢?另一个问题就是函数并非对象object它不能被返回,作为参数传递的时候必须改为指针,那么是否还存在所谓的top level cv qualifier呢?我的感觉是这是个伪命题,函数可以看作就是一个地址是不可改变的,所以,不存在什么const不const,我的证明如下,我们的函数是一个返回void的名字take_fp的函数,它的参数是一个函数指针,这个指针指向的函数返回值为int,参数是char,所以定义如下: void take_pf(int(*pf)(char)){},那么我们现在能够对这个函数重载些什么呢?把函数指针改为const可以吗?首先我们做这样的声明两个编译器是很高兴的接受了
      
      void take_pf(int(* const pf)(char)); // declaration #1
      void take_pf(int(* pf)(char));  // declaration #2
      void take_pf(int(*pf)(char)){}	// definition of both #1,#2 which are identical
      
      为什么?因为在编译器看来#1和#2是相同的声明,对于它们两个声明的实现是一样的,如果你把#1,#2两个声明再实现就会在链接出错说重复实现。也就是说这里的参数* const pf)(char)有没有const编译器都会把它拿掉因为它是top level cv qualifier,它修饰的是一个传值的参数不值得为它而重载。也就是作为函数take_pf的参数的类型没有实质的改变。那么作为函数指针它的组成部分还包括了它自身的返回值和参数,这个当然是会影响到它作为参数的角色了,也就是说这个是合法的重载 void take_pf(const int(*pf)(char)){}这里虽然说函数的返回值是否加const作为整数是无关痛痒的,但是标准没有像针对参数那样规定拿掉不需要的const,导致函数的参数类型不同了因为作为参数的函数指针的签名是包含它自己的返回值类型的,现在我们改变了它的返回值类型自然作为参数来看就改变了,因为函数的签名是作为take_pf的参数。不过不用担心因为c++有另一个规定函数返回值的不同不能作为重载的依据我脑子有点乱了
    7. 最后要指出的是c和c++的微妙的不同,对于这样两个在同一scope的声明void f(const int);void f(int);在声明阶段c编译器是不能接受的因为c语言不允许重载,但是对于c++编译器声明是无害的因为它们两个本质上是相同的声明,重复声明不是罪,所以是合法的,但是如果你真的要把它们当作重载来实现c++就在链接阶段抓住你重复实现了。从原理来看c编译器会认为它们是不同的函数签名如果不是函数名字一样编译器是允许它们的不同实现的,只不过不允许重载而拦下来了,假如我们写的是动态库分别把它们放在不同的动态库里,在c编译链接看到的是不同的函数签名应该很高兴的实现了重载,可是c++编译器看到的是相同的函数签名应该就是碰运气看不同的动态库的查找顺序吧?这个是我的想像还没有实际证明过。以后再说吧,太累了。
  2. 这里的题外话是我想要定义个函数类型就是它的参数是一个指向函数的指针要怎么写它的类型呢?注意不是变量的定义仅仅是函数的签名而已。回顾这个函数定义,就应该是这样子的:首先如果它没有返回值,我们就写void,然后写挂号(),然后在里面填写它的参数类型,这里一个指向函数的指针类型是这样子的void(*)(),它没有返回值并且没有参数,它是一个指针,所以完整的函数签名是 void(void(*)()),这里我折腾了好久因为习惯上总是定义指向函数的指针类型总忘不了在函数参数前面再加一对(),结果编译器认为你定义的是一个返回一个函数类型的函数而这个是非法的。你可以返回函数的指针或者引用探视不能返回函数本身,对于数组也是一样。
  3. 这里还是要记录一下这个关于函数如果返回一个函数指针是如何定义的,这个实在是太丑陋了,我以前也学习过随后就忘了因为实在是太无理了。 这个是依据栈的方式来解析的,最外层是返回函数指针的签名,最内层才是我们定义的函数名称,比如我们的函数返回值是一个函数指针,它指向的函数返回值是int,参数是char,那么它的类型就是int(*)(char),如果我们函数名字叫做ret_f的话,并且没有参数,就是这样子声明的int (*ret_f())(char);看到没有高亮的部分才是返回值,这个写法实在是太难看了,只有当年的c编译器作者才能想出这么稀奇古怪的东西。
  4. 当年大师Dan Saks的文章我应该抽空好好读一下。但是现在实在是太累了。
  5. 阅读大师的一篇《Mixing const with Type Names》很有教育意义的小品文非常令人震撼,我对于typedefdefine的区别简直一无所知,而这里让人震撼的地方好多,比如后者是在预编译里的宏替换,而后者则是编译器的类型定义,这里会有截然不同的效果,因为预编译的宏经常是噩梦一样的邪恶。比如在遇到指针变量的声明里
    
    #define defineType char*
    defineType d1,d2;
    static_assert(is_same_v<decltype(d1), char*>);
    static_assert(is_same_v<decltype(d2), char>);
    
    看到了吧程序员随手声明的两个指针变量,前者d1是不错的,可是后者却成了非指针,而这种错误有的时候是致命的。 而作为类型定义的typedef则不存在。
    
    typedef char* typedefType;
    typedefType t1,t2;
    static_assert(is_same_v<decltype(t1), char*>);
    static_assert(is_same_v<decltype(t2), char*>);
    
    而作为新时代的继承者using几乎和typedef是一回事。 这个也许还是小问题,可是和const打交道的问题才叫严重呢!这个是我从来没有遇到过的大问题,比如
    static_assert(is_same_v<const typedefType, char*const>);
    谁能想到我本来以为我声明的是const char*结果却是char*const,也就是说我本来想声明一个指向const char的指针,没想到声明了一个const指针指向char,这个是多么巨大的误区啊!然后大师讨论了一个有趣的做法以后写const的时候把它放在右边,比如
    static_assert(is_same_v<typedefType const, char*const>);
    这样子你在心里做宏替换的时候会很自然的把它在脑海里改成了char*const, 而且从右往左念出声来也对就是const pointer to char,这个方法真好!我决定把这篇小品文收藏在本地
  6. 大师的网站没有给出这个小品文《const T vs T const》的链接,我手动发现并下载了它其实wget -r之类的也可以自动做到的。这篇文章似乎是前面那篇的前作,但是读起来格外的有教育意义,因为我始终对于declaration/declarator的语法部分不明所以,现在似乎豁然开朗了。保存一个本地版本。 文中有很多有趣的地方,比如declarator我们现在知道有一类是所谓的ptr-declarator里的ptr-operator,这个部分是理解所有cv specifier的一个关键,换句话说const是和指针*连在一体的,即*const合在一起来修饰后面的declarator-id,通常它就是一个id。 一个有趣的问题
    How did I know that *x[N] is an “array of ... pointer to ...” rather than a “pointer to an array of ...?” It follows from this rule:

    The operators in a declarator group according to the same precedence as they do when they appear in an expression.

    For example, if you check the nearest precedence chart for either C or C++, you’ll see that [] has higher precedence than * . Thus the declarator *x[N] means that x is an array before it’s a pointer.

    而这一段话是很有教育意义的,大师说明了()的两个大的功能,而且我以为是c/c++里最重要的两个功能,可以说就是核心与灵魂:first, as the function call operator, and second, as grouping.

    Parentheses serve two roles in declarators: first, as the function call operator, and second, as grouping. As the function call operator, () have the same precedence as [] . As grouping, () have the highest precedence of all.

    而接下来的这个考题才是检验你是否真的明白了大师的讲解
    For example, *f(int) is a declarator specifying that f is a “function ... returning a pointer ... .” In contrast, (*f)(int) specifies that f is a “pointer to a function ... .”
    这里把declarator和declaration-specifier分开来是有帮助的,头脑中脑补一下,这个时候我觉得我需要一个例子来帮助我理解比如我们声明一个函数返回一个const指针指向int,它的参数是一个char,它是镇样子的int*const f(char);,那么f的类型是什么呢? static_assert(is_same_v<decltype(f), int*const(char)>); 这里我稍稍改变一下
    
    int*const fp(char){return nullptr;} // a function returning a const pointer pointing to int
    decltype(fp) *const g=&fp; //a const function pointer initialized to pointing to fp
    static_assert(is_same_v<decltype(g), int*const(*const)(char)>);
    
    这里我想表达的是这个g的const就不是一个所谓的top level cv qualifier,因为这里的这个const是它的declarator的一部分也就是int*const(*const)(char)当它作为参数传递的时候,它是不能被去除掉的。比如这个函数声明是这样子的void call_fp(decltype(fp)*const);,那么很明显的call_fp的类型在转化参数的时候是不能把那个const拿掉的我好像被打脸了,还不知道为什么?
    static_assert(is_same_v<decltype(call_fp), void(int*const(*const)(char))>);
    这个结果是我意想不到的,我的这些声明看起来是无差别的!
    
    void call_fpc(const decltype(fp)*);
    void call_fpc(decltype(fp) const*);
    void call_fpc(decltype(fp) *const);
    
    因为编译器根本就不在乎那个const 我觉得有些头晕要出去呼吸一下新鲜空气了。
  7. 欧洲的那些圣母婊左们和《三体》里的程心一样的地方就是都是高喊着以爱的名义来毁灭一个文明。

七月二十九日 等待变化等待机会

  1. 这个我需要反复记忆一下就是数组的指针的类型是什么样子的,引用实际上是类似的,只是它们和函数类型经常混在一起:
    
    int ary[3];
    static_assert(is_same_v<decltype(&ary), int(*)[3]>);
    decltype(ary)& ary_ref=ary;
    static_assert(is_same_v<decltype(ary_ref), int(&)[3]>);
    
  2. 经过了这么多天的努力,我今天早上终于觉得有点理解了我观察的现象:
    1. 首先我们要明确一个概念,就是函数的签名的的确确是遵循标准的要求进行数组到指针的转换的,换言之,这两个函数类型之所以相同是因为在编译器眼里数组类型的参数还是要被转化为指针的,所以,这个是等效的 static_assert(std::is_same_v<void(const int[3]), void(const int*)>);
    2. 其次就是那个关于typedef类型的指针的top-level cv qualifier的有趣的地方,它是会被去掉的,假如我们的参数是一个typedef的数组类型的话typedef int* IntPtr;那么它的const是一个top-level cv qualifier,这个迷惑了不少人,我是昨天看到大师Dan Saks的小品文才明白的!也就是说static_assert(is_same_v<const IntPtr, int*const>);,注意不要以为这个const是那个可以随意放置的非top level的cv qualifier,比如static_assert(is_same_v<const int*, const int*>);换言之,这两种指针是不等价的static_assert(!is_same_v<const int*, int*const>); 基于以上的认识就能够理解下面这个函数类型的结果了
      
      void f_IntPtr(const IntPtr);
      static_assert(is_same_v<decltype(f_IntPtr), void(int*)>);      // #1
      static_assert(is_same_v<decltype(f_IntPtr), void(int*const)>);  // this is identical to #1 because top-level cv-qualifier is dropped
      
    3. 与之相对的是如果我们的typedef类型是一个数组typedef int Arr3[3];的话,函数类型void f_Arr3(const Arr3);的计算要怎么对待数组参数呢?以下两个函数类型是等效的因为数组被转化为指针了。
      
      static_assert(std::is_same_v<decltype(f_Arr3), void(const int[3])>);
      static_assert(std::is_same_v<decltype(f_Arr3), void(const int*)>);
      
      换言之,如果typedef是数组的话,它的const不像指针类型那样被传为top level cv qualifier,而是先转为const数组,然后数组再作为参数被转为指针参与到函数类型的最后的参数列表里,所以,比较上述两个结果:当typedef为指针的时候,函数类型的参数是没有const的void(int*),而typedef为数组的时候,函数类型是有const的void(const int*),这个都是没有问题的定论,三大编译器表现在这一个基本观念上是一致的。
    4. 现在的关键在于如果typedef是来自于模板类的话是如何的呢?第一步我们先不适用模板类的成员变量简化问题,结果是没有问题: 我们顺利的得到了期待的函数签名void(const int*)
    5. 现在我们加大难度使用和问题一样的模板类的成员变量做函数参数: 依然没有问题!这是怎么回事呢?难道问题出在explicit specialization身上?难道static_assert走的是不同的路径吗?我需要重新编译,因为这个static_assert包含了头文件type_traits,需要使用完整的gcc/g++可执行文件也就是说包含了预处理文件的处理,我不能像之前那样使用纯粹的parser可执行文件libexec/gcc/x86_64-pc-linux-gnu/10.2.0/cc1plus
  3. 再次开始gcc的debug之旅,这里有一个小技巧,因为我不想被gcc-driver的复杂的进程转换带来的debug的困难,比如gcc/g++内部先要调用cpp可执行程序做预处理,这是一个不同的进程,随后再调用cc1plus,这是另一个可执行程序,在gdb里我需要很复杂的技巧才能打开源代码的跟踪,我以前尝试了几次就放弃了,因为也不可靠,所以,因为我只关心parser的地方就直接使用cc1plus来直接debug跳过了gcc-driver,那么现在对于代码里用到头文件处理的怎么办呢?方法是分段处理,先使用g++ -std=c++20 -E input.cpp -o intput.i这里直接使用cpp和gcc/g++效果是一样的。,然后再使用cc1plus的选项-fpreprocessed就行了:cc1plus -std=c++20 -fpreprocessed input.i,不过呢这里我犯了一个小小的错误让我对于这个方法一开始怀疑了,事情是这么的细微以至于我事后都很恼火:
    1. 我习惯于使用is_same_v,而这个小小的改进居然是c++17才有的,相比于is_same是c++11就有了,于是,你如果预处理没有加上-std=c++17的话,你得到的预处理过的文件被cc1plus使用它会抱怨is_same_v不属于std,这个以至于让我怀疑这个方法走不通而放弃。
    2. 更让我后来无语的是,当我注意到了这个c++17的问题后,我在cc1plus的选项加了这个开关却忘了这个输入的预处理文件没有更新,让我很是疑惑,这种问题最头疼,因为你对于本来正确的路径感到不太可能而放弃,实际上你离正确的路径就只有一步之遥而错过,如果事后你因此而耽误你会后悔好久的!
    总之,把这个debug的路径打通是一个很重要的里程碑,意味着我可以拓展代码的复杂度来继续相对容易的cc1plus的debug。先休息一下吧。
  4. 第一次意识到static_assert是c++语法的一部分,这个简直就是笑话,我用了这么久居然没有意识到它是语言的一部分吗?我猜想也许因为有些比如consteval我印象中是在c++库里实现的?我不知道这个奇怪的印象是怎么来的,这里明明是c++的关键字,我为什么不相信呢?我有这种奇怪的想法真的是无厘头。所以,很自然的看到这个static_assert的语法部分就感到有些意外了。
  5. 我原本以为现在应该更容易debug,但是我增加了语法的复杂度,因为单单is_same_v就是一个多么复杂的东西呢?而我以前对于cp_parser_toplevel_declaration的认识是错误的,这个其实是每一个namespace/scope的开始,我以前建立的概念似乎不对,为什么cp_parser_translation_unit不是每一个语句的开始呢?而且我在使用了static_assert(std::is_same_v<decltype(f<int>), void(const int*)>);后,这个f<int>不再需要调用determine_specialization,看起来这个似乎是最大的区别了,但是确定代码的路径真的挺不容易的,我一时间一筹莫展。但是反过来说如果static_assert能够正确的验证函数的签名有正确的类型,是否就说明determine_specialization是有问题呢?问题是怎么比较代码非常的困难。如果我对于gcc的代码有信心那么我应该直接回到这个determine_specialization
  6. 与其尝试比较static_assert的行为不如直接瞄准determine_specialization,经过了也许一个多月我又一次回到原点,但是这一次站得很高了。呼吸新鲜空气吧。

七月三十日 等待变化等待机会

  1. 其实,我每一次多明白一些就越发觉得这些前辈们早就看穿了问题的本质。我唯一不是很确定的是他们没有后续的跟进是觉得这个不值得还是等待标准的最后表述,或者时代久远就被遗忘了。或许等我真的明白之后就有答案了。
  2. gcc里的warning也要谨慎使用,尤其不要太多的打印集中在一条可能会溢出吧,总之尽量简短。同时加上q也就是加quote还可以高亮显示比较好看。
  3. 同时我有一种感觉似乎我昨天高兴的太早了,就是说关于模板函数特例化的声明从来就是对的,比如f<int>的函数签名从来就是void f(const int*),所以,我昨天看到static_assert反映的就是这个现象。 这里说的还是模板声明方面的问题吧?
  4. 我觉得我无法接受这个就是gcc对于top level cv qualifier的理解,我也不敢相信这个就是一切的源头,我之前也看到了但是不太理解,现在看到了感觉这个很有问题,在grokparms里计算函数参数列表要抛弃top leve cv qualifier,但是对于一个复杂类型的const你能够随便抛弃吗?究竟它是不是top level呢?但是看看这个最直接执行的函数cp_build_qualified_type_real被无数次的调用,我根本不敢相信它的做法有什么问题。肯定是我的理解有问题。这里我看不到任何关于Type-dependent expressions的处理,难道不需要考虑吗?至少typedef的类型也应该要处理啊?不是吗?看来我一定是哪里搞错了。

七月三十一日 等待变化等待机会

  1. 问题比我想像的要复杂,但是这个是否能够否定我的看法我也不知道。总之我昨天试图用一个更简单的例子来说明问题部分失败了,至少它是把水搅得更混。 这个标准的关于引用的例子是我前所未闻的,其中的某些方面似乎和typedef的top level cv specifier很有相似之处。我用下面这个例子更能说明某些让人吃惊的结果 看出来问题了吗?也就是说如果这里的行为实际上是和指针类似的,如果你把Ref定义成int*一切就都明白了,因为引用和指针本质上是一回事,只是在高级语言里给程序员加的一个护栏,在实际汇编码里应该是很接近或者完全一样都有可能,所以,它们遵循的原则是类似的,因为这里的const对于typedef得到的引用和指针是一样的都是top level cv qualifier,是不需要的,至于引用瘫缩的那个复杂大东西我现在没有空去想了,以前听说过是关于rvalue reference的,大概也是按照这个原则设计的吧,就是无用的传值加const是多此一举不应该出现防止重载泛滥。
  2. 但是这个例子却对于我有反作用,因为它导致我对于我修改后的例子得到的错误信息感到困惑了,让我不确定是否这个是标准的行为了。 我把我的例子里的数组改为引用 我们得到这个意外的错误是否让人吃惊呢? 为什么会这样子的呢?我看cp_build_qualified_type_real的注释里说由typedef引入的引用是允许的,但是我却看不到相应的代码,后来意识到那个是否报错的参数可能起作用,但是在函数参数这一层,它只检查参数是明显的typedef,而对于这种定义在结构里的typedef是无法知道的。换言之,假如传入这个函数的最后的参数里面是tf_ignore_bad_quals的话,我们就可以无声的创建我们认为正确的type_qualifier,而这个也许就是正确的解决之道,然而,我看这个却相当的不容易,因为函数参数要保存的信息要很多,比如要保存最原始的参数声明是typedef这个事实,这个不太容易啊。
  3. 我有另外一个例子证明我的猜想部分正确,至少目前gcc对于简单的typedef的处理是正确的 这里可以看出来数组和指针或者引用的typedef是截然不同的因为对于数组这里的const不是top level cv qualifier。我感觉似乎有一条遥远的光芒在很远的黑暗中若明若现。
  4. 想法很简单实际很困难,在debug中又跟飞了。

八月一日 等待变化等待机会

  1. 我想先做一些基础工作,就是搞明白typedef信息是怎么存储的。
    1. cp_decl_specifier_seq的结构里有一个成员数组location_t locations[ds_last];ds_typedef的位置存储location_t的信息,这个是一鱼两吃存储的是代码位置信息顺带存储了什么spec被声明了。而它的另一个成员tree type;才是真正记录类型,对于typedef的类型这个tree的code是TYPE_DECL,这个应该是我关注的重点。
    2. 而在tree的存储上我不是很确定,看到一个tree_type_decl,但是我不理解怎么用,这里就是一个小技巧,所有的tree的定义的文档就是看tree.def里的注释:
      The TYPE_NAME field contains info on the name used in the program for this type (for GDB symbol table output). It is either a TYPE_DECL node, for types that are typedefs, or an IDENTIFIER_NODE in the case of structs, unions or enums that are known with a tag, or zero for types that have no special name.
      这个信息是非常有用的。
    3. 这个是关于TREE_CONTEXT也很有用
      The TYPE_CONTEXT for any sort of type which could have a id or which could have named members (e.g. tagged types in C/C++) will point to the node which represents the scope of the given type, or will be NULL_TREE if the type has "file scope". For most types, this will point to a BLOCK node or a FUNCTION_DECL node, but it could also point to a FUNCTION_TYPE node (for types whose scope is limited to the formal parameter list of some function type specification) or it could point to a RECORD_TYPE, UNION_TYPE or QUAL_UNION_TYPE node (for C++ "member" types).
      For non-tagged-types, TYPE_CONTEXT need not be set to anything in particular, since any type which is of some type category (e.g. an array type or a function type) which cannot either have a id itself or have named members doesn't really have a "scope" per se.
    4. 关于结构是这样的
      TYPE_FIELDS chain of FIELD_DECLs for the fields of the struct, VAR_DECLs, TYPE_DECLs and CONST_DECLs for record-scope variables, types and enumerators and FUNCTION_DECLs for methods associated with the type.
    5. 对于函数可能是目前最有用的部分
      TREE_TYPE type of value returned. TYPE_ARG_TYPES list of types of arguments expected. this list is made of TREE_LIST nodes. In this list TREE_PURPOSE can be used to indicate the default value of parameter (used by C++ frontend).
    6. 这个我们已经有概念了,这个是类的成员函数
      METHOD_TYPE is the type of a function which takes an extra first argument for "self", which is not present in the declared argument list. The TREE_TYPE is the return type of the method. The TYPE_METHOD_BASETYPE is the type of "self". TYPE_ARG_TYPES is the real argument list, which includes the hidden argument for "self".
    7. 最最复杂的就是这个declaration的部分,这个是通用的部分,这里的binding是一个什么概念呢?我在代码里看到很多次了。
      Declarations. All references to names are represented as ..._DECL nodes. The decls in one binding context are chained through the TREE_CHAIN field. Each DECL has a DECL_NAME field which contains an IDENTIFIER_NODE. (Some decls, most often labels, may have zero as the DECL_NAME). DECL_CONTEXT points to the node representing the context in which this declaration has its scope. For FIELD_DECLs, this is the RECORD_TYPE, UNION_TYPE, or QUAL_UNION_TYPE node that the field is a member of. For VAR_DECL, PARM_DECL, FUNCTION_DECL, LABEL_DECL, and CONST_DECL nodes, this points to either the FUNCTION_DECL for the containing function, the RECORD_TYPE or UNION_TYPE for the containing type, or NULL_TREE or a TRANSLATION_UNIT_DECL if the given decl has "file scope".
      The TREE_TYPE field holds the data type of the object, when relevant.
      For TYPE_DECL, the TREE_TYPE field contents are the type whose name is being declared.
    8. 关于参数的声明我看不太懂
      PARM_DECLs use a special field:
      DECL_ARG_TYPE is the type in which the argument is actually passed, which may be different from its type within the function.
    9. 函数的声明是我要看的重点:
      FUNCTION_DECLs use four special fields:
      DECL_ARGUMENTS holds a chain of PARM_DECL nodes for the arguments.
      DECL_RESULT holds a RESULT_DECL node for the value of a function.
      The DECL_RTL field is 0 for a function that returns no value. (C functions returning void have zero here.)
      The TREE_TYPE field is the type in which the result is actually returned. This is usually the same as the return type of the FUNCTION_DECL, but it may be a wider integer type because of promotion.
      DECL_FUNCTION_CODE is a code number that is nonzero for built-in functions. Its value is an enum built_in_function that says which built-in function it is.
      DECL_SOURCE_FILE holds a filename string and DECL_SOURCE_LINE holds a line number. In some cases these can be the location of a reference, if no definition has been seen.
      DECL_ABSTRACT is nonzero if the decl represents an abstract instance of a decl (i.e. one which is nested within an abstract instance of a inline function.
      我觉得这个一定要自己亲自实践才能真正理解。
    10. 以上是通用的tree的一部分,而c++固有的一些tree在这里,先睡了。

八月二日 等待变化等待机会

  1. 昨晚太困了这个是c++相关的一些tree的部分,这里我只是挑选我感兴趣的部分。它们都是定义在cp-tree.def里,我摘抄其中的注释部分,这些都是没有文档的文档。
    1. 这个OFFSET_REF还没有接触过。但是TEMPLATE_ID_EXPR一直有碰到。这里是获得operand的宏TREE_OPERAND,它接受两个参数,第一个是tree,第二个是index
      
      /* An OFFSET_REF is used in two situations:
      
         1. An expression of the form `A::m' where `A' is a class and `m' is
            a non-static member.  In this case, operand 0 will be a TYPE
            (corresponding to `A') and operand 1 will be a FIELD_DECL,
            BASELINK, or TEMPLATE_ID_EXPR(corresponding to `m').
      
            The expression is a pointer-to-member if its address is taken,
            but simply denotes a member of the object if its address is not
            taken.
      
            This form is only used during the parsing phase; once semantic
            analysis has taken place they are eliminated.
      
         2. An expression of the form `x.*p'.  In this case, operand 0 will
            be an expression corresponding to `x' and operand 1 will be an
            expression with pointer-to-member type.  */
      
    2. 这个PTRMEM_CST也是没有怎么接触过的,这个RECORD_TYPE昨天的普通tree里提到过它的成员变量极其函数的声明。
      
      /* A pointer-to-member constant.  For a pointer-to-member constant
         `X::Y' The PTRMEM_CST_CLASS is the RECORD_TYPE for `X' and the
         PTRMEM_CST_MEMBER is the _DECL for `Y'.  */
      
    3. 这个BASELINK很重要,它的类型在我看来应该只能是METHOD_TYPE不然怎么可能是成员函数呢?难道static成员也可以在子类里重定义?
      
      /* A reference to a member function or member functions from a base
         class.  BASELINK_FUNCTIONS gives the FUNCTION_DECL,
         TEMPLATE_DECL, OVERLOAD, or TEMPLATE_ID_EXPR corresponding to the
         functions.  BASELINK_BINFO gives the base from which the functions
         come, i.e., the base to which the `this' pointer must be converted
         before the functions are called.  BASELINK_ACCESS_BINFO gives the
         base used to name the functions.
      
         A BASELINK is an expression; the TREE_TYPE of the BASELINK gives
         the type of the expression.  This type is either a FUNCTION_TYPE,
         METHOD_TYPE, or `unknown_type_node' indicating that the function is
         overloaded.  */
      
    4. 出去呼吸了一些新鲜空气,终于听到了《三体》结局的最后一集,从消灭人类暴政,世界属于三体开始,经过了毁灭你与你何干?,终于结束于小宇宙回归大宇宙
    5. 这个TEMPLATE_DECL是我格外关心的部分,要理解这个宏就要理解模板声明是什么
      
      struct GTY(()) tree_template_decl {
        struct tree_decl_common common;
        tree arguments;
        tree result;
      };
      
      这些宏是定义在tree.h和cp-tree.h里的
      macroremarks
      DECL_ARGUMENTSIn FUNCTION_DECL, a chain of ..._DECL nodes.
      DECL_TEMPLATE_INFO
      
      /* If non-NULL for a VAR_DECL, FUNCTION_DECL, TYPE_DECL, TEMPLATE_DECL,
         or CONCEPT_DECL, the entity is either a template specialization (if
         DECL_USE_TEMPLATE is nonzero) or the abstract instance of the
         template itself.	
         In either case, DECL_TEMPLATE_INFO is a TEMPLATE_INFO, whose
         TI_TEMPLATE is the TEMPLATE_DECL of which this entity is a
         specialization or abstract instance.  The TI_ARGS is the
         template arguments used to specialize the template.
      
         Consider:
      
            template <typename T> struct S { friend void f(T) {} };
      
         In this case, S<int>::f is, from the point of view of the compiler,
         an instantiation of a template -- but, from the point of view of
         the language, each instantiation of S results in a wholly unrelated
         global function f.  In this case, DECL_TEMPLATE_INFO for S<int>::f
         will be non-NULL, but DECL_USE_TEMPLATE will be zero. 
        
      DECL_VINDEX
      
      This field is NULL for a non-virtual function.  For a virtual
       function, it is eventually set to an INTEGER_CST indicating the
       index in the vtable at which this function can be found.  When
       a virtual function is declared, but before it is known what
       function is overridden, this field is the error_mark_node.
      
       Temporarily, it may be set to a TREE_LIST whose TREE_VALUE is
       the virtual function this one overrides, and whose TREE_CHAIN is
       the old DECL_VINDEX. 
      
  2. 这个是一个惊人的例子,我都看的目瞪口呆,这个难道是指针的指针吗?我实验了好几次才明白:
    
    struct A{
    	int A::* const p;
    };
    int A::* const A::* p1 = &A::p;
    static_assert(is_same_v<decltype(A::p), int A::* const>);
    static_assert(is_same_v<decltype(p1), int A::* const A::*>);
    
    这里的A::p是一个指向A的int类型的成员的指针并且指针本身是常量,那么对于它的地址操作得到的自然是指针的指针,只不过这个指针的指针本身就是指针变量,它可以指向其他上述类型的指针。这个需要细细体会才能领悟,而我们通常为什么能够叠加的写**指针指向一个指针呢?这里我还有一个基本的疑问就是对于一个非静态变量取地址居然不用有实例是什么道理呢?我把上述的成员p去掉const属性,那么&A::p的类型就是int A::* A::*它类型的读法是一个A的成员指针指向A的成员的指针,而这个指针指向一个int。算了我放弃了,感觉不太对。而这个例子精彩的是要如何初始化A呢?注意这个const是要求它正确的初始化的,否则你就改不了了!,这里我们稍稍改变一下定义
    
    struct A{
    	int m;
    	int A::* const p1;
    	int A::* p2;
    };
    A a{1, &A::m};
    a.p2=a.p1;
    
    对于这个&A::m要怎么理解呢?我觉得这个如果对于类的成员的定义有所了解应该就能够比较容易明白,因为饿成员变量并不是真正意义上的指针,它们实际上是偏移,明白了这一点就能够理解A::m的代表偏移的意义了。
  3. 对于指向类的成员函数的指针是怎么使用的呢?这个也是让人抓狂的一件事情。我故意定义了一个static成员以示其中的区别。
    
    struct B{
    	void base(){}
    	static void base(int){};
    };
    void (B::* pMember)()= &B::base;
    void (*pStatic)(int)=&B::base;
    (B{}.*pMember)();
    (*pStatic)(0);
    
    作为函数指针它的类型是包裹着变量名的,这里我一开始还担心取地址&B::base得到的有可能有歧义需要static_cast,事实证明编译器足够聪明不会选择静态函数,事实上从类型也可以理解所谓的static成员函数根本和类没有关系,它们不是method而是procedure,仅仅是不同scope而已,也就是说它是一个scope在类里的普通自由函数,它们的签名是截然不同,没有任何歧义!最后要记住函数指针的调用也是一样遵循它们的定义,必须要在{}里使用dereference的*。这一点我一开始感到不可思议,这个实在是太平常不过了。
  4. 我常常在想为什么数组指针(*)[]和函数指针(*)()的定义里都要有一个(),现在我的理解是()的两个功能之一的grouping,也就是它改变了它跟随在后的操作符的优先级以至于在(*)里的指针*的优先级超过了后面的操作符能够让编译器理解到要先解析指针,再解析后面的操作符判断出它是指向数组[]还是函数()。这个意义就是典型的grouping的作用。
  5. 多数人的思想来源于视野,少数人的视野起源于思想。翻译成大白话就是多数人因为看到才相信,少数人因为相信所以才看到
  6. 什么是tree呢?我一直有一个模糊的认识,其实文档写的都不如注解清楚,因为注解是程序员写给其他程序员看的,基本上大家是心灵相通的,否则也就不会读懂,读不懂的就不是程序员。这个是循环论证吗? 其实原因是这些东西是不足为外人道也!不应该被外界程序员所了解,因为没有意义,除非你是开发者,但是如果你是开发者你基本不需要看文档,直接看代码就好了呀?这个真的是另一个悖论,就和我遵循的看电影的原则是一样的,因为我从来不看我没有看过的电影,因为我讨厌看烂片一定是看那些我喜欢看的电影。很多公司招人也是一定要有本行工作经验的才行,如果全部公司都遵守这个原则那么永远都不会有新人能够入行了。这就是难题啊。所谓的tree一定是要包含tree_base,它是所有的类型的核心或者如同基类一样的东西,它包含了几个最重要的共同的部分,其中有TREE_CODE,这个大都定义在tree.def里的enum。 算了我还是遇到具体问题再去搜索相关的tree吧。
  7. 我忘了大概是在哪里看到的,对于gdb里面很多时候要小心,因为如果我执行了一个函数就有可能把当前的代码的执行顺序搞乱了,所以,注解里告诫我说大写的通常是宏可以反复执行而没有副作用,可是小写的是函数就不同了,比如这个函数你是不要执行的cp_lexer_next_token_is_keyword,这个是显而易见的,它不是peek,它有可能是会consume当前的token的。这个也许不是好的例子,因为这个函数恰恰是调用的cp_lexer_peek_token,所以无大碍。总之要小心。

八月三日 等待变化等待机会

  1. 这么多天还是第一次注意到gcc的全局变量:global_namespace,它的code是NAMESPACE_DECL是一个相当大的数组。
  2. 可能大多数时候,你从declaration获得的都是一个所谓的TYPE_DECL,很显然的它的TREE_TYPE是盖头下真正的类型
  3. 假如一个tree是RECORD_TYPE
    
    (gdb) p TREE_CODE(current_class_type)
    $426 = RECORD_TYPE
    (gdb) p TREE_CODE(TYPE_FIELDS(current_class_type))
    $427 = TYPE_DECL
    (gdb) p TREE_CODE(DECL_NAME(TYPE_FIELDS(current_class_type)))
    $428 = IDENTIFIER_NODE
    
  4. 我观察到这个至关重要的注解
    
    /* Setup a TYPE_DECL node as a typedef representation.
    
       X is a TYPE_DECL for a typedef statement.  Create a brand new
       ..._TYPE node (which will be just a variant of the existing
       ..._TYPE node with identical properties) and then install X
       as the TYPE_NAME of this brand new (duplicate) ..._TYPE node.
    
       The whole point here is to end up with a situation where each
       and every ..._TYPE node the compiler creates will be uniquely
       associated with AT MOST one node representing a typedef name.
       This way, even though the compiler substitutes corresponding
       ..._TYPE nodes for TYPE_DECL (i.e. "typedef name") nodes very
       early on, later parts of the compiler can always do the reverse
       translation and get back the corresponding typedef name.  For
       example, given:
    
    	typedef struct S MY_TYPE;
    	MY_TYPE object;
    
       Later parts of the compiler might only know that `object' was of
       type `struct S' if it were not for code just below.  With this
       code however, later parts of the compiler see something like:
    
    	struct S' == struct S
    	typedef struct S' MY_TYPE;
    	struct S' object;
    
        And they can then deduce (from the node for type struct S') that
        the original object declaration was:
    
    		MY_TYPE object;
    
        Being able to do this is important for proper support of protoize,
        and also for generating precise symbolic debugging information
        which takes full account of the programmer's (typedef) vocabulary.
    
        Obviously, we don't want to generate a duplicate ..._TYPE node if
        the TYPE_DECL node that we are now processing really represents a
        standard built-in type.  */
    
    实现这个的做法是set_underlying_type,就是如果这个tree是built-in type,那么仅仅是设定它的TYPE_NAME,当然数组除外。看来数组也算是built-in type了?
    
     if (DECL_IS_BUILTIN (x) && TREE_CODE (TREE_TYPE (x)) != ARRAY_TYPE){
    	if (TYPE_NAME (TREE_TYPE (x)) == 0)
    	TYPE_NAME (TREE_TYPE (x)) = x;
    }
    
    那么对于其他呢?就是设定DECL_ORIGINAL_TYPE指向一个复制的TREE_TYPE
    
    if (TREE_TYPE (x) != error_mark_node  && DECL_ORIGINAL_TYPE (x) == NULL_TREE){
    	tree tt = TREE_TYPE (x);
    	DECL_ORIGINAL_TYPE (x) = tt;
    	tt = build_variant_type_copy (tt);
    	TYPE_STUB_DECL (tt) = TYPE_STUB_DECL (DECL_ORIGINAL_TYPE (x));
    	TYPE_NAME (tt) = x;
    	TREE_TYPE (x) = tt;
    }
    
    这个告诉我们遇到一个声明的类型TYPE_DECL要检查它的DECL_ORIGINAL_TYPE来查看它的typedef的类型?是吗?但是TYPE_DECL的背后是各种各样的类型啊?没错!是这样子的!我找到了对应的小函数is_typedef_decl
  5. 此外注释里强调
    /* It's important that push_template_decl below follows set_underlying_type above so that the created template carries the properly set type of VALUE. */
    因为接下来的push_template_decl是一个相当复杂的过程,而且这些部分已经不再有严格的语法规范的要求了,我的感觉这里更多的是内部实现的细节部分吧?总之,究竟怎么保存typedef的信息这一点我还是不太明确。
  6. 我心底里有一个怀疑不敢说出来,就是decl2.c这个代码文件是不是有人想要尝试的把原本让人抓狂的decl.c做一些修正呢?但是很明显的放弃了。不过这个可能性也不不大吧?因为decl.c不到两万行代码和parser.c的四万行相比其实不算太大。当然了臭名昭著的那个四千行三千行的函数grokdeclarator就在其中,我是真心的希望有人能够挺身而出改造那个函数,看来没有人愿意做这个可怕的工作。这个英雄出场的配乐我都想好了一道闪电裂长空,裂长空。 我感觉我可能是发现了可能的问题的根源,但是要确认和找到可能的修补的办法可能还是非常的遥远,大概还需要gdb几万行代码吧?在c++的parsing几行看似简单的模板源代码的解析工作可能是要走过几万行代码,稍不留神错过一个函数可能就是错过了几千行代码。这个让我想起了《三体》最后的章节,程心和关一帆在蓝星上考察回来被困在黑域里的几天时间结果大宇宙就经历了一千七百多万年的时间,这个比神话里的天上方一日,世上已千年还要久远的多得多。而云天明的三个童话里的长帆没想到却暗合着关一帆,这个露珠公主最后的归宿的那个长帆竟然不是云天明自己,这个预言实在是太让人哀叹了,但是难道云天明已经看到了未来?也许三体文明有某种预测未来的手段?
  7. 前面依然不知道还有几万行代码,因为我依然不知道问题的准确的原因,在第一个模板类的成员typedef里我们是看到了信息是被记录下来了,但是它是怎么存储的我依然很模糊,应该是在当前的全局变量里,可是我还是没有定位lookup_name之类的函数是怎么被调用的,这些其实是更基本的,而我始终不清楚,这是让人汗颜的。在模板函数声明的第二句里,这个变成了查找arr这一个id而已,结果返回的tree就是一个identifier显然没有了type_decl的丰富的信息,那么究竟信息是在哪里丢失的呢?依然不清楚,但是我似乎清楚的是问题不在第三句的specialization上,如果这个结论是对的,至少我一个月的debug没有白费,因为我前面大部分时间都花在了这个第三句特例化上了,可以几乎肯定的是在start_function里调用这个庞然大物grokdeclarator得到的就是错的,问题就在于说在这之前呢?出错的表现是declarator,可是它是一个函数,它的参数的解析过程是否已经错了,这个最后的表现出来的函数签名只是结果而已? ‘void f(typename A<T>::arr)’这里的const究竟是为什么被遗弃了呢?当然是没有认识到arr是一个typedef,可是如果你查找的arr是一个简单的identifier_node,你能够想到它是typedef吗?这个信息原本是记录在它的所谓的context也就是类A里的,它是一个record_type,可是代码逻辑寻找的是arr,A仅仅是解析的一个过程,而且我已经糊涂了不知道要怎么找到那个typedef。彻底晕了。

八月四日 等待变化等待机会

  1. 停电之后把我的debug环境全部清空也让我有一个机会重新反省问题。这个的确是一个非常复杂的问题,而我又引入或者说发现了另一个潜在相关联的问题。如下的代码的编译错误是过分严厉或者说不适当的把编译器内部的错误归咎于用户了。 对于这样子的编码给出这样子的错误是不适当的 如果你还要争辩const int&的定义本身是无意义的,那么特殊化一个结构如何呢? 对于这样子的错误要怎么说,为什么不能呢? 你可以搬出一大堆的理由来争辩在计算函数签名中那个const是被drop掉了,所以,这个声明中的const有和没有是一样的,所以,这个声明有问题,也就是说以下都是正确的 但是回过头来说,用户按照语法规则加了一个无用的const,so what?这个就是出错了吗?用户违法语法了吗?至少gcc的处理方式是错误的,深层次的问题是gcc忽略了这个类型是dependent-type的时候没有保持在这个id下检查它的context以便保持typedef的定义来源特性,因为reference只有在typedef的情况下才允许引入类型。相对应的clang处理的最好,因为它接受并且给出了警告说那个const被忽略了,其次msvc也还可以因为接受了但是没有给警告,这个应该是有可能加开关输出的吧?总之,gcc做的不对,我准备提一个bug。

八月五日 等待变化等待机会

  1. 我提了这个bug到GCC。但是心里总是惴惴不安,因为我写的太凌乱了,很可能没有人愿意看吧?
  2. 我觉得我应该好好研究一下之前我提的bug被归咎于dup的那个bug的含义,只有弄懂了它才能真正理解问题的实质,毕竟大佬们比我高明的不是一点半点了。
    Johannes Schaub says "in both situations the question is whether the parameter type adjustments happen immediately or after instantiation (when T is replaced by the actual type)", regarding the possibility that http://gcc.gnu.org/bugzilla/show_bug.cgi?id=49051 is related.
    我一开始也是这样子认为的,可是,我总是觉得有什么地方不是很清楚。
  3. 无意中搜索到三个月前的笔记,花了好长时间才明白怎么回事,这个问题确实不简单。学而时习之吧。
  4. 很基本的东西我都不知道,git diff产生的有根目录名字,结果我用普通的patch的时候总是抱怨,我却不明白加一个目录深度就可以了,patch -p1 < my.patch,或者是不要产生prefix也好啊。git diff --no-prefix
  5. 在这个很特殊的announce_function里看到cxx_printable_name怎么打印函数声明的,实际上和error.c里面是一样的,但是我似乎也有了一个选择,而且跟踪这个函数打印的过程进一步加深理解函数打印的原理。
    1. 首先我们要确保TREE_CODE (decl) == FUNCTION_DECL,因为复杂的情况我也不想看了。
    2. 函数返回值,这个实际上是最简单的了。就是TREE_TYPE(TREE_TYPE(decl)),或者说FUNCTION_TYPE的TREE_TYPE就是返回值。而对于FUNCTION_DECL来说它的TREE_TYPE就是返回FUNCTION_TYPE
    3. 函数名字是DECL_NAME返回的一个id,所以,IDENTIFIER_POINTER(DECL_NAME(decl))
    4. 参数是FUNCTION_FIRST_USER_PARMTYPE返回的一个TREE_LIST
    5. 对于TREE_CODE是TYPENAME_TYPE的情况,是获得它的TYPE_NAME
    6. 对于是否是一个typedef的判断宏typedef_variant_p基于说它的TYPENAME是否存在并且TREE_CODE (x) == TYPE_DECL并且DECL_ORIGINAL_TYPE (x) != NULL_TREE这个我之前已经有记录,但是我觉得我忽略了TYPENAME的这个属性,所以,我需要特别留心parsing模板声明的时候参数是否设置了DECL_ORIGINAL_TYPE
    吃饭吧。今天的收获之一是开始使用gdb的jump直接检验我的猜想,这个跳跃相当于改变程序的执行要小心应用,同时不在跳跃点设置断点很多时候你自己都糊涂了,另一个办法是在函数里直接设定return的值来改变下一步的走向,这些都是快速检验我的一些猜想的办法,很快的省却了我修改代码编译执行的时间。今天足够多了。
  6. GCC的coding conventions是很有必要熟悉的,这个好像一支军队的正规化建设一样的重要。
  7. GCC的wiki比传统的文档好多了。
  8. 其实对于纯粹编译错误的testcase很简单,就是把源代码命名为pr101783.C在gcc/testsuite/g++.dg/parse下,这个目录下的编译基本没有特殊要求,根本无需学习怎么写testcase,因为编译指令不需要,也没有平台的要求。至于运行也很简单,除了需要的库和工具之外我发现我还需要安装一个叫做autogen的工具。然后在编译目录下运行 make check-gcc RUNTESTFLAGS="dg.exp=pr101783.C" 这里因为我需要的dg.exp是位于g++.dg目录下的最普通的*exp。所以,运行它是合适的,那些高级的分布在各个其他目录下的名字各异的*.exp我懒得看了,而使用测试代码的文件名让测试精准运行。今天还是小有收获的。

八月六日 等待变化等待机会

  1. GCC patch requirement

八月七日 等待变化等待机会

  1. 这段关于引用的定义的话看的我眼睛都疼。

八月八日 等待变化等待机会

  1. 这里的reference collapsing虽然听说过,但是依然有些吃力。 针对这段精炼的表述要怎么理解呢?
    If a typedef-name ([dcl.typedef], [temp.param]) or a decltype-specifier ([dcl.type.decltype]) denotes a type TR that is a reference to a type T, an attempt to create the type “lvalue reference to cv TR” creates the type “lvalue reference to T”, while an attempt to create the type “rvalue reference to cv TR” creates the type TR.
    我是这么总结reference collapsing的:
    1. 假如typedef的是lvalue reference,那么无论怎么添加多少const或者&,它还是原本的lvalue reference to T。比如typedef T& TLR;凡是TLR声明的变量始终是TLR,不论你加不加const或者再多几个reference(&)
    2. 针对typedef的是rvalue reference情况有些不同,const依然会被无视,但是最后类型是否是rvalue reference还是lvalue reference,则需要看总共的&的奇偶性:奇数是lvalue reference,偶数是rvalue reference
    3. 事实上你对于一个类型只能声明它的lvalue referene或者rvalue reference,那种超过两个以上的reference(&)都是非法的,所以,也没有什么奇偶性那么复杂。
  2. 我犹豫了好久决定还是提交我的建议的bug fix,希望能够被哪位大神注意并采纳。

八月九日 等待变化等待机会

  1. 其实我不太需要关心GCC查找的全局变量是什么,因为有一些所谓的lookup_name_xxx函数,甚至还有什么使用什么oracle的东西,总之,这个查找机制我可以作为透明来看待。
  2. 对于一个TEMPLATE_DECL来说也很简单,首先就是DECL_NAME是一个IDENTIFIER_NODE,然后,它的DECL_CONTEXT是一个TRANSLATION_UNIT_DECL,从它TREE_TYPE可以得知它是一个RECORD_TYPE当然这个是针对一个模板函数的声明而言的。,要获得模板参数使用DECL_TEMPLATE_PARMS,很显然的它是一个TREE_LIST,还有所谓的TMPL_PARMS_DEPTH,这些都是很复杂的模板模板的情况。这里我看到要转化模板声明为所谓的most_general_template,这个逻辑还是很陌生的,但是似乎是在检查DECL_LANG_SPECIFICDECL_TEMPLATE_INFO,幸好我没有看到。,在这个lookup_template_class函数里参数是相对应的模板参数,它的深度是TMPL_ARGS_DEPTH,很显然的模板类声明的参数深度要和代入的模板参数的深度相匹配才行。它随后的是一个看似很吓人的言简意赅的函数tsubst,我感到很犹豫要不要跟呢?肚子都有些饿了,这一次在一两个星期后重新跟踪gdb感觉少了很多迷茫,原因是语法路径稍微的清晰了,知道哪里是在兜圈子。另一方面对于GCC的代码风格熟悉了很多,知道了什么是宏和类似宏的小函数,对于那些判断类的函数名字是结尾xxx_p都可以放过,而对于那些xxx_tentatively的函数也都放过,我现在的体会是这个应该就是上下文有可能歧义的所谓的lookahead的吧?当然是对于token而言的parser了,很多时候有多条语法路径的时候更需要这种试探性的lookahead,这个是不可避免的。不过我对于GCC的parser里众多的稀奇古怪的做法还是感到很彷徨,还看到什么firewall把parser阻断一下,难道是防止多线程吗?应该不是,注解写的我也看不懂。总之,这就是一个大迷宫,走入的时候没有自带一根绳子是走不出来的。我觉得撒面包屑都不够,有可能会递归兜圈子最后后来的覆盖前面的,就是我在bt里看到函数名字依然不知道我自己在什么路线上一样。
  3. 这里我又看到了另一类的查找形式,就是使用所谓的iterator的形式吧,比如这个查找模板特例化的就是使用所谓的elt这类的iterator把模板声明和参数代入一些所谓的hashtable来搜索,这些构成了GCC庞大复杂的搜索体系,这些虽然复杂但是我们不需要关心它的实现,我在想clang之类的在这种实现上不一定就不如GCC因为毕竟GCC很多实现是很古老的纯粹C语言的方式,还需要强制的实现垃圾回收,而不是依赖引用计数的自动方式,也许效率高了一点点,代价是太难以维护了吧?
  4. 感觉很累了我想停下来吧,这个内心的呼唤让我想起了《三体》里申玉菲对于汪淼所重复的那句话,停下来试试看吧?我对于她的这句话始终理解不透如果有个人拯救了人类或毁灭了人类,那你可能的功绩和罪恶,都将正好是他的一倍为什么正好是一倍呢?似乎主人公汪淼和史强也都问过同样的问题。申玉菲这个名字也许是作者找的一个歌手的影子,应该很容易联想到她和王菲的形象有一点类似,因为她有可能来自上海就是,而玉菲就是有一点王菲了。《三体》里很多名字都是来自英文,这个是最容易理解的部分,可是这里居然这么瞎联系。史强(strength/strong),罗辑(logic)这个是最明显的例子。艾AA我觉得是来自于中文和法语的结合,就是中文的,而AA就是法语的Amour, Amour,也就是爱啊爱啊,这个暗示她就是一个多情的种子,和书中描述的未来人代表的率性滥爱是一致的,她最后和云天明的结合似乎也是顺理成章的吧?两个截然相反的男女在地球文明毁灭之后的伊甸园里发生的必然。
  5. 找到了这个amour, amour, amour好像又名《The last time I saw Paris》,是不是Trump也提到过这个?可惜巴黎我还没看到它就凋谢了,不过这样子也很好因为我不会为失去的记忆而悲伤。
  6. 《三体》里的逻辑是好多层的,我看的都糊涂了,这个和gdb跟踪GCC是一样的道理吧?之乎里有大量的有关《三体》的帖子,我看的头都大了。

八月十日 等待变化等待机会

  1. 所谓的TYPE_DECL其实就是DECL_NAME是一个IDENTIFIER_NODE,而TREE_TYPE才是它原来的原始类型,比如RECORD_TYPE,这里要注意一点就是DECL_ARTIFICIAL对于这个TYPE_DECL的意义应该是typedef的意义。我读到过所谓的implicit typedef的概念,但是忘记了具体是什么,这里看到的情况是它的DECL_IMPLICIT_TYPEDEF_P被设定了,而它的TYPE_NAMETYPE_STUB_DECL都指向一个刚才设定的为typedef的TYPE_DECL,就是说在TYPE_DECL包裹了一层。这个实在是太复杂了,我希望我不会遇到。
  2. 对于这个函数lookup_template_class_1我总感到一丝的狐疑,英文就是fishy,说它小是也不过几百行的代码,但是它的复杂程度让人感到头疼,注释自己都流露出对于它的不安,因为它在完全不同的阶段被调用过两次。而最让我不愿意看到的是对于传入参数好几个地方都进行了修改与替换,这个是代码里我以为最头疼的地方,它为了营造一种虚假的参数一致性把多个实质上的函数集成在一个函数里来处理,这个应该是当年c程序员对于变量名惜字如金的习惯,这让我想起了从前的前辈导师介绍远古时代编程的时候甚至要节约纸带上的常量。总之,第一次看这个函数是看不懂的,以后可能会反复在这里打圈子先留一个记号吧。
  3. 对于一个DECL_CONTEXT来说就是它的含义可以是多种多样的,这个相当的复杂
    
    /*  For FIELD_DECLs, this is the RECORD_TYPE, UNION_TYPE, or
        QUAL_UNION_TYPE node that the field is a member of.  For VAR_DECL,
        PARM_DECL, FUNCTION_DECL, LABEL_DECL, RESULT_DECL, and CONST_DECL
        nodes, this points to either the FUNCTION_DECL for the containing
        function, the RECORD_TYPE or UNION_TYPE for the containing type, or
        NULL_TREE or a TRANSLATION_UNIT_DECL if the given decl has "file
        scope".  In particular, for VAR_DECLs which are virtual table pointers
        (they have DECL_VIRTUAL set), we use DECL_CONTEXT to determine the type
        they belong to.  */
    
  4. 又转了无数的圈子我已经迷失了前进的方向了,但是我的感觉在lookup_name这类函数应该是不会有问题的,因为它们太基本了很容易发现其中的错误,虽然它们返回的是被层层包裹那只是因为所在的所谓的context和scope的不同,并没有改变本质里包裹在最深层的本源,因为如果那里复制本源的类型有错误这个就是大麻烦了。所以,我认为还是要抓重点在把参数声明类型进行计算函数签名类型转换过程中的点。因为函数签名也就是所谓的declarator是后续特殊化解决的关键所在
  5. 读这些DR报告真的是很困难,一个主要原因也许是英语,作者都是语言使用的专家,不论是计算机还是人类的语言,而且惜字如金,锱铢必较,读起来非常的吃力。我就对于最后两次会议的结论的两句话就看了半天依然不是很清楚,原因也许是我对于问题本身也不是很清楚,只是模模糊糊的认为我的理解就是给引用添加cv-qualifiers对于函数类型是有特殊处理的,这一点我在代码里已经看到了,但是作者的问题是说标准未提及关于typedef/template-parameter引入的引用应该有什么特殊处理,这个本身我就不是很清楚,我似乎实验过把函数类型作为模板参数或者typedef来代入引用,但是结果我也忘了,总而言之,有一个结论似乎是可以记下来的:除了使用typedef和模板参数是没有其他途经来引入引用类型的。这个似乎是一个铁律。不知道我理解对不对?

八月十一日 等待变化等待机会

  1. 依然处于迷茫之中,但是我至少观察到了一个现象,在解析模板函数声明的语句中,在cp_parser_parameter_declaration_clause得到的模板函数的参数不是模板参数,而是函数参数!的结果是存储在一个cp_parameter_declarator的结构里,其中,它的成员里decl_specifiers是一个cp_decl_specifier_seq的结构这个和语法是一一对应的而且忠实的记录了所有信息,它在它的locations这个数组里记录了ds_const出现的位置,这个是一鱼两吃的做法,使用location_t来记录位置同时表达有哪些cp_decl_spec遇到过,这个投机的做法我很担心会被遗漏。,至于说参数本身的类型则是在cp_decl_specifier_seq的成员type里保存着,它的TREE_CODE(parameter->decl_specifiers.type)TYPENAME_TYPE,所以,这里最让我感到收获的是对于const并不是类型本身的一部分,比如使用TYPE_QUALS(parameter->decl_specifiers.type)是得不到它的真实的cv-qualifier的,那么代码是如何处理的就是以后的关注的重点。换句话说,就是parser得到的参数类型的所有信息是要依赖于两部分的,这个部分我不知道是不是因为是typename的特殊情况呢?我以前看到过讨论说const是不能放在typename后面的,这个是语法本身的设定,这个是否是一个因素呢?
  2. 另一个显而易见的结论是在模板函数的函数参数解析完成后需要完成declaration的计算,所以,调用这个庞然大物grokdeclarator得到的结果是一个PARM_DECL,这个庞然大物我已经反反复复在很多不同的应用场景遇到,这个也是它为什么这么大的缘故因为计算各种不同对象的声明,比如这个是参数的声明,传入的标志量是PARM,那么它在完全不同的情况下的执行,我也许可以依赖参数来定点的设定断点?
    
    /* In grokdeclarator, distinguish syntactic contexts of declarators.  */
    enum decl_context
    { NORMAL,			/* Ordinary declaration */
      FUNCDEF,			/* Function definition */
      PARM,				/* Declaration of parm before function body */
      TPARM,                        /* Declaration of template parm */
      CATCHPARM,			/* Declaration of catch parm */
      FIELD,			/* Declaration inside struct or union */
      BITFIELD,			/* Likewise but with specified width */
      TYPENAME,			/* Typename (inside cast or sizeof)  */
      TEMPLATE_TYPE_ARG,		/* Almost the same as TYPENAME  */
      MEMFUNCDEF			/* Member function definition */
    };
    
  3. 那么在这个庞然大物呼叫之前的传入参数parameter->decl_specifiers.type我按照%qT打印的结果是typename A<T>::Type显然是正确的,我以前会认为它丢失了const,实际上const信息是存储在locations里的,所以,没有丢失,这个是今天长进的地方。
  4. 关于它的返回结果我想要打印出来看,这里有一个小技巧就是针对PARM_DECL我不能直接打印,而是要打印它的TREE_TYPE,这个是一个TYPENAME_TYPE,而它是可以使用%qT来打印类型的,结果让我比较的意外,因为是正确的const typename A<T>::Type,这个其实应该是意料之中的,这个庞然大物是被无数人反复检验在几乎各种情形下的结果的,错误不在这里。也就是i说参数在还没有进行模板替换之前是正确的,我要进入到下一步的模板参数代入吗?还是说函数声明的计算步骤?太热了。今天到此为止吧?
  5. 我今天相当于检验这个庞然大物在计算函数参数的过程。

八月十二日 等待变化等待机会

  1. 有时候最原始的也许是最好用的办法,比如打log是被认为最基本的无技术含量的debug手段,但是往往最简单实用。我目前终于可以断定出错的函数还是那个庞然大物grokdeclarator,只不过我以前也认为是这里的问题,可是因为反反复复的调用它并不知道是怎么回事。现在明白了因为解析会遇到好多的声明,不仅仅是模板函数的声明,包括参数也是声明,更不要说模板类以及内部成员,那么每个声明最终都要归结于一个计算一个declarator来存储在一定的scope里作为后续的查找比对,所以,针对不同的声明就多次调用这个函数,这个本来是很普通的常识,我却花了好久才明白,尤其是昨晚看到了它的那个decl_context参数才明白源尾,这个学习的过程实在是太漫长了,主要还是基础没有纯粹靠看代码来理解是很难的,好比没有修习过玄门正宗的气功却想靠摸索揣摩来修习一样吃力甚至经常的走火入魔。当年梅超风虽然拿到了《九阴真经》下卷能够学习很多毒辣实用的武功但是没有上卷的内功修习她无法修习更高深的基础,而她并非道家出身连基本的三花聚顶,五气朝元的基本道家术语也看不懂,竟然要在交战中套路马钰才能理解,这个就是没有基础看不懂黑话或者叫做行业术语的痛苦。
  2. 所以,问题就出在计算函数的declarator的过程,也就是模板函数的声明的解析过程,这个在函数参数正确的解析结束之后,这个和预想的是一样的,一个多月以来我们一直都是这么认为的,而且前辈大侠们也是这么认为的,只不过Knowing the path is completely different from walking the path。否则《Matrix》里Morpheus从Oracle那里已经知道全部的路径为什么自己不去趟路非要苦苦寻找Neo来走那条路呢?知易行难是人人都知道的道理,但是依然未必人人都能亲自实践。归根结底是在它的context是FUNCDEF的时候,传入的declarator是函数的参数,而declspecs指的是函数它包含了函数的返回值以及其他信息,所以,只有在这个时间点才是问题的关键!我这里重复了昨晚的战术,就是这里参数不能直接实用%qA来打印只能使用它的类型%aT来打印TREE_TYPE(TREE_VALUE(declarator->u.function.parameters))。当然grokdeclarator返回的是一个函数声明是可以使用函数声明的%qF来打印的。结果就是输入的参数是const typename A<T>::Type返回的函数签名是void f(typename A<T>::Type),参数的const丢失了,我认为是typedef的信息没有被考虑,但是这个函数至少是四千行三千行的代码,而且我怀疑其中又要走无数的岔路可能涉及到模板替换的无数的复杂情况,所以,我在这里打转转一个多月依然还有至少四千行三千行的代码在前面。
  3. 我现在的战术是对于这些大函数不再跟进去而是在传入传出打log看看是否正确,这个本来是最简单的战术,我不知道为什么我没有第一时间采用,可能是这样两个原因,当初不知道要看什么参数,也并不理解参数传递的原理,一直以为解析的结果是存在某个全局变量里看不到,现在才明白最终是在类似于scope的某个全局变量里依靠lookup_name_xxx之类的函数查找,但是在top level declaration没有完成之前是存在parser里的,而且我一开始对于parser里的scope误解它是全局变量带偏了思路,现在知道那个是针对当前解析的类或者名字空间的所谓scope和全局变量有联系但是不尽相同,这个是另外一个复杂的机制,经常看到代码里有push/pop就是和这些有关吧?只能是有关,这里太复杂了我根本不想去管,因为牵扯到变量访问权限和视野的问题那是多大的范围的问题啊。另一个重要原因就是我当初对于打印函数虚参数总是segment就束手无策,现在才知道对于typename用类型是可以打印的。应该就是这两个主要原因限制了我的想像力才没有办法使用最简单的办法。非不愿也,乃不能也。有很多时候不是想不到最简单的办法而是在实际中碰壁了。那种以为长在大路边上的成熟的李子没有被路人吃光而感到沾沾自喜的人应该想到那些李子很可能是酸的而被路人所放弃的。年轻的时候,大多数人都认为自己有很多想法很聪明以为别人想不到,越来越碰壁之后才明白别人其实也有过类似的想法,那种所谓一个主意能够卖一佰万的故事大都是骗人的鸡汤文案。
  4. 今天早上我发现我连数数都会错!不到三千行代码我却四舍五入看成了四千行代码!我真的是老糊涂了。好消息就是需要debug的代码减少了一千多行!
  5. 我现在目标很明确,范围也确定了,可以说在打明牌了,这要是还攻不下阵地就太差劲了。然后我再次来到一个月以来好多次到达的地方,这个也是我反复怀疑的地方,就是cp_build_qualified_type_real,而这里究竟要怎么对待由typedef带来的cv-qualifier呢?是一律忽略掉对吗?这个是我刚刚提交的bug-fix添加的注释
    
     /*
       * Cv-qualified references are ill-formed except when the cv-qualifiers
       * are introduced through the use of a typedef-name ([dcl.typedef],
       * [temp.param]) or decltype-specifier ([dcl.type.decltype]),
       * in which case the cv-qualifiers are ignored.
       */
    
    那么我现在自己面对这个复合的类型是否需要再三思呢?我觉得我需要再次好好全面地理解标准
  6. 我来到了另一个关口grokfndecl,我觉得我应该使用同样的战术检查传入传出参数再做打算。结果我在这个函数传入参数的函数签名就看到了问题:void(typename A<T>::Type),这个丢掉const的原因就是计算cv-qualifier时候因为以上的typedef被丢弃,而作为函数参数却依然保持原样:const typename A<T>::Type,这个让我觉得这里的问题更大了,因为我还没有看到代码怎么处理函数签名与参数不一致的情况要怎么办。总之问题可能也没有那么简单。

八月十三日 等待变化等待机会

  1. 你即便非常清楚问题的症结,但是也不一定清楚知道怎么解决,否则这个问题十年前就解决了。即便我心里知道这个const不能针对数组被丢弃,但是我怎么知道它是数组呢?唯一的办法是重新回到第一条语句看看当解析到数组后这个信息是怎么被存储的。这个无疑是很大的工作量。
  2. 追本朔源是一个比较耗费耐心的苦力活,这个是针对不熟悉行话术语的人来说的,我就是不熟悉这些函数名,比如如果我已经知道我就是要看解析数组的那部分代码我能否快速的查找到代码部分设断点呢?我做不到只有傻傻的一路跟下去,然后对于parser的解析过程感到十分的困惑为什么看到一个数组名之后要打转转如此多次都不能确定要去下面看看[]来决定呢?这部分的语法我懒得看,总之饶了很久才到这个函数make_array_declarator,那么数组这个declarator是怎么组成的呢?首先,最尽头的cp_declarator是一个kind为cdk_id的数组名字,因为它的u.id.unqualified_name是一个IDENTIFIER_NODE。它的上一级的declarator是一个kind为cdk_array,而它的u.array.bounds指向的是一个TREE_CODE为NON_LVALUE_EXPR的数组的size。
  3. 怎样把parser的临时数据形成tree才是关键,然后在grokfield里又一次的遇到grokdeclarator14.08.2021这一次它的context是FIELD,我然后直接就想吐了,我对于这个函数实在是头疼,在它里面打圈子我很快就跟丢了。但是没有办法啊。要么先去游泳吧?太热了。

八月十四日 等待变化等待机会

  1. 抽丝剥茧终于看到羽化飞仙之前蜕变的过程了。怎么制作一个是array的tree呢?首先是基本的tree t = cxx_make_type (ARRAY_TYPE); 然后它的TREE_TYPE设定为数组的数据类型,我的情况是一个很令人费解的TEMPLATE_TYPE_PARM,其次是TYPE_DOMAIN设定为数组长度类型,我的情况是常数数字所以是INTEGER_TYPE。这个过程号称是最简单的数组类型的tree。
  2. 接下来是设定TYPE_CANONICAL,这部分比较复杂我不太理解,但是我的情况似乎很简单这个属性就是之前的tree本身。这个让我意外,这里的关键似乎就是如果数组数据类型和长度的tree对应的TYPE_CANONICAL如果和本身数据类型不同的话就要依法设置,否则就是指向自己。总之让我感到意外。同时,我看到TYPE_MAIN_VARIANT和我也没有什么关系,这个也让我意外,这个是针对variant的吗?这个到底是什么呢?到目前为止我得到的是一个ARRAY的tree。
  3. 接下来是制作所谓的TYPE_DECL的tree,先使用make_node制作骨架。然后设定DECL_NAME为数组的名字的那个IDENTIFIER_NODE,然后设定TREE_TYPE指向我们之前制作的ARRAY。毫无悬念的是这个TYPE_CONTEXT是我们当前解析的结构所以是RECORD_TYPE
  4. 这里看到一个我以为很重要的设定就是DECL_ORIGINAL_TYPE设定为ARRAY_TYPE,这个看起来是名副其实合情合理的。然后是一个看不懂的骚操作就是build_distinct_type_copy复制了一个自己然后设定TYPE_MAIN_VARIANTTYPE_NEXT_VARIANT组成的链表,我看的头晕,总之这里让我感觉这里的variant是有很特别的意义,以后要留心它的作用。TYPE_STUB_DECL指向了克隆前身的DECL_ORIGINAL_TYPE,这个是在暗示这个类型是的吗?而克隆的TYPE_NAME指向了原先的TYPE_DECL,这里我都看晕了,到底这些东西在哪里有文档描述呢?这些只能考验程序员的读码能力,但是不同场景的需求是不一样的,没有说明记忆力差就痛苦了。
  5. 看到了set_underlying_type的注释我觉得解答了我的疑问,这个无疑是很重要的,但是似乎和我的问题有些距离,我并不关心怎么回朔原先的定义,而是关心原先定义被替代之后丢失了typedef的信息。似乎是有联系的吧?这里解释了什么叫做variant,核心就是使用TYPE_NAME
    
    /* Setup a TYPE_DECL node as a typedef representation.
    
       X is a TYPE_DECL for a typedef statement.  Create a brand new
       ..._TYPE node (which will be just a variant of the existing
       ..._TYPE node with identical properties) and then install X
       as the TYPE_NAME of this brand new (duplicate) ..._TYPE node.
    
       The whole point here is to end up with a situation where each
       and every ..._TYPE node the compiler creates will be uniquely
       associated with AT MOST one node representing a typedef name.
       This way, even though the compiler substitutes corresponding
       ..._TYPE nodes for TYPE_DECL (i.e. "typedef name") nodes very
       early on, later parts of the compiler can always do the reverse
       translation and get back the corresponding typedef name.  For
       example, given:
    
    	typedef struct S MY_TYPE;
    	MY_TYPE object;
    
       Later parts of the compiler might only know that `object' was of
       type `struct S' if it were not for code just below.  With this
       code however, later parts of the compiler see something like:
    
    	struct S' == struct S
    	typedef struct S' MY_TYPE;
    	struct S' object;
    
        And they can then deduce (from the node for type struct S') that
        the original object declaration was:
    
    		MY_TYPE object;
    
        Being able to do this is important for proper support of protoize,
        and also for generating precise symbolic debugging information
        which takes full account of the programmer's (typedef) vocabulary.
    
        Obviously, we don't want to generate a duplicate ..._TYPE node if
        the TYPE_DECL node that we are now processing really represents a
        standard built-in type.  */
    
  6. 你以为这就完了吗?那就错了,因为完成TYPE_DECL是不够的,因为我们是在模板类的声明里我们要完成的是模板的声明,这里是很有教育意义的。所谓模板声明就是说通过DECL_NAME取的TYPE_DECL的那个IDENTIFIER_NODE制作一个TEMPLATE_DECL的tree,很明显的它的DECL_NAME还是指向那个IDENTIFIER_NODE,并且设定DECL_TEMPLATE_PARMS指向模板参数的那个TREE_LIST,设定DECL_CONTEXT指向当初TYPE_DECLDECL_CONTEXT也就是RECORD_TYPE。当然我的情况是最后这个TEMPLATE_DECLTREE_TYPE也是指向了原先的ARRAY_TYPE这里我忘记还有DECL_TEMPLATE_INFO的设定了:首先制作一个TEMPLATE_INFO的tree,然后设定它的TI_TEMPLATE指向我们的TEMPLATE_DECL,而它的TI_ARGS则指向模板参数。最后设定DECL_TEMPLATE_INFO指向我们的TYPE_DECL,这一点是让人费解的。这个看起来就完整了。这个就是所谓的模板声明TEMPLATE_DECL
  7. 这一系列的骚操作就是建立一套能够自由访问的指针链表,可以从TYPE_DECL通过它的DECL_TEMPLATE_INFO来访问它帽子下面的模板属性,也可以从TEMPLATE_DECL来回朔它的TYPE_DECL,而它们共通的一些比如TREE_TYPE是相同的。这个机制非常的复杂,在不同场景下又各自不同,我以为这个可能是gcc里最让人诟病之一吧?通过巨大的通用的tree union建立的内存最优化的结构体系以及结构体之间互相闭合指针系统的访问机制在效率上显然是无与伦比的,但是维护与扩展实在是太困难了。当然我也不知道有什么更好的方式。clang是怎样的呢?
  8. 拨云见日我总算从grokfield这个峡谷里钻出来了,得到的TYPE_DECL现在是可以理解的,接下来是什么呢?自然是把我们获得的这个TYPE_DECL作为当前的结构的TYPE_FIELDS了,这个是里所应当的,我也懒得看了,但是一个疑问就是如果我仅仅在后面得到了指向这个结构的所谓的RECODE_TYPE我不通过lookup_field之类的方法我能够轻易的获得我现在的这个TYPE_DECL显然是不可能的,一个结构有那么多的fields,不论是数据还是类型,肯定是不能简单使用获得的,因为这个数量太大了。实际上就是加入链表而已,没有什么好办法查找。不过我看到它又加入了一个所谓的CLASSTYPE_DECL_LIST,不知道这个是什么意思?是专门为类型定义安排的链表吗?
  9. 我有一种感觉就是针对一个结构里的类型定义和成员变量的处理应该是要不同的,单单靠一个id不去查找这个是变量还是类型似乎是不够的。但是这就是又一个大的战役了。感觉看过几千行代码也不解决问题。

八月十五日 等待变化等待机会

  1. 事情远比我想像的来的复杂,只能说明我的无知与天真。我昨天在担心通过一个id要查找到它所在的context的属于的类型要很麻烦的搜索,这根本就不是个事。我现在才意识到我已经手里握着那个id的tree了我却居然没有办法判断它是属于什么类型!这个简直是让人无语,我看到了创建数组ARRAY_TYPEDECL_NAME地址就是后来我在模板函数的参数里要解决的id的地址,可是昨天看到的都是从TYPE_DECL设定的方式,现在我有了INDENTIFIER_NODE的指针却不知道它从何而来,这个真的是笑话!感觉有一点点像是单向链表的意思。我现在开始疑惑parser找到id的时候发生了什么。
  2. 总之,模板的解析总是最复杂的,而我的情况并非最复杂的情况,还有非常多的情况比如模板的模板的模板。。。我不知道最深的模板有多少层,而且遇到默认参数的情况还可能会又歧义吧?我看到相关的代码注释才明白还有那么多的问题。真是想不到gcc它怎么做到的,无论怎么看都是奇迹,它不出错我觉得就是奇迹。当然这是因为我看不懂那些复杂的逻辑。A

八月十六日 等待变化等待机会

  1. 跟踪了一个早上才明白了一个简单的道理,我又走错了方向,这个函数cp_parser_make_typename_type的意义是什么呢?它使用一个hash搜索找不到结果就创建了一个代表什么呢?我完全没有理解就在这里浪费了无数的时间!上一次差一点就在这里改错贻笑大方!这里是它的名字所说的要创建一个typename的类型库,因为这个等于是再定义或者说依赖于其他类型的类型,那么作为一个类型来说要放在这个所谓的hash里以备重复创建,这个根本不重要,即便重复创建在我看来也不过就是内存浪费i一些而已不是什么错误!我却理解错了一开始始终以为是在搜索原来的类型定义没有找到,这让我非常困惑因为作为id我都已经拿到了原先的指针了怎么又报告找不到它之前的定义呢?这里我真的是闹了大笑话,这里的定义也许在第三句里模板特例化有可能再来查询可是我的问题出在第二句的模板定义,所以,完全无关!真的是好辛苦啊!每天前进几厘米一样的而且是蛇形的曲折,之前还大踏步前进与大踏步后退,两个星期前我还以为前面就是三四千行的代码而已,现在又一次失去了踪迹,看来我走错了方向了。难道要重新回到这里cp_build_qualified_type吗?或者是它的根源grokparms?后者似乎更有可能,毕竟前者是被无数次的调用的,本身是没有可能出错吧?
  2. 这里还有一个小细节就是关于typename只有当它是代表跟随的是依赖性的类型才是必须的,而对于我的第三句的模板特例化类型不再是依赖性的它是可以省略的,这是大侠指出的,我一开始并没有很理解。这些人都是多少年的打行家啊!一出手就知道高低深浅!而仅仅在我的帖子后一个星期我就看到几十个新的patch帖子被post,基本上我都看不懂,连边都摸不着,可以想见编译器的复杂,而我目前接触的大概是最最浅显的前端部分,以前教授讲编译器的工作95%以上都是在优化,而这也是最难的部分,而优化也许只是后端工作的一个小的部分可见前端在整个流程里的比重有多么的低。当然我不应该小瞧前端,这个是用户第一面对的,所有的错误应该在第一时间反映,否则后端出错实在是太难查找了。前端需要非常的坚实的一个基础。

八月十七日 等待变化等待机会

  1. 几乎又是从头来过,只不过我心里明白现在是在一个更高的起点的螺旋上升而已。
  2. 对于参数PARM_DECL通过它的TREE_TYPE获得TYPENAME_TYPE我的笔记里是记录清楚的,可是针对这个具体怎么解析就没有了,还是我没有找到?总之,
    1. 首先使用TYPE_CONTEXT获得的是一个RECORD_TYPE
    2. 通过TYPE_NAME得到TYPE_DECL
    3. 顾名思义TYPENAME_TYPE_FULLNAME得到的是它实际的类型名字也就是一个IDENTIFIER_NODE,不过这里分两种情况:到底它是针对typedef还是template
    4. 是否为typedef需要判断!DECL_ARTIFICIAL!alias_template_specialization_p,这个情况不是我的情况也比较难懂。
    5. 而模板的情况则更复杂,我实在是不忍再看了。
    不过从这个打印过程还是让我得到了一个概念,对于typename这种东西确实是比较讨厌的,你得到的TYPENAME_TYPE_FULLNAME是一个IDENTIFIER_NODE它是很多属性的终端,彷佛单向链表的末端怎么返回真的不容易,虽然说有context,可是我看到的那个record并不能轻易看到当初typedef的信息?还是说我没有找对思路?

八月十八日 等待变化等待机会

  1. 当你撞墙的时候最好的一个做法就是沿着原路返回然后审视每一个引导自己到达陷阱的每一个脚印。我查看一个月以前的笔记,可以清晰的看到我对于这个问题的认识是一步步加深的,其中有一个关键节点是关于语法的认识。因为今天早上我遇到了一个意外,对于函数参数const typename A<T>::arr我本来预计会有三次调用lookup_name之类的函数,依次应该是模板类A,模板参数T以及我们遇到的数组类型arr,可是最后一个没有发生,这就成了我探寻的重点,为什么没有发生呢?从语法的角度应该是怎样的呢? 关键是语法的第11步:
    
    typename-specifier:
    	typename nested-name-specifier identifier
    	typename nested-name-specifier templateopt simple-template-id 
     
    这里明确的要求在nested-name-specifier之后是紧接着identifier,结果就是语法不要求它是我们解析的所谓的scope或者说nested-name-specifier里的成员。但是这里我依然是有疑问的,究竟identifier是什么机制就确定的呢?这里我跟踪了好几次也没有答案,似乎这个节点总是突然就冒出来了,不过有一点似乎是明确的就是编译器不需要为这个identifier建立所谓的declarator吧?或者我的观察是没有了lookup_name。总之,我需要彻底解决这一个链条。它发生在函数cp_parser_nested_name_specifier之后,不过我感觉有一点点悲观的是我以前就注意到注释里使用的是很早的版本的语法,和标准里最新的语法有一些非常微妙的差别,也许是程序员从古代语法进化来的而保留原来代码的因素吧?总之,这里的解析逻辑和语法有可能有些许的不一致,结果也许一样否则问题就大了,不是吗?这个是连想都不要想的可能。不过这里让我对于语法解析有了新的一点认识,比如在nested-name-specifier之后到底要选择哪一个呢?这里我就看到parser尝试可选的templateopt再尝试simple-template-id失败之后才不得已相信identifier是正确的选择。只不过我现在迷惑的是这个id居然不需要lookup_name就能够准确的找到之前定义里的那个指针tree,地址我曾经比较过是一致的,这说明什么?我无法解释。
  2. 不知不觉,已经日上三竿日悬中天了,感觉这个尝试对于一个普通智商与精力的人来说实在是太困难了。因为智力不足精力来补是铁律,可是精力也不足只能依靠时间来补,那么时间呢?

八月十九日 等待变化等待机会

  1. 关于语法,如果感觉看BNF感到头疼的话,这个关于declaration的语法解释更加的贴心。而这里最最关键的还是怎么理解Dependent Names,这个问题我之前看到过但是只是了解了一个名词,这一次看样子是需要仔细阅读了。这里的原因是关于type specifiers里的typename specifier。而这里讨论的non-dependent-name-binding-rule是很容易理解的,因为它们不存在不确定性,和模板可以看作无关,即便它们出现在模板定义内部也是一样,按照ODR的原则,一旦被更改就视作违反一个定义的原则(ODR)但是仔细阅读我才发现这里着重讲的是所谓的ADL(Argument-Dependent-Lookup这个和我关注的有距离。
  2. 究竟dependent-name和dependent-type是什么关系呢?

    The following types are dependent types:

    • template parameter
    • a member of an unknown specialization (see below)
    • a nested class/enum that is a dependent member of unknown specialization (see below)
    • a cv-qualified version of a dependent type
    • a compound type constructed from a dependent type
    • an array type whose element type is dependent or whose bound (if any) is value-dependent
    • a function type whose parameters include one or more function parameter packs
    (since C++11)
    • a function type whose exception specification is value-dependent
    (since C++17)
    • the template name is a template parameter, or
    • any of template arguments is type-dependent, or value-dependent, or is a pack expansion (since C++11) (even if the template-id is used without its argument list, as injected-class-name)
    • the result of decltype applied to a type-dependent expression

    The result of decltype applied to a type-dependent expression is a unique dependent type. Two such results refer to the same type only if their expressions are equivalent.

    (since C++11)

    Note: a typedef member of a current instantiation is only dependent when the type it refers to is.

    我感觉我的情况包含了至少两种:
  3. 关于Type-dependent expressions我感觉太复杂,好像不是我遇到的问题,先放一边吧?本来嘛,我的问题就是type而不是expression,因为在函数声明里只有参数类型才是有效的,不存在表达式!
  4. 我找了很多相关概念突然发现Abbreviated function template这个似乎让人震聋发聩,原因是似曾相识,又好像司空见惯,但是细思极恐,这个并不是什么理所当然,因为这个是c++20的概念吧? 比如
    void f1(auto); // same as template<class T> void f(T)
    这个看起来人畜无害理所当然,可是背后的跨越有多么的大呢?可是让我惊讶的地方就是这里说是c++20才引入的概念可是编译器都泰然处之,似乎自从c++11引入auto就理所当然了?我做了实验才意识到这个是gcc的代码过于宽松,正如我以前所说的gcc代码并不是针对c++20重新改写代码,而是最新版里已经包含了新语法的支持,而对于不同c++版本的检查是没有办法做到严格的,换言之,最新版代码是按照c++20也就是最新标准来解析的,只不过在一些地方做了标准版本的检查,可能在目标代码部分做更加严格的审核吧?总之这个比clang差很多。甚至于说连msVC++都做到了!GCC还能自称自己是业界的事实上的标准吗?我觉得这是一个标准兼容性的bug,等我有空再提吧。但是我又犹豫不定似乎我找不到c++20 feature list有这一条。而标准里的Placeholder type specifiers (since C++11)是很早就有的,这个让人不是很确定,我还是多研究研究再说吧?
  5. 这里再重新审视一下GCC对于c++20支持的状态我看到这个很有关联性的议题,可以说这里typename的引入非常重要
  6. 我看到这个关于placeholder-type-specifier帖子相当不错,回答了我部分的疑问。

八月二十日 等待变化等待机会

  1. 在这个
  2. 查找标准发现这个决议似乎没有被各大编译器遵守。标准的例子
    template<class T> T::R f();             // OK, return type of a function declaration at global scope
    但是所有的编译器都说必须添加typename原因是T是dependent scope。
    
    error: need ‘typename’ before ‘T::R’ because ‘T’ is a dependent scope
        1 | template<class T> T::R f();
          |                   ^
          |                   typename 
    
    我犹豫了一下还是提了这个bug 这里的问题核心就是说template parameter scope是否是global scope?标准给的定义很简单:
    The global scope contains the entire program;
    基本上这个定义是对于实际操作是无用的,只能是用反证来定义
    every other scope S is introduced by a declaration, parameter-declaration-clause, statement, or handler (as described in the following subclauses of [basic.scope]) appearing in another scope which thereby contains S.
    我现在在想也许这个和是否是global scope无关,而是说这个是所谓的type-only context,因为这个id一定就是一个type,没有其他,period。
  3. 什么是locus?我在代码里看到很多次不明其所以然:
    The locus of a declaration ([basic.pre]) that is a declarator is immediately after the complete declarator ([dcl.decl]).
    这个简直就是绕口令,locus是一个declarator身后的影子吗?可是它是另一个declarator!而且是紧接着之前刚声明完的那个,所以,活脱脱就是影武者!这个例子说明了什么是locus,可是我已经没有胃口看了,这个和我有什么关系?我为什么要关心它?
  4. 只不过locus of template parameter是很奇妙的。
    
    #include<type_traits>
    typedef int T;
    template<class T
      = T               // lookup finds the typedef-name
    >
    struct A { };
    static_assert(std::is_same<A<>, A<int>>::value,"template parameter locus!");
    
    这个问题看起来简单,其实很复杂,我看到这个例子都要崩溃了, 你能想象为什么模板参数的作用域这么的短呢?只要一脱离就变成了另一个天地,这里val1的偏好不是那个紧密的模板参数的域,而是继承来的基类的typedef。这个例子说明什么?这个是编译器实现的主观行为吗?我看clang也是这样子,只能说这个是共识吧?,但是locus这个影武者根本和我的问题无关,它是解决特定declarator身后紧接着的那个重名declarator的问题。我没有这个烦恼。不过现在知道了locus好烦人啊,难怪代码里反复出现。
  5. global_namespace和global scope有什么关系呢?我找到一个小宏函数global_scope_p。它是基于比较当前传入域的namespace level和这个全局变量global_namespace来比较NAMESPACE_LEVEL (global_namespace),这个是一个cp_binding_level的结构,所以从这里来看这个宏函数接受的参数并不是tree而是这个注解里说的True if SCOPE designates the global scope binding contour,这个contour是什么概念?
  6. 我也反复看到所谓的late return type这里终于看到它是什么了。这个和trailing return type是一回事吗? 我试图使用模板的默认参数来解决,但是才意识到模板类型是无法作为表达式来结算操作结果的,也就是说这个是不可能的 所以,只有使用所谓的late return type 这个才是正解!
  7. 一个早上饥肠辘辘,完全偏离了十万八千里,我已经忘记了我怎么来的,为什么要来,要来干什么?从头来看最早scope的概念来自于继承的C语言,而它很简单就是我们熟知的四大领域: 但是我真的明白C语言的scope概念吗? 在C语言里for loop的body是所谓的immediate scope吧?或者说是nested scope,所以,你再次声明在for-loop-init的变量实际上是合法的。
    
    int main(){
            for (int i=0; i<10; i++){
                    int i=10;
            }
    }
    
    就算你使用warning选项你只能看到它是shadow而已:g++ -x c -Wshadow 1010.cpp注意这里使用-Wall只能发现unused variable的警告,只有-Wshadow才能发现这个警告。
    
    1010.cpp: In function ‘main’:
    1010.cpp:4:7: warning: declaration of ‘i’ shadows a previous local [-Wshadow]
       int i=10;
           ^
    1010.cpp:2:11: note: shadowed declaration is here
      for (int i=0; i<10; i++)
               ^
    
    但是在c++编译器眼里似乎这个nested scope是不接受的,甚至于我认为它就是一个for-loop-scope,否则为什么g++ -x c++ -Wall 1010.cpp报出这样子的错误呢?
    
    1010.cpp: In function ‘int main()’:
    1010.cpp:4:7: error: redeclaration of ‘int i’
       int i=10;
           ^
    1010.cpp:2:11: note: ‘int i’ previously declared here
      for (int i=0; i<10; i++)
               ^
    
    redeclaration的错误是基于ODR的理由的话那么一定是在同一个scope才会发生的,不是吗?所以,我才人为c++认为for-loop紧贴的不是nested-scope,就是说那个body-scope也是for-loop的scope,关于这一点可以很容易在语法里得到证明,但是我肚子太饿了。这个严重偏离了我的初始目标,但是如果不解决scope的问题,我似乎无法前进。
  8. 问题是究竟函数返回值的scope是global scope吗?
  9. 这个bug也许不能算是bug吧?但是它揭示了如果不是dependent-type是不能使用typename的,这个让我感觉有一些光亮,但是是什么我不知道。 这里的typename不被接受,说明GCC是严格遵循语法的吧?
  10. 实在是太累了,停下来吧。
  11. 闹了个大笑话!我怎么会没有使用-std=c++20来检验呢?我明明知道这个是c++20的新feature啊!这个Down with typename! (P0634R3)是c++20的新东西,就算是猪脑子也应该使用开关啊!吃一堑,长一智,我发现看代码如果查找cxx_dialect >= cxx2a会有很多的收获,比如这个是我今后读码的重点之一。而其中这个开关CP_PARSER_FLAGS_TYPENAME_OPTIONAL就是所谓可以省略typename的标志量。不过我原本的问题并不是c++20可以省略typename的根源,所以,这个只能作为学习怎么解决和typename相关的方法来参考。
  12. 这个关于c++20新feature的博客我实在是没有时间看了。

八月二十一日 等待变化等待机会

  1. 我有一个期待就是如果按照A qualified or unqualified name is said to be in a type-only context if it is the terminal name of parameter-declaration in a declarator of a function or function template declaration whose declarator-id is qualified, unless that parameter-declaration appears in a default argument这个原则能够不是把那个id作为普通的id,而是类型,我并不是指望去掉typename,但是如果它能够去做lookup也许会有帮助。我的感觉是因为它被当作dependent-type所以不去做lookup_name。但是这个想法也是落空的,clang也不接受省略typename,看来dependent-type是做实了,是毫无疑问。但是为什么它不是type-only context呢?看来我的理解应该还有偏差。
  2. 看到了lookup_template_class_1里是使用hash_table全局变量的find_with_hash来查找模板类的名字的。
  3. 其实很多地方标准是有严格规定的,比如关于typename是否遗漏有很详细的规定,而且对于是否需要diagnose都有规定,这就是我反复看到这类的xxx_diagnose_invalid_type_name函数的原因。
  4. 太热了,我只能记录下目前最好的gdb的断点方法思路:
    1. cp_parser_declaration里的else if (token1.keyword == RID_TEMPLATE)设断点, 这个作用是清晰的分段三个模板语句的开始。
    2. grokfield里我看到模板类的成员的typedef解析为一个TYPE_DECL,而它的TREE_TYPE依然是一个ARRAY_TYPE。同时我记录下则个tree的地址,看看事后能否找到它,但是困难的就是随后在各种腾挪闪转中它淹没在一层又一层的嵌套里。当然我无论在何时何地使用gdb打印它的地址依然可以看到它,比如
      
      (gdb) p TREE_CODE(TREE_TYPE((tree)(0x7ffff7270b48)))
      $1411 = ARRAY_TYPE
      
      可是问题是我不知道要怎么使用手中的线索找到它的路径。
    3. 在decl.c:grokparms函数里的type = cp_build_qualified_type (type, 0);设断点,因为这个就是关键的之所在,它计算函数的参数类型的时候根据所谓的函数的top-level cv要drop掉的原则就直接传入参数0代表参数的const要忽视掉,可是我要在这里怎么说服编译器这个类型是typedef而且它的参数类型是一个数组不能这样做呢?这就是所有的难题所在,因为目前我们在模板函数的声明阶段,其中的函数参数是一个dependent-type,原则上是不需要lookup_name,也不能吧,而这里看到的这个typename盖头下面又是那么复杂,即便我按照打印的原则追踪到了最终的类型数组的名字它也就是一个id,而我没有任何其他证据说明它是一个类型这里似乎可以使用type-only context的概念,可是即使如此也没有用啊。归根姐弟还是当初大侠们的断言,这个在计算函数签名时候还不知道它的确切的类型,所以按照一般原则就把函数参数的top-level cv-qualifier去掉了,可是后来等到模板函数的特殊化的时候已经对不上了,因为一个有const,一个没有了,因为后者计算实例化的时候const是被保留下来的。
    这个几乎就是我差不多两个月的结果,只有一小部分就是在grokfield之后怎么腾挪被加入了所谓的CLASSTYPE_DECL_LIST我要去深挖一下,但是我也是很悲观,原因就是数组变量名在解析里就是一个id,怎么回朔得到它真实的类型这点我始终找不到。
  5. 在山穷水尽的时候,我常常想,能不能参考clang的代码,它的做法应该容易看懂的多,但是核心问题就是即便知道了clang的做法,也未必能够得到什么借鉴,原因就是目前的parser的逻辑就是对于qualified-id就按照语法就是一个terminal,不需要再去查找了,这个让人很悲观,我始终不清楚在lexer得到的token是怎么转化为parser级别的token的,这中间有一些断层,因为我看到代码是事后尝试失败后才设定cp_token的属性,但是作为一个identifier的token它的地址对应的tree是怎么得到的,没有查找就能够指向之前的那个,这是我始终解不开的谜。
  6. 又是一个碰壁迷茫的一天。

八月二十二日 等待变化等待机会

  1. 似乎是峰回路转,有时候事情来的太突然以至于你不太敢相信,夙兴夜寐之后你往往变得淡定,因为你不再那么奢望单纯的运气能够解决问题。所以,为了验证我决定彻底测试,那么工欲善其事,必先利其器,第一步我把以前下载的编译jGCC的脚本再完善一下实际上这个基本上就是我以前做好了的,但是我自己居然忘了以至于丢失了,现在我意外的发现了它,很快的修改完善了,其中最重大的改进是不再手动下载GCC的配套程序,而是使用自带的下载工具download_prerequisites来下载。我相信它也很容易扩展为编译snapshot,这个随后再做吧。
  2. 主要的改动实际上我以前已经注意到的这个函数resolve_typename_type,它可以帮助你找到typename背后的隐藏的类型。所以,只要判断在grokparms函数里在cp_build_qualified_type之前调用得到真实的类型看它是否是array就可以决定是否应该保留const了。就这么简单但是花了我两个月。接下来要让别人有信心我自己先要有信心,所以,这一次我要严格比较make check的结果,这个将是漫长的过程。可能至少一个星期吧?
  3. 经过一天的make check考验似乎没有问题,我也不过就是比较我的版本和正版的check的结果。然后我设想了一个更加丧心病狂的例子,结果GCC又失败了。 clangVC++表示毫无压力!唉!这就是号称当今业界标杆的GCC啊!我只能顺手再添加了一个补丁,我对于自己能够不到半小时就改出来表示不可思议,为什么之前花了两个月而现在犹如神助?其实我是闭着眼睛在resolve_typename_type添加了一个根据我的结果返回的选择,这个是属于no-brainer,我压根不明白为什么只是看到lookup_member找到了我需要的却没有返回就造了这么一个返回结果
    
      /* If we failed to resolve it, return the original typename.  */
      if (!result)
      {
    	  if (TREE_CODE (fullname) == TEMPLATE_ID_EXPR
    			  && TREE_CODE (decl) == TEMPLATE_DECL)
    		  return TREE_TYPE(decl);
    	  return type;
      }
    
    原本这里直接返回return type;,我把我的条件加三儿塞了进去。如果再嵌套一层会如何呢?
  4. 果不其然GCC还是不行 这一次我是有些绝望了,因为这个需要递归查找我觉得这个函数让我继续改下去是要出问题的吧?GCC需要一个系统的解决方案。

八月二十四日 等待变化等待机会

  1. 我开始怀疑GCC这个模式是否能够持续下去,因为c++标准不像二十年前那个时代缓慢推进,在今天层出不穷的新feature被标准采纳,而那些有十几年高龄的代码能否适应新的快速变化的要求呢?我决定随便提交了几个bug了事,可能要休息一下,回头看看。
  2. 我胡思乱想了一些例子本来预计不可能编译的,没想到GCC还是可以的,这个让人重拾了一些信心 我保留这个例子主要是我一开始不知道怎么声明数组的引用这个问题我已经反反复复的遇到了,但是始终都忘记!
  3. 还有一个小细节是我吃惊不已的就是这个模板声明居然说不认识size_t,原来size_t不是原生的数据类型,需要cstddef这样子的头文件,有人建议使用decltype(sizeof(int))代替。
  4. 我希望依靠多提几个类似的bug来引起相关大神的重视。
  5. 对于这个测试例我完全没有概念它是怎么来的 clang不在乎的说这就不是事!,而MSVC++弱弱的说不太对吧? GCC给出的错误让人摸不着头脑。

八月二十六日 等待变化等待机会

  1. 改动了一下,简化了编译GCC的命令
  2. 使用contrib/compare_tests输入之前运行make check的两个gcc/testsuite目录就可以得到很好的比较结果。
  3. 而使用contrib/[legacy/]mklog输入之前git diff HEAD产生的diff文件可以产生出文件list。
  4. strip_typedefs这个函数很有意思,大师的这段注解我还没有完全理解:
    
      E.g. consider the following declarations:
         typedef const int ConstInt;
         typedef ConstInt* PtrConstInt;
       If T is PtrConstInt, this function returns a type representing
         const int*.
       In other words, if T is a typedef, the function returns the underlying type.
       The cv-qualification and attributes of the type returned match the
       input type.
       They will always be compatible types.
       The returned type is built so that all of its subtypes
       recursively have their typedefs stripped as well.
    
    这里的思想是什么呢?
  5. 昨天我才意识到我的这个bug是不能用老办法解决的,因为decltype不是typename,我没有一个工具去解析它背后的类型是什么。 我的想法是尝试看看能不能忽略所有的decltype就像我对于不能解析的typename一样,当然我的鲁莽的尝试完全忽略任何类型根本就编译不过去,说明对于普通类型来说需要严格执行drop top-level-qualifier的命令,可是我始终觉得应该需要一个能够分辨是否是top-level-cv-qualifier的函数才是最好的解决。也许我有时间去clang看看。
  6. 事情是越来越复杂,对于没有typename指引的情况怎么办呢?这个问题更加的难办啊! 我只好在decltype的情况之外再加上dependent_type_p,这个需要重新完整编译才能知道行不行。 看样子是不行的,经过比较这个测试例有问题
    
    struct A;
    template <class T> void f (void (A::* const)(T)) {}
    void (*p)(void (A::* const)(int)) = f;
    

八月二十七日 等待变化等待机会

  1. 我花的时间越多就越感觉自己的渺小,这个是前辈们的努力,因为这个core1001/1332的问题是如此的复杂,这个pr92010是另一个努力解决的方向。我在反复修改我的评论的过程中更加深刻的领悟了我之前的工作,说白了就是依靠typename的提示来解决这其中的一部分问题,可是如果遇到decltype是否一定成立呢?这个是我最感到惴惴不安的,我简化了之前的例子 这个是我能够解决的,可是保留const是否是正确的呢?

八月二十八日 等待变化等待机会

  1. 我每次改写我的提交评论就发现一个新领域,然后为自己的无知无畏而羞耻。总之,我现在开始体会一些前辈反复修改阅读自己的写作的那个体会,思想是需要反复打磨的,每次你推翻自己就是前进一步。顺便说一下,我今天验证了一个小小的东西。 这个在gcc-11.2修正了这个错误 而这个与我的问题关系不大,因为你如果把specialization放到decltype的语句之前就没有问题了,就是像错误说的那样这是一个编译器误认为decltype是一个实例化的问题。
  2. debug一晚上居然是工具或者说测试例本身的问题!我一开始百思不得其解为什么我的检验结果有问题,最后才意识到居然同样的结果比较工具compare_tests两次测试结果不同!这个实在是让我无语了,归根结底就是这个测试例居然出现XFAIL和PASS同时出现的结果!这个是允许的吗?我不知道。总之这个是把我纠缠到抓狂的结果,而另一方面我使用make check-gcc单独测试我的测试例就是失败,可是如果我运行可执行程序并没有看到错误,难道这个是运行的什么不同的程序?让我strace看看。
  3. 居然发现我的测试例被GCC报错,使用strace探查是痛苦的,因为我一开始不知道使用follow-fork结果是一头雾水,后来才终于使用-ff,因为我的版本居然不支持这个长的开关名!strace -ff make RUNTESTFLAGS="dg.exp=pr102034.C" check-gcc 2>&1 |less最后看到原因居然是在调用cc1plus的时候runtest使用的是严格的参数-pedantic-errors,而它暴露出了一个内部的错误
    
     错误:keyword ‘template’ not allowed in declarator-id [-Wpedantic]
       11 | void f(const typename A<TA>::template B<TB>::Arr3){}
          |                                       ^~~~~
    
    这个就导致我来到了这个极其著名的帖子,我只看了几句就知道是高手写的,可惜我已经精疲力尽了,明天再看吧?
  4. 到现在为止看起来我的patch的测试例子并没有什么大障碍,应该是没有问题吧?可是我依然有些惴惴不安,毕竟还有一个是时间戳的测试例我屡屡遇到,我怀疑这个肯定跟我同时运行多个编译和测试程序笔记本超负荷的关系因为它是cpp的部分,我的c++的代码部分修改怎么会影响到预处理器呢?总之下午当我看到结果有出入时候是非常的沮丧,现在好多了,看起来应该是工具和测试方法本身的问题。
  5. 为了压制这个-Wpedantic我是无法在开关上做文章的,因为check-gcc的测试例是固定使用这个开关运行的,我是无法关闭吧,所以只能在代码里加上
    
    #pragma GCC diagnostic ignored "-Wpedantic"
    
    30.08.2021现在我意识到实际上这个是一个c++98的问题,就是说在多重的template里只需要一个template的关键字就足够了
    
    void f(const typename A<TA>::template B<TB>::Arr3){}
    
    第二个template在c++98里是不接受的吧?-std=c++98 -Wpedantic
  6. 顺便说一下,在git里新添加文件在git diff里要显示的话必须使用git diff HEAD
  7. 今天累死了忙到了半夜。

八月二十九日 等待变化等待机会

  1. 我再次读这个dependent-type感觉其中博大精深。如果说要寻找理论依据就应当在这里,目前我希望能够实验是否所有的typename都是dependent-type。或者说凡是dependent-type就适用于不能binding的原则?
  2. 昨天太累了,今天来好好研读这篇著名的帖子
    1. 首先是为什么的问题,这个是一切问题的出发点和原动力。因为在其他弱类型语言里不是问题的问题,在c++里是大问题。就是说编译器随时随地都要知道自己在干什么,怎么干。也就是说不能有丝毫的模糊性。一个id是类型还是变量名是兹事体大的问题。我上一次看到那篇c++20消除typename的论文好像题目是down typename吧?里也是相关的就是在某些情况下typename是不必须的,因为除了类型它不可能有别的可能性。我记得它也是说的所谓的dependent-name吧?还是nested-specifier?总之这里说的是标准里的name lookup的概念
      Some names denote types or templates. In general, whenever a id is encountered it is necessary to determine whether that name denotes one of these entities before continuing to parse the program that contains it. The process that determines this is called name lookup.
    2. 另一个概念是dependent name的概念,但是这个是特指模板内的,所以,我觉得我的代码里要加入这个是否是模板声明的检验吧?不过这里仅仅是dependent name的一个情况吧?就是模板不论是声明还是定义都必须依赖于实例化才能做name lookup。,不过我现在看这里也是开篇就说inside the definition of a template
    3. 这里的一个细节是模板按照模板参数是否是类型参数而分两大类的:参数是type还是non-type这个也是一个大问题。
      In particular, types and expressions may depend on types of type template parameters and values of non-type template parameters.
    4. binding rule是另一个大概念。这句话是很好理解的
      Non-dependent names are looked up and bound at the point of template definition. This bindingholds even if at the point of template instantiation there is a better match
      那么真的有better match这个情况吗?
      If the meaning of a non-dependent name changes between the definition context and the point of instantiation of a specialization of the template, the program is ill-formed, no diagnostic required.
      这里应该就是我的那一家族的bug的理论依据,就是在特例化的时候找不到而判定出错,前提是它把那些dependent name当作是non-dependent name了,是吗?我的情况和dependent name意义改变无关,是函数签名,或者说声明与特例化之间的不一致。想多了。
    5. 但是因为它是dependent name导致它的lookup被推迟了
      the lookup of a dependent name used in a template is postponed until the template arguments are known, at which time
      • non-ADL lookup examines function declarations with external linkage that are visible from the template definition context
      • ADL examines function declarations with external linkage that are visible from either the template definition context or the template instantiation context
      这里其实非常的难懂,首先,我虽然明白ADL是什么,但是怎么判断的?意思应该都是是在本scope范围内找不到函数的声明对吗?所以non-ADL才要看external linkage,这里也许是说已经使用ADL-lookup了吗?这里的template definition context做何解呢?ADL是明显的,肯定就是要去模板名的所在的模板定义项下去找?总之这里其实很复杂的一个过程。我在代码里看到不少的lookup但是不理解什么时候用什么。
    6. dependent type之前我就望而却步,现在依然是看的心惊胆战。我感觉我如果纠缠于细节我永远都无法从这个大泥坑里爬出来。暂时放一边吧。
    7. 我终于找到理论依据
      A name used in a template declaration or definition and that is dependent on a template-parameter is assumed not to name a type unless the applicable name lookup finds a type name or the name is qualified by the keyword typename.
      这里我们学到了什么?就是说typename不是随便给的,如果不是dependent name是一个dependent type的话是不能随便给typename头衔的,就像例子里的,如果
      
      typename T::A* a6;          // declare pointer to T's A
      T::A* a7;                   // T​::​A is not a type name:
      							// multiplication of T​::​A by a7; ill-formed, no visible declaration of a7
      
      第二个声明里没有冠以typename,它就是一个变量那么*就不是解释成ptr-declarator,而是乘法了。这个虽然是常识,但是我因为被那篇削减typename的论文搞糊涂了现在遇到都会犹豫。现在看来不是我一个人有疑惑 现在看来这篇论文真是应证了那句名言人狠话不多!就那么几个字我看的头晕目眩,信息量极其的大,说一句顶一万句也不夸张!我实在是没有能力读了。因为牵扯到parameter-declaration的情况就有五种,可是我算哪一种呢?而且它是允许不使用typename,并不是不可以。似乎很难把握吧。至少我怀疑编译器没有真的严格执行。
    8. 又饿又累,基本上还没有看一半。疑惑却越来越多。
    9. 我现在尝试简化成这个样子
      
       int type_quals = 0;
        /* Inside template declaration, typename and decltype indicating
      	 dependent name and cv-qualifier are preserved until
      	 template instantiation.
      	 PR101402/PR102033/PR102034/PR102039/PR102044 */
        if (processing_template_decl
      		&& (TREE_CODE (type) == TYPENAME_TYPE || TREE_CODE (type) == DECLTYPE_TYPE))
      		  type_quals = CP_TYPE_CONST_P(type);
        type = cp_build_qualified_type (type, type_quals);
      
      关于decltype我现在还找不到理论又据只是测试证明暂时没有问题而已。

八月三十一日 等待变化等待机会

  1. The syntax allows typename only before qualified names这句话画龙点睛。
    After name lookup (3.4) finds that a id is a template-name, if this name is followed by a <, the < is always taken as the beginning of a template-argument-list and never as a id followed by the less-than operator.
    我似乎看到过类似的代码在GCC里,看来严格遵守标准是无二法则。但是作者的那个模板的例子我觉得似乎不是很有说服力,因为我想不出怎么能够在同一个名字空间重复定义,这个违反了ODR吧?
  2. 可以说是数易其稿,惴惴不安,最后思前想后还是提交了我这个耗费了整整两个月的不懈努力的补丁,一个原因是我觉得我的能力就到此为止,而且最重要的是人微言轻,即便提交了也未必采纳,空耗力气于事无补。
  3. 我对于这个bug感兴趣。

九月一日 等待变化等待机会

  1. 又进入一个比较烧脑的领域:TYPE_ARGUMENT_PACK是什么?就是模板的类型参数,我不知道怎么翻译parameter pack。那么要访问它可以使用ARGUMENT_PACK_ARGS,这个是一个TREE_VEC。与之相对应的是TYPE_PACK_EXPANSION,这个我现在脑子比较乱,这个是声明部分吗?可以使用PACK_EXPANSION_PATTERN取的那个领头的id。还有什么尾追部分。牵扯到parameter pack,别说编译了,就是使用我都犯难。
  2. 但是这个难度肯定是不一样的,因为很容易就看到在parameter pack的情况下的一系列的所谓的unify之类的函数最终比较模板函数的参数类型和要转化的函数指针的参数类型,一个是指针一个是数组。当然是函数参数如果是数组要转化为指针这一步没有做。但是问题出在哪里却不是那么容易找到的。最好的办法就是比较正确的做法,于是我就不用parameter pack,得到正确结果的路径来看问题。这肯定是一个降低了难度的debug过程了。然而还是很辛苦的。先吃两根玉米来充饥吧。

九月二日 等待变化等待机会

  1. 对与错永远是相对而存在的,在你知道什么是错误的之前,你必须知道什么是正确的。然而阴在阳之内,不在阳之对,矛盾仿佛是基本粒子的磁极子一样总是成对存在的。那么在定义正确之前能否定义错误呢?显然是不行,同样的,能够在定义错误之前定义正确吗?
  2. 在debug错误之前先看看这个正确的结果是怎么来的吧 首先这个是一个初始化过程的赋值动作,lhs是指针POINTER_TYPE,rhs是所谓的模板表达式TEMPLATE_ID_EXPR,这个建立转化的过程其实是相当的复杂,TREE_TYPE(rhs)是一个所谓的LANG_TYPE,所以这个不是简单的一一对应的转化,而是所谓的implicit_conversion,因为lhs不是引用所以GCC选择普通的转化standard_conversion,所以我们要转化的目的类型是指针POINTER_TYPE,而源头类型是LANG_TYPE这里据说如果表达式的类型是所谓的unknown_type_node也就是说type_unknown_p (expr)那么它就是函数,这个让人比较的错愕,这就是GCC里的gimple的最让人烦恼的东西,没有多少系统的文档,因为大部分都是程序员在设计时候写在注释里的,仿佛当年《九阴真经》写在一本佛经的字里行间等待有缘人自己来发掘。
  3. 总而言之我来到了我最最关心的核心地带:如何把一个模板表达式合法的转化为一个指针类型,这个函数就是instantiate_type,这个就是揭开谜底的关卡之战了。对于这个今天早上可能的进展我打算保留一会儿,休息一下了。
  4. 早晨起来散步冷风吹面才发现气温只有13度,路面半湿才知刚才下过小雨,公园里果树坚果时常掉落,松鼠忙碌的东收西藏有感而作。
    乍起秋风扑面寒,
    朝雨匆匆路半干。
    秋叶秋实随风曳,
    落与不落都两难。
    
  5. 实际上路还很长,首先作为一个模板表达式,需要做的是使用TREE_OPERAND,参数为0取得的是OVERLOAD,参数为1取得的是模板参数是一个TREE_VEC。这时调用的resolve_address_of_overloaded_function才是关键,这里牵扯到我看过很多遍名字的partial order,这个是关于模板特例化的一个很复杂的概念,我至今也不敢接触它的边缘,究竟什么才是更加的特殊化是一个很复杂的算法吧?
  6. 对于一个模板表达式得到的所谓OVERLOAD使用所谓的lkp_iterator可以得到所有的TEMPLATE_DECL,这里我有点理解什么是特殊化了,因为本质上还是针对同一个模板id的所谓的模板声明。所以这个函数算法的核心就是从表达式得到的所谓的explicit template argument和从模板特殊化的模板声明里的模板参数进行比较。后者是用DECL_NTPARMS配合make_tree_vec复制了一个参数来比较。
  7. 另一个细节是我一直不太明白怎么从函数FUNCTION_TYPE声明里取的参数列表,这个是TYPE_ARG_TYPES
  8. 所以,现在的核心实际上是fn_type_unification它的目的是:使用以上所说的各个遍历的模板特殊化得到的模板声明取出它的声明的模板参数列表,和我们从要转化的目标函数指针的函数参数做目标转化,使用的是所谓的表达式里得到的用户提供的显式模板参数列表做源头,所以,这个是比单纯的比较来的复杂,因为我们还有目标参数类型要转化,似乎是一个三方都要照顾的算法吧?fn_type_unification这个才是核心?这里我好像是多虑了因为模板函数的参数类型是依赖于模板参数类型的,所以这个dependent-name是不会被解决的。我似乎找错了地方?
  9. 今天浪费了大部分的时间,现在如果有太阳也是日薄西山了,肚子饿了,啃玉米吧?

九月三日 等待变化等待机会

  1. 这个三方博弈让我看的头昏脑胀,首先要分清楚模板参数和模板函数的参数,前者通常是TREE_VEC,后者通常是TREE_LIST,而这里的explicit_parms是模板显式参数,但是在特殊化匹配里的目标是什么?通常就是在overload里匹配那个最接近explicit param的模板声明,可是在resolve_address_of_overloaded_function似乎多了一个转化的因素,这一点我有些糊涂了,目标是什么?目标是你先把lhs的类型提取出来作为目标传入这个函数,不存在转化的问题。也就是说我们就是要寻找指针类型,这个其实都是废话,因为函数的名称很清楚,这个是专门为指针类型解决地址的,不是吗?我对于GCC的代码处于完全陌生的状态才会有这种疑惑,如果真正写过编译器代码的话就不会有这种迷惑了,地址变量的解决是一定一个单独函数的。这里和我的问题无关。所以,特殊化解决就是通常的逻辑,我们需要的是由用户提供的显式模板参数决定的和OVERLOAD中声明的模板是否匹配的问题,至于返回一个指针是匹配之后的问题。其实我意识到这是一个心理现象,我因为一直关注于函数参数里数组转化为指针的问题,对于指针类型特别的敏感,结果模板函数特殊化匹配返回的是函数,等式左边是指向函数的指针是类型兼容的,这里没有什么特别的,所谓的解决地址是形象的说法,函数即地址,地址即函数,函数指针和函数本身没有什么类型兼容的问题。
  2. 看来我还是轻敌了,凡是涉及模板特例化的代码都不是简单的代码,这个是我的教训,而且奋战了两天还没有搞明白非parameter pack的原理,就更不要说parameter pack的特殊处理了,这个绝对不是一个简单的bug,因为这个bug从提交到有人确认就花了一年多时间,你可以说这就是GCC的风格,可是单单敢拍板说这个是一个bug就需要绝对多的能力与资格。如果把GCC和linux kernel来相提并论的话谁更难呢?大多数人都是认为是后者,可是不要忘记了前者的早期的C语言版是后者的基础,甚至两者是手拉手成长起来的,如果编译器出错了兹事体大啊,你还能相信谁?不是任何程序员都有能力查看汇编码来验证自己对于代码正确性的怀疑的,这个太难了,你首先要敢说你的代码没错是编译器错了,这个胆识恐怕不是普通的大牛才能做到的。当然你也许可以说c++语法和c语法是两回事,c编译器的可靠性和c++编译器的可靠性也是两回事,但是现如今有多少人敢拍胸脯说是编译器错了而语法没有错?这就是困难所在。我总算给自己找了一个不错的理由去休息一下。
  3. 如果我说GCC的代码是一团浆糊会不会招致很多人的仇恨?我在这个两行代码的parsing里的那句转化代码里同样的instantiate_type竟然被调用了三次,这个不是一个小函数,背后调用的resolve_address_of_overloaded_function也不是一个简单的函数,究其源头都是typeck.c:convert_for_assignment里有多个分支反复调用导致具体来说在这个函数里面第一次在can_convert_arg_bad里导致了依次调用,更夸张的perform_implicit_conversion_flags这个函数居然会调用两次!让人看了叹息!如果能够看到GCC每天提交的bug数目,就知道积重难返,像这类不是要命的问题是无暇顾及的。毕竟是一个开源项目,在计算机界里这个相当于做慈善了,你还能要求什么呢?无数牛人都是在其他下游赚大钱,这里能有人给你维护更新就谢天谢地了。,也许这个是历史缘由吧?可是这个实在不是什么好现象。我常常想现代软件工程里那么多的实践方法论工具不知道有多少可以应用在GCC这个工程上,难道没有听说过unit test或者说使用某种工具来静态分析代码执行路径?啊,且慢也许这很多的工具都是依赖于GCC的正确执行?至少大多数人都把眼光关注在目标代码以及优化上了,至于说前端这个不屑一顾的本科生都能干的工作不需要太操心,难道现在都已经进入人工智能阶段了我们人类连一个基本语言的编译器的语法解析都做不对?这个简直是天大的笑话。偶尔有些小的歧义那不过是c++语法委员会指定标准自己都遗漏的模糊地带,要不就是错误信息表述不够人性化,毕竟语法解析说的都不是普通程序员能够听董的语言嘛。这样子的解释还不够吗?我想我肯定是想多了。
  4. 当然你也可以说这个是我的误解,因为无非是一个看似无害的函数的调用的副产品,比如从名字上看can_convert_arg_bad返回bool,看似一个人畜无害的试探性的常量函数,但是谁能想到转化是如此的复杂的过程呢?因为这个失败了当然就结束了,可是成功了最后就全都要调用最后的perform_implicit_conversion_flags,可是后者为什么会调用两次呢?看下一层的stack,居然交叉调用了implicit_conversionconvert_like_real,至此我似乎也只有哑口无言了,GCC里有很多这种类似尝试与失败的调用方法,这个也都有它的道理,看来是我的无知了吧?因为第一次的调用是一个本来只是尝试的函数,后面转化再次尝试两次肯定是有道理的。只是这其中的奥妙非等闲凡人所能参悟。我还是闭嘴吧!
  5. 看来我是有些太乐观了,尽管我使用gdb跟踪了两个月我对于GCC的基本AST的数据结构依然非常的陌生吃力,使用gdb根本是一个最低级debug的过程,几乎不需要对于代码有多少了解,所以,即便你使用这个笨办法找到了bug的源头不代表对于代码逻辑有多少深入的了解,这个本身就是一个很机械的底层工作,所以,不要太高估你的能力,我只是一个三体人眼里的可怜的虫子,在虫子去填饱可怜的肚子之前记录一下今天的进展,还是要关注instantiate_template_1,因为这里的结果直接影响了函数参数匹配的结果,总之,我看到的结果就是这样子的。

九月四日 等待变化等待机会

  1. 终于看到了我想看到的东西,然而我却不知怎么办,因为这个是非常复杂的过程,比如这个tsubst包含了一个家族的函数tsubst_decl, tsubst_function_decl, tsubst_function_type, tsubst_arg_types然后就是这个type_decays_to把数组蜕变成指针,可是这个过程真的很复杂,我在反反复复的tsubst_xxx的函数里兜了一圈又一圈。而我对于这段代码pt.c:tsubst_arg_types的正确性感到迷茫
    
        /* Do array-to-pointer, function-to-pointer conversion, and ignore
           top-level qualifiers as required.  */
        type = cv_unqualified (type_decays_to (type));
    
    怎么知道这个就是top-level-cv-qualifier?

九月五日 等待变化等待机会

  1. 我试图用一个正确的parameter pack的替换来比较这个问题,结果发现模板特例化是同源的问题
  2. 我想尝试clang的前端,可是看到代码里包含llvm的内容就意识到前端和后端是紧密联合的,因为它们彼此依赖的。
  3. 重新回归本源看看语言的语言。这个对于CSG的定义更有意义。

九月六日 等待变化等待机会

  1. GCC里的代码有时候写的很聪明就是说能省则省,非常的令人讨厌,不要轻易的认为代码有问题,即便有问题也是有理由,老牌的程序员就是这种怪脾气,代码没有错是改bug的人改出来的错,如果一定是运行结果有问题,那就是标准改了,总之代码没有错,是读码的人水平不够看不懂,如果,你一定认为代码错了,那是你的母语不是英语,你看不懂注释。这种牛气冲天的霸道让无数的自认为不够霸气的程序员去了clang。这就是GCC和clang的故事。
  2. 遇到TEMPLATE_TYPE_PARM怎么办,首先它是一个type,所以就看它的TYPE_NAME,通常是一个TYPE_DECL既然是declaration,那么就看它的DECL_NAME,十有八九它是一个id,这个不是很顺理成章吗?
  3. 总算明白了一个道理,这个是一个其实很大很复杂的专题,就是说pack parameter的核心是参数的个数的不确定,但是当你使用用户给的具体的实参的时候必然有一个所谓的type unification的过程,简而言之就是把实参和不确定个数的型参的比对统一的过程,实际上也是一个验证声明与表达式之间是否合法的过程,这个的确是一个非常复杂的过程,其中的代码非常的复杂,想想看单单使用这些...就是程序员的一个噩梦不要说编译器怎样条理清晰的检验匹配了,我个人对于这些让人看着发毛的小点点是有说不出来的敬畏之心的,因为不知道怎么才是正确的使用方法。
  4. 那么这个过程的一个路径是这样子的determine_specialization->get_bindings->fn_type_unification->type_unification_real->unify_pack_expansion,在这个底部的函数检查一个一个的参数类型是否匹配,就报错了,之前的非parameter pack的函数在模板替换的过程就已经知道是什么参数类型了自然就进行了所谓的type_decays_to转化数组为指针,可是对于parameter pack那个时候不知道是不是还要等到type unification才知道类型结果是没有开始,所以就错过了,到这个时候已经太晚了。结果发现函数指针类型不匹配就失败了。所以,花了好几天才有了一个大致的方向,关键还是类型的unification过程的机制在哪里,是怎么发生的。
  5. 总是感觉哪里有不对,似乎我的理解有问题,比如一个模板函数的声明是template<class ... Ts> void foo(Ts ...),使用DECL_NTPARMS得到它的模板参数是1,那么而实际的或者是explicit argument是2,这样子就是一个unifiy的过程,对吗?对于函数特例化的问题就是我也许还有一个模板特殊化的声明void foo(int*, char*),相对应的就是如何把用户显式声明的模板参数int []char []进行验证匹配的过程,那么这个过程应该发生在get_bindings,因为在随后都是一个参数一个参数和型参的比对,根本没有函数原型的角色,因此是纯粹的类型比对,根本没有函数类型的type_decays_to的机会了。这个似乎是把问题定义了一下,对不对明天检验吧?有点饿了。

九月七日 等待变化等待机会

  1. 定镜一看还是回到了那个神操作的函数fn_type_unification,且不说这个函数代码写的有多么晦涩难懂,单单一个传入参数的形态就让我吐了一口血,传进了一个tree的指针,我纳闷了半天才明白这个是名副其实的数组,我的天为什么不采用通常的list或者vec,却随手一挥使用一个预分配的数组来传递?原本函数参数好好的放在tree_list里,这位大侠看的不顺眼苦心孤诣的分配一个数组硬生生的把每个list的元素放在这个原始的数组里,这个一定有什么深意?因为通常函数参数使用List,模板参数使用vector,这个是惯例,结果它突然使用一个原始的tree*指向一个纯纯的数组,这个是嫌看代码的人不专心搞不清楚谁是谁吗?我在模板参数,模板显式参数,函数参数,模板推理参数几个打转转看的我都要气晕了,这位前辈果然非同凡响,诚心诚意的不想让资质低劣的普通程序员看懂他要干什么。总之,我是一肚子气没处发泄。
  2. 我差点就被气疯了那个所谓的list_length怎么和我的参数列表长度对不上呢?而且我打印的参数列表和我在gdb里看到的怎么不一样呢?难道说我运行了几个星期的gdb内存有问题?我打算存下所有的断点save breakpoints [filename]重启电脑再看看。
  3. 一个早上折腾了一个小问题,就是关于list_length对于一个TREE_LIST的长度为什么总是多了一个呢?原因是你必须检查节点的类型是以void结尾。而如果是检查TREE_CHAIN就多了一个。
  4. POINTER_TYPE怎么打印这么简单的东西gdb一早上居然还不知其所以然,就是说它的TREE_TYPE都是一个INTEGER_TYPE可是不要被它骗了以为是指向整型的指针,不是的,要看它的TYPE_NAME是否存在,如果有的话就打印它的DECL_NAME,比如一个指向char的指针类型的名字是这样子的
    p IDENTIFIER_POINTER(DECL_NAME(TYPE_NAME(TREE_TYPE(t))))
    显示char,这么简单基本的我搞了两个月居然还是搞错!
  5. 类似的模板参数是TYPE_DECL,它的TREE_TYPETEMPLATE_TYPE_PARM,那么打印它是这样子的
    p IDENTIFIER_POINTER(DECL_NAME(TYPE_NAME(TREE_TYPE(parm))))
  6. 俗事缠身,在缝隙中我认为问题应该出在更上层,也就是说所谓的fn_type_unification因为这个时候参数里已经没有函数声明了,已经太晚了。这个是我这几天一贯的感觉,可是似乎作者是说这个不要紧在随后还有动作补救,这个就是让问题复杂的地方。到底是在determine_specialization还是get_bindings出的错呢?我肯定是已经睡着了在说梦话吧,问题当然是在某个模板替换的环节,但是我现在感觉这个一千行代码的tsubst比之前那个三千行的grokdeclarator问题还要大,因为模板绝对是c++最复杂的部分,这里面还有模板模板的嵌套,而且我基本上看不懂,看来可能又要从标准的基本语法学起来了。我已经决定提交了这个bug,看能不能归结到一个新bug因为如果和转化操作混在一起就更加的难以处理了。

九月八日 等待变化等待机会

  1. 早晨起来看了几眼的代码我就觉得昨天晚上的结论可能不准确,我觉得必须从语法template parameter pack看起来,没有其他办法。
  2. 所以,我的问题是两个的结合:template parameter pack and function parameter pack,这里说是or和我的意思不一样,我是说我的问题同时牵涉到了两个方面。
  3. pack expansion终于给了我叫不上名字的那个id加上...

九月九日 等待变化等待机会

  1. 早上起来看关于pack expansion的概念,这个范围相当的广泛,涉及很多复杂的场景。一时摸不着头脑,我也想这个也许暴露它有很多的实现上的问题,于是我找到一个好办法就是直接的比较GCC在其中的场景下为什么有成功与失败。这个就是一个很好的对比的例子,如果使用using的话这个没有问题: 这个对比这个失败的例子 两者的差别也许很多,一个是所谓的standard_conversion,一个是specialization,但是对比之前原始bug这个using还是成功的,为什么呢?
  2. 我找到了一个表面的原因,在替换函数参数类型的过程tsubst_arg_types里如何扩展参数tsubst_pack_expansion中,对于这个用户传入的模板显式参数的理解不同,在失败的情况下GCC认为它是所谓的incomplete的,也就是说这个显式参数int[],char[]在这个宏ARGUMENT_PACK_INCOMPLETE_P返回结果不一样,它的注释说
    
    /* Whether the argument pack is "incomplete", meaning that more
       arguments can still be deduced. Incomplete argument packs are only
       used when the user has provided an explicit template argument list
       for a variadic function template. Some of the explicit template
       arguments will be placed into the beginning of the argument pack,
       but additional arguments might still be deduced.  */
    
    而这个宏是依赖于这个所谓的TREE_ADDRESSABLE这个宏来判断的,它的意义是
    
    /* In VAR_DECL, PARM_DECL and RESULT_DECL nodes, nonzero means address
       of this is needed.  So it cannot be in a register.
       In a FUNCTION_DECL it has no meaning.
       In LABEL_DECL nodes, it means a goto for this label has been seen
       from a place outside all binding contours that restore stack levels.
       In an artificial SSA_NAME that points to a stack partition with at least
       two variables, it means that at least one variable has TREE_ADDRESSABLE.
       In ..._TYPE nodes, it means that objects of this type must be fully
       addressable.  This means that pieces of this object cannot go into
       register parameters, for example.  If this a function type, this
       means that the value must be returned in memory.
       In CONSTRUCTOR nodes, it means object constructed must be in memory.
       In IDENTIFIER_NODEs, this means that some extern decl for this name
       had its address taken.  That matters for inline functions.
       In a STMT_EXPR, it means we want the result of the enclosed expression.  */
    
    我觉得这个是非常让人狐疑的一件事情,即便是在using里它也是一个函数的参数,同样都是指针当然都是要放在内存里的,难道不是吗?总之我还不清楚它的真正含义,但是至少我看到了不同的现象。
  3. 所以下一步就是瞄准模板参数的parsing时候是怎么设定这个令人疑惑的所谓的[in]complete的属性的。暂时到这里吧。

九月十日 等待变化等待机会

  1. 这个例子的fold我看了感觉还是很神奇这里的关键是cast而不是普通的函数调用,这个是fold吗?我怎么总觉得这个是参数的自然而然的行为。世界上没有无缘无故的行为,这个不是自然而然就有的,这个的确是fold。看来我对于fold的理解仅仅是fold expression。而这里说的是function parameter list
  2. 原来fold是来自于functional programming的所谓的high-order function。看这个表所显示的各个语言对于fold的支持,很多流行的语言都是依赖于某个类来实现,比如C#,Java等等,而很少像c++这样子在语言本身就支持的,这个肯定是一个相当有挑战的特色吧。很显然的在编译器级别支持肯定是能够利用优化实现很多并行操作吧?
  3. 这个例子正好说明问题,为什么同样是specialization在成员函数就没有问题呢? 结果是类似的,就是说那个complete_type判断不同,所以,这个是另一个追踪的途经来看看两者区别究竟在哪里。

九月十一日 等待变化等待机会

  1. 我昨天对于GCC几乎已经丧失信心了,因为做一个对于不断变化的语言的前端实际上并不那么容易,否则也不会有传闻这个无名的小公司EDG的产品居然被这么多大公司使用,据说微软的IDE也使用EDG,当然这个是我听说。总之我找到了问题的可能源头就是这个标志量ARGUMENT_PACK_INCOMPLETE_P的设定与消除,根据注解它是为了大概是什么默认参数的支持而设定
    
       /* Mark the argument pack as "incomplete". We could
    		 still deduce more arguments during unification.
    	 We remove this mark in type_unification_real.  */
    
    但是问题是在消除它之前的tsubst_pack_expansion就提前返回了,这个就是我这几天观察到的现象,我猜想作者是要处理这个情况吧? 我的很初步的概念是默认参数和parameter pack都必须是出现在参数结尾,而后者的硬性规定超过前者优先级。语法上看模板参数不存在这个限制
  2. 我的感觉是这样子的,凡是目标模板函数参数如果是parameter pack结尾的话,GCC认为这个是所谓的deduciable,那么它就认为参数incomplete,然后需要在日后instantiate时候再次处理结果就有错误发生。比如这个测试似乎只有clang能够通过,结果让我对于它的语法的正确性也开始感到怀疑了。 GCC对于test2的错误就是典型的这个问题PR102235,可是因为这个问题MSVC++也通不过于是就没有人敢承认这个是一个bug,而它的本源和这个是一样的PR94501 现在就是要怎么理解什么是parameter pack的deduciable?pack_deducible_p从代码来看
    
    /* The template parameter pack is used in a function parameter
    	   pack.  If this is the end of the parameter list, the
    	   template parameter pack is deducible.  */
    
    这个要求是否是对的呢?我认为这个如果是用户传入了explicit argument就不应该了吧?这个问题实在是太复杂了,而我的概念还是起步阶段,看来还是要读标准才行吧?
  3. 要补课,基本原理我还是不清楚。等吃了早饭再看吧。
  4. 我对于编译clang需要额外编译llvm感到很头疼因为笔记本电脑的内存不够大经常在链接的时候出错,而且我对于LLVM的那套虚拟机优化不感兴趣。所以,我就找到了这个帖子单独编译clang,但是它还是误导了我。或者也许那个是过时的做法,总之正确的做法对于版本12.01是下载相对应的编译好的binary,然后设定环境变量LLVM_DIR指向编译好的binary里面的lib/cmake目录,然后使用cmake编译clang应该就可以了。就是说那个目录下面都有事先编译好的配置,现在看看这个编译需要多久吧。
  5. 我感觉我对于CFG的理解还是不太明白,这里说的这些个generator都是针对context-free-grammar,可是c++明明是context-sensitive-grammar,这个是什么意思呢?我一定是理解上有什么误区吧?
  6. debug编译的开关是CMAKE_BUILD_TYPE,我同时设定了环境变量最后也不知道是否都在起作用,而最后链接的失败的确是内存不足造成的因为最后居然使用超过8G的内存,同样gdb里也是消耗了超过一个G的内存。我发现使用gdb跟踪是最快理解代码的办法,毕竟找入口不是那么容易的。

九月十二日 等待变化等待机会

  1. 我看了clang的代码觉得的确是有使用静态库做一个简单的纯parser的可能性,至少parser依赖于semantic还有预处理等等,但是这个范围有多大是一个问题。既然我可以部分地在GCC这样的困难环境下实现一部分那么在clang里是不成问题的,我甚至看到一个说法GCC的某些行为是某种政治行为,因为有些早期的开发者是彻底的GNU的极端分子担心开源成果被某些不法奸商窃取,所以,故意把代码写成这样的强耦合让你分拆不了,这个说法有些阴谋论的腔调,但是,我以为不大可能,或者只是某些极个别的人这么想,大多数程序员不是在追求这个目标是的确迫不得已,不是人们想要呆在地球这个摇篮里不出去,而是万有引力拉住你让你出不去,非不为也,乃是不能也。想要做到模块化说起来容易做起来难,而且在早期没有看到它的坏处之前是没有很强的动力去遵循它,当年的软件工程实践还是过度依赖于单个程序员的超人能力,没有过多的考虑这是一场持久的战争。就如同当年元首认为闪击战可以在三个月击溃苏联一样根本没有准备过冬的棉衣一样,当初的开创者也没有想到c++语法后来居然每三年就要更新变革一下,而且很多时候还不是小的变化,这个是让第一代开发者始料未及的。所以,在C语言下的原则被打破了,因为c++和c的目的与实践非常的不同,后者是对于汇编机器语言的高级化,而前者追求的是一个高效的接近于自然语言,而且近些年的发展趋势是兼收并蓄希望把很多其他语言的精华吸收为我所用,几乎要囊括宇宙万物的语言的趋势,这种雄心一方面是汇集各路精英的喜好,另一方面也是计算机编程语言相互竞争的结果,作为一种专家语言最后的结果就是只有专家才用,必须要让傻瓜也能用才能普及大众,那么这有两个基本途经,一个是靠海量的傻瓜化的库来实现无所不能的功能,比如java/c#等等,这个在语言生态圈建立的情况下自然有公司去作,而且不断完善。另一个途经是在语言本身就能够做到强大的以至于为使用者实现了大部分的功能,这个当然是更高的追求,因为前者毕竟是良莠不齐难保错误,而且不同的库对应于不同的编程风格理念,等于是人为的创造方言与范式,形成所谓的小圈子与流派之争。从语言本身入手是公平与高效的。这个也就是标准库的想法,因为标准库已经近似成为语言的不可分割的一部分,甚至于在一定阶段标准库可以在编译器内部实现与优化。这个都是所有语言都采取的策略,但是唯一的区别在于语言标准是否不断更新发展,还是说仅仅依靠在标准库里堆砌增加?我的理解是c++作为背负很多包袱的古老语言有很多的羁绊不得不断的更新以便追赶时代潮流以便能够抓住新的一代。所以,我看到有的人批评c++贪大求多走入歧途,我并不赞成但是也不能说完全没有道理,关键是某些原则必须坚持的前提下的不断发展,而且用户群体要能够跟得上,也就是说新的更新应当是使用者的期望的吧?对于一个语言的发展前途这种大问题普通人是看不清楚的。早上看到一篇文章说早在1941年8月战略家就已经预见到纳粹德国必然战败,而多数专家是在1941年12月斯大林格勒失败才意识到的,这几个月的差距是巨大的,而普通人是在1943年库尔斯克会战失败才认识到的。我估计我这个级别最多能在1942年的某个时间点认清形势吧。
  2. 我首先尝试把unittest下的例子lexTest进行编译,很惭愧对于gtest这个常用的东西我竟然不熟悉折腾了不少时间就放弃了,只能把gmock的东西都去掉,这个是用到的静态库: 顺序是重要的,我为了找出这些库用了以前对付gcc静态库的办法,就是for-loop里使用objdump -Ct|grep未定义的函数名字来查找。
  3. 另外说一下我是一开始使用clang的-cc1来gdb跟踪看看它是怎么调用的,这个机制是模拟的GCC的外表,但是不是使用多个excutable,而是直接调用各个所谓的cc1_main之类的函数入口,这个也是特色。至少gdb里不需要vfork follow之类的特殊处理。
  4. 在散步的时候我在想我做的这些是否有一丝一毫的意义,我只能这么安慰自己:在现实里c++的语法可能复杂到以至于有时候连专家都不一定能够判断对错,很多时候程序员就是依靠编译期的最终裁判来写代码的,那么这个也就不难理解这个网站命名为godbolt的用意了,很多时候编译器在普通程序员眼里就是神,而制作维护编译器的程序员有着近乎神一样的地位,或者像海船上船长一样是仅次于上帝的人,那么刺穿披着神的外衣的这些祭祀们的伪装有时候是一件需要极大勇气的,但是这个不是目的,而是希望所有的上帝的仆人都能够忠实的履行他们应尽的责任,这个需要透明度。

九月十三日 等待变化等待机会

  1. 关于在less里显示颜色的问题,需要在pipe前面的命令里使用强制颜色的命令--color=always,同时less需要开关-R
  2. 单独编译gtest需要先配置,我嫌麻烦就全部编译llvm,结果当然就会有需要的这两个库:libgtest.a, libgtest_main.a,为了解决内存不足导致链接失败的问题只能选择编译类型是release,完整的命令是: 其中LLVM_ABI_BREAKING_CHECKS强制关闭一些检验的宏。总之在之前的这些经历是不可避免的,否则连基本的编译都做不好就不要再奢望看代码了。我的感觉GCC几乎必然在c++2x以后会有很大的阻力,而微软的编译器看样子也好不到哪里去,似乎未来属于clang。

九月十四日 等待变化等待机会

  1. 我一开始试图使用最最基本的preprocessor和sema来创建parser,但是遇到一些小问题发现有tooling里现成的创建AST的函数tooling::buildASTFromCode就想图省事走捷径,但是这个引入了很多额外的库,并且有一个函数始终不能解决,我反复看了函数名字才意识到名字一样参数一样但是所谓的ABI-tag不同,这才明白这个所谓的[abi:cxx11]跟在函数名字和参数之间的tag是函数签名的一部分,我之前接触了一下clang的这个ABI感觉有些麻烦,最好还是不要多纠缠,因为这个实在是有一个先来后到的优越性的问题,GCC诞生在先,大部分的程序尤其是旧程序都是按照GCC编译而成的所谓的ABI运行环境,作为后来者clang要和旧的GCC编译出来的结果匹配不得不做这个额外的努力。这个里面很烦人的,据说对于GCC在某个早期版本犯的错误也必须去做兼容,可能是吧?总之很麻烦,我还是回到由下向上的步骤吧。

九月十五日 等待变化等待机会

  1. 虽然clang比GCC对于程序原来说要友好千百倍,但也不可能是那么的轻而易举,我卡在一个小问题上,就是说我应该是正确的初始化了preprocessor并用它创建了Sema,然而从这里再创建Parser的时候初始化在Parser里面寻找preprocessor的targetInfo的时候居然没有设定,这里面有一个让人狐疑的地方的是unittest里面唯独没有关于Parser的测试,我除了阴谋论只能解释它是要和其他部分紧密派生比如AST等,否则你怎么测试呢?能够看得到的结果就是AST,难道不是吗?但是怎么创建这个巨大的pipeline是一个复杂的工程,我想前端后端本身各个部分的复杂性是与生俱来的不可改变,而一个复杂的编译流水作业的衔接也是一个复杂的过程,就是说复杂的阶段性被串接起来中间监控调度效率测量错误处理等等本身也是复杂的。这让我想起了现在网上热议的光刻机的问题,也许有一点点可以类比的,它是一套复杂的机器有成千上万的零件和众多的子系统,要把这些众多的系统集成起来完成一个流水作业本身就是一个复杂的系统,即便你突破了各个子系统的技术,能否做出一个耐用而又易于调试更新的控制系统也是一个大学问。因为很多时候子系统之间的耦合度相关性是非常的高的,人为打破关联性独立运行是相当的困难,甚至是无意义的,假如一个parser就是设计在和preprocessor供给输入的情况下,你要单独设计一个通用的接口,也许是可行的,但是如果和实际运行不一致这个看上去通用也便于独立测试的parser系统又能够给你多少好处?出了错你要怎么还原?总之不简单。
  2. 折腾一个小问题,我一开始偷懒使用eclipse里的调试器结果被误导了好久,就应该直接使用gdb又快又容易,这个著名的triplet的问题反反复复遇到,是这么平常我却总是忘记,这个是工具链的基本概念,我还是记不住。 ARCHITECTURE-VENDOR-OPERATING_SYSTEM或者是ARCHITECTURE-VENDOR-OPERATING_SYSTEM-ENVIRONMENT,很典型的我的系统是x86_64-unknown-linux-gnu这里environment是gnu,我一开始以为是这个设定导致crash,后来发现如果按照默认的x86_64-apple-darwin11.1.0那么binaryformat就不是ELF,苹果居然是MachO,我实在是太孤陋寡闻了,我一直以为苹果也是Linux,应该使用的binary是一样的ELF,没想到他们这么卑鄙竟然来了个釜底抽薪强制要把你封闭在外,这才是狠招子,操作系统的核心就在于可执行程序运行的操纵上,不在这个根本的load&run上做文章岂不是奇怪吗?我怎么会想不到呢?
  3. 还有就是文档是一堆的.rst文件,我本来以为是什么特殊文件还专门下载安装了一个rst2pdf,后来才明白就是文本文件,真的有些多此一举,然而看上去还是舒服多了。
  4. 还是第一次听说OpenMP,看来llvm支持众多平台此言不虚啊,我还是孤陋寡闻。原来这个东西可以在同一个节点并行可以结合MPI的跨节点。太复杂了,知道名字就好。
  5. 我把OpenCL当成了NVidia的那个什么语言了,原来这个也是苹果的。
  6. 我扫了一眼所谓的internalmanual,对于我心目中很重要的parser仅仅几行文字,大概就是说parser以前还像GCC一样报告一些事件,后来全改了只向Sema汇报,并不直接访问AST,而是透过所谓的不透明的什么什么stmt/ExprResult来访问AST,这个当然是好了,其实parser本身并不难,难的是解释parser的结果,很多时候解释到底在哪里做的这个是GCC代码很混乱的一个原因吧,至少在我这个外行看起来是这样子的。所以,parser没有多少代码,核心应该在于Sema,这个是我的感觉。但是脱离了AST来谈论Sema那也是耍流氓了,无本之木,无源之水从何谈起呢?所以,这几个部分难分难舍。
  7. 总之预处理单独运行基本上人人都会,我想我还是先读文档有一个大的认识再说吧。
  8. AST的设计思想是很深奥的,我看了只是体会到一些但是不是很明白,比如这个immutable once created所带来的好处似乎处处都是瞄准了GCC的核心问题,因为最近这个bug我之所以放弃的一个原因就是程序员在解析的时候随手修改了ARGUMENT_PACK_INCOMPLETE_P,注解上说这个是一个类似临时的标志量,在随后的过程会消除,结果在不同的路径上它起了不同的作用导致了这个bug,我看了无语也不敢改,天知道有多少问题会牵连出来呢?所以,clang设计的哲学这一点很关键。另一个就是和我遇到的众多的模板问题息息相关,到底AST里要保留多少的原始信息,对于一个模板的声明和它众多的实例化是否保留多个声明,这些都是要害问题。我只能意会但是不能真的体会。究竟所谓的CanonicalType是怎么做到的?这些原则说起来容易做起来难,对于dependenttype你怎么能够一次定型呢?什么时候创建AST算是结束,不经过查找能行吗?那么这个过程是否有临时的半成品呢?这些对于我这个门外汉只能是瞎猜。这么看来这个小小的内部文档还是有价值收藏的。
  9. 我感觉这个三十几页的小文档确实是一部武林秘籍值得认真阅读,尤其作者前言说的是面向的读者不是终端用户而是企图骇客clang的人,我高看自己一眼姑且把自己归结为这一类吧,尽管我连用户手册都没有看过。
  10. 这个入门AST文档值得阅读。它有视频我准备看。它和GCC的非常的不同,我以为这个设计是独特而有很多优势,不是内存节省而是可持续发展,和语法接近是好事。不像GCC的一个巨大的union。没有common base class是好事吗?很可能吧?

九月十六日 等待变化等待机会

  1. 应该说你能够折腾GCC那么clang是容易太多了,因为后者本来就是设计让你这么干的,而前者有意无意的设下重重障碍阻止你这么干,也许我这个是小人之心,前者没有那么坏心眼,但是在长期的演化里不断的堆砌成了一座垃圾山,我这么说是对前辈们的不敬,可是结果就是导致后来人很难再有什么办法来改变,而且我认为GCC和LinuxKernel的渊源让它成为一个背负历史包袱的巨人再也无法进行有效的突破,的确兹事体大,谁敢动那牵一发而动全身的c部分的代码,而处于效率的考量使用内存紧凑的举行union配合宏来访问是最高效率的,只是维护起来的确是问题,尤其那些在不同场景下复用的lang相关的部分,唯一的文档就是看程序员的注释与代码,天知道后来的人有多少能够遵循当初设计者的初衷呢?
  2. 我徒发议论忘了我想说什么了。其实也很简单,无非就是在gdb里跟两次就知道那些instance,invocation,execution等等都是障眼法,你就抓住那个ParseAST就可以了,之前我只是不明白一些细节为什么我的preprocessor需要initialize去初始化targetinfo而parser里后来自己又要作一次,总之我的感觉是TargetInfo这个在每个环节都要初始化的原因也许是为了toolchain的吧,这个是猜测,总之使用gdb没有搞不定的,因为你有源代码配合还能看不懂代码那么就不要再干这一行了。因为这个工作就是农民工的活,不需要什么知识只是一点点经验,而所谓的知识仅仅是帮助你快速理解代码而已并不是绝对的,因为你瞎蒙也能debug,因为程序崩溃是最最让人赏心悦目的,否则就是debug的噩梦。我们不怕nullptr崩溃,怕的是不崩溃然后梦游。
  3. 我观察到一个做法就是在本文件内使用匿名名字空间来声明是一个安全的做法看到这个帖子我才意识到这个是c++的static做法我不是很确定,因为我想把我的所有代码都放在一个私有名字空间里原作者的匿名名字空间改名之后就不好用了。
    The difference is the name of the mangled identifier (_ZN12_GLOBAL__N_11bE vs _ZL1b , which doesn't really matter, but both of them are assembled to local symbols in the symbol table (absence of .global asm directive).
    这个只能说是不明觉厉了,前者是匿名的名字空间的mangle,后者是static的,这里我对于汇编的数据区不熟悉,也不想也不能深入了因为第一重第二重匿名名字空间是怎么办的太复杂了,对我也没有用了。 这里提到的local symbols,似乎在代码里哪里看到过,这个难道是所谓scope里lookup的一个特殊的表吗?这个是每个文件都分配一个那么在GCC里parse一个文件的时候分配一个结束就丢弃掉因为外界是不能访问的,以后也不可能被使用,在编译好的symbol里不加.global,这个是否就是和c语言的static一样了,当然除了mangled name之外,但是不同文件它重名也没有关系了吧?虽然mangled是一样的。我看到clang这个做法的好处是在本文件里使用这个匿名名字空间的元素不需要任何scope就好像全局变量一样,但是我一旦想把它纳入我自己的在另一个文件的同样的名字空间里就有问题了,这个是好事,它阻止了你的行为因为作者不想你这么干。
  4. 在clang里dumpAST的开关实际上很容易发现是这个实现的ASTConsumer接口的这个ASTPrinter来实现的,我觉得这个是一个很好的理解怎么traverseAST的机会。至此我的初步的实践有了一个可视化的里程碑,虽然是一小步可是我很满足。

九月十七日 等待变化等待机会

  1. 真的是折腾一个早上几乎把我逼疯了,因为看似相同的编译指令居然总是爆出一个莫名其妙的链接错误,而错误的信息让人摸不着头脑 undefined reference to `typeinfo for clang::ModuleLoader'我为了缩小范围最后就简化成这样子一个简单的声明依然如此报错
    
    #include "clang/Lex/ModuleLoader.h"
    clang::TrivialModuleLoader modLoader;
    int main(){}
    
    当然编译的时候你要指定clang的include的路径,而链接的时候需要clang的library的路径和必要的library。比如g++ -L${CLANG_PATH}/lib -o "test" ./test.o -lclangLex -lclangBasic -lLLVMMC -lLLVMCore -lLLVMRemarks -lLLVMBitstreamReader -lLLVMBinaryFormat -lLLVMSupport -pthread 那么问题出在哪里呢?我反反复复折腾我之前的编译配置终于发现是编译开关里如果没有-fno-rtti就会造成链接的这个错误,看来是很深层次的问题,这个超出了我的能力,我看到.o文件里有不少的weak symbol,但是比较两者的区别超出了我的能力。总之,这个真的是教训,c++代码的复杂性不是一般语言能够比拟的,而我使用clang自己来编译这个代码立刻反映出了更多的问题。首先clang编译的速度慢了不知道多少倍,这个从二进制文件大小就看出来了,GCC文件不过几十兆,而clang却是一个多G,即便我不用debug编译,即便做相应的优化,两者也是有数量级的差距,这个速度和效率的牺牲是开发之初就注定了的,所以,对于很多嵌入式系统恐怕使用GCC是没有选择的选择,因为没有人指望你在草莓派之类的单片机上用c++写什么奇幻的程序。而clang为了在运行期和GCC编译的程序兼容肯定是花了大功夫了,我之前秒过一眼它的ABI的东东,还是超过一般应用程序员的理解的,所以,我估计这个rtti也是和ABI有关的吧。我不想再分身了。

九月十八日 等待变化等待机会

  1. 昨天高兴了半天看来过于乐观,今天发现了问题,就是使用原生的clang -cc1 -ast-dump命令输出的和我的结果非常的不同,我觉得可能要先从preprocessor的输出结果来比较。经过gdb才发现是我创建的ASTPrinter的参数不同导致输出形态也不同,这个结果没有什么问题,还有一点就是以前也在GCC的预处理看到的,就是所谓的搜索路径会输出,就是说除非你在预处理输出开关加上-P否则就会输出所谓的 而这个如果使用抑制linemarker的开关-P才能阻止,这个也就是导致在AST输出有一些系统固有的数组之类的吧。
  2. 我不知道这个算不算是一个问题 所有的编译器都接受这个,可是我还是感觉不太对。
  3. 关于-fno-rtti究竟是不是一个问题呢?
    -fno-rtti
    Disable generation of information about every class with virtual functions for use by the C++ runtime type identification features (dynamic_cast and typeid). If you don't use those parts of the language, you can save some space by using this flag. Note that exception handling uses the same information, but it will generate it as needed. The dynamic_cast operator can still be used for casts that do not require runtime type information, i.e. casts to void * or to unambiguous base classes.
    我的问题是clang::TrivialModuleLoader会导致undefined reference to `typeinfo for clang::ModuleLoader',那么怎么理解呢?
  4. 出去散步回来i才明白一个简单的道理,在preprocessor里得到的token的kind为什么不认识c++里的template关键字,原来这个是纯粹的cpp就是说它不认得c++的关键字,难怪我看到始终把template当作是identifier。但是下一个问题是为什么得到的AST是不对的呢?根本的原因是我没有设定LangOptions,这个preprocessor可以说是c/c++/objectiveC通用的,没有设定Lopt.CPlusPlus = true; Lopt.CPlusPlus17 = true;当然就是默认为c语言的lexer了。
  5. 有一个好处是使用命令行clang无法做到的,因为你没有办法设定输出AST带颜色,至少我还没有找到命令参数,这个就是自己编程的优势,你只需要在DiagnosticsEngine设定setShowColors就可以输出好看的图。
  6. clang的预处理设定的宏和GCC的cpp的宏差别非常大,这个让人实在是感到不安,如果有很多宏设置不同那么标准库也许都会不同了吧?
  7. 关于variadic template parameter pack,这篇论文很值得仔细阅读。天晚了,明天再对吧。

九月十九日 等待变化等待机会

  1. 很多时候你以为你已经懂了,可是一实践就又遇到问题,发现自己其实什么也不懂。如果我使用template type paramter pack,这一切似乎很正常,我可以在函数参数里做类似的类型的体现,而这个unexpanded paramter pack是我需要在fold expression里来使用的。 那么当我使用non-type parameter pack怎么办呢?我能够同样在参数里体现吗?这么做的原因是我可以不用显式的传递模板参数而让编译器自己去根据参数来推理,比如上面的函数调用就是很令人赏心悦目的sum(1,2,3,4);,当然这里我是不满意的因为我本意是返回sumation的值而不是打印,所以,我才想要这样子 主意这里我反复想要尝试在函数参数里做类似的,比如int args...,但是这个已经不是unexpanded parameter pack了,我没有办法在fold expression里使用这个args...,而variadic function是陈旧的c语言的余孽:int sum(int args,...)需要使用丑陋的va_start/va_end之类的,根本就不是我想要的。 You can probably use variadic non-type templates in functions, but not as (non-template) arguments,这个是我找了两个小时的答案,其间看到很多很多,眼花缭乱啊。这个再次应证了我的看法,单单使用parameter pack就是一个大的头疼事情,不要说编译器在这方面有缺陷了,这个就是天大的事情了,因为你真的很难明白到底谁对谁错。

九月二十日 等待变化等待机会

  1. 结合gdb来阅读clang的前端代码相对来说是一个好办法,因为可以和实际的语法来应证。结合它的注释看到一个Diagnose的判断场景,因为标准里经常会提到关于什么情况下不需要diagnose就可以直接认定是ill form。这也是和标准直接应证的实现,但是我经常读不懂标准的英语,似乎是对于plain English理解上的障碍,语言主导标准,母语限制了我的思想。难道不是吗?为什么学医都要学拉丁语?一个看似中立的计算机语言的标准是使用英语来作为定义解释的,这里面应该就不完全是中立的了吧?比如我前两天始终不明白这个pack expansion的概念,还google了好久不得要领,这里定义是很清晰的,我不明白我为什么没有懂。
    A pack expansion consists of a pattern and an ellipsis, the instantiation of which produces zero or more instantiations of the pattern in a list (described below). The form of the pattern depends on the context in which the expansion occurs.
    这个列表是非常复杂的场景,有很多我始终理解不透,抄写来帮助机械记忆吧。 相比之下,cppreference的解释更加的人性化。它更像是描述一个算法的实现。也许是程序员写下的心得体会?

    Pack expansion

    A pattern followed by an ellipsis, in which the name of at least one parameter pack appears at least once这个我感到很难理解???, is expanded into zero or more comma-separated instantiations of the pattern, where the name of the parameter pack is replaced by each of the elements from the pack, in order.
  2. 这句话可能和我的问题息息相关,但是我一时还是吃不透它的含义:
    If the parameter-declaration-clause terminates with an ellipsis or a function parameter pack ([temp.variadic]), the number of arguments shall be equal to or greater than the number of parameters that do not have a default argument and are not function parameter packs.
    要怎么理解呢?我的实验结果让我也怀疑这个是所有编译器的问题,但是大概率是我的错误理解,但是我还是提交了bug。 GCCclang都认为这个是合法的,可是我怎么想也不对,Ts被实例化为int*,double这一点从参数结尾就看出来了,那么Extra是怎么被实例化为double的呢?这个肯定是不合理的。
  3. 人生的意义是否就是一个漂流瓶,整个人类文明的最终的意义就是在宇宙大神最后发出的广播里人类语言出现在四百多万名的位置上吗?作为人类的一个微不足道的个体要怎么证明自己来过,看过,想过,离开呢?我的人生的漂流瓶怎么制作?

九月二十一日 等待变化等待机会

  1. 这句话非常的难懂。
    Where syntactically correct and where “...” is not part of an abstract-declarator, “, ...” is synonymous with “...”.
    什么叫做不属于abstract-declarator?难道是说parameter-declaration-clause就已经包含了variadic function的variadic parameter了吗?
    
    parameter-declaration-clause:
    	parameter-declaration-listopt ...opt
    	parameter-declaration-list , ... 
    
    也就是说,...直接在parameter-declaration-clause直接得到解决不需要解析parameter-declaration-list 所以也就是解析不到abstract-declarator?这个eclipse(...)本来应该要通过noptr-abstract-pack-declarator来解析...
    
    noptr-abstract-pack-declarator:
    	noptr-abstract-pack-declarator parameters-and-qualifiers
    	noptr-abstract-pack-declarator [ constant-expressionopt ] attribute-specifier-seqopt
    	... 
    
    但是它解析不了,...,所以必须要来一个说明它是...的synonym。这个脑回路是很长的,因为对于c程序员来说它是有另一个含义的,因为大多数c程序员的parameter pack或者说variadic parameter和c++完全不是一回事,它实际上是通过va_list来访问的一个特殊实现,它要求你的第一个参数是const char*并且指示变量类型。问题是我在GCC里好像看到这个代码的解析,也许是在纯C部分做的,可是这个并不是c专有的语法不可能通过extern "C"的c与c++代码栈的转换来解析,只能是在c++语法里做,也就是我的疑惑是两者语法有区别吗?我感觉我有一点点把标准库和语法混同起来了吧?本身va_list之类的是一个相互约定的冒险的强制类型转换,根本不需要编译器的支持直接从栈里取指针,或者从寄存器里取参数,这里应该是对于调用约定相关的实现,根本无视语法的。而我们讨论的parameter pack是编译期的类型解析,是风马牛不相关的吧?22.09.2021这里是证明这个根本就不是parameter pack! 我折腾了好久才明白自己为什么总是搞不明白一个简单的道理,c++就是为了避免这个混淆才在函数参数声明里强制规定declarator-id一定是这样子的
    
    noptr-declarator:
    	declarator-id attribute-specifier-seqopt
    	noptr-declarator parameters-and-qualifiers
    	noptr-declarator [ constant-expressionopt ] attribute-specifier-seqopt
    	( ptr-declarator ) 
    	
    declarator-id:
    	...opt id-expression 
    
    换言之,就是...必须出现在id-expression之前,我以前对于模板参数和函数参数里这个差别始终糊里糊涂,现在才明白其中的意义就是要防止语法的歧义吧?
  2. 我想为parameter pack的每个场景实验一个例子。
  3. 我以前花了大量时间去理解GCC预处理器对于搜索路径的设定,这篇文章提纲挈领总揽全局写的不错值得收藏。我以前看了可能少走很多弯路,不过现在也增加一个知识:可以使用-nostdinc来压制默认搜索路径选择。但是clang似乎对于这个选项不支持?而且在-cc1下面根本拒绝。
  4. 实际上我对于template argument的理解不够。
    There are three forms of template-argument, corresponding to the three forms of template-parameter: type, non-type and template.
    ,那么argument就是parameter吗?我脑子里还是函数的实参型参的概念,这里可能是误区吧?那么这里
    When the parameter declared by the template is a template parameter pack, it will correspond to zero or more template-arguments.
    ,这句话的理解还真难啊,就是说parameter可以是由parameter pack组成,替换的过程是实例化template argument的过程,这个应该是对应于GCC里的tsubst之类的template substitution的各个函数吧?总之我的困惑就是我对于template-argument的语法的理解上的偏差,它到底和template-parameter是什么关系呢?
    
    template-argument:
    	constant-expression
    	type-id
    	id-expression 
    
    template-parameter:
    	type-parameter
    	parameter-declaration 	
    
    其实两者非常的不同,为什么我总是分不清呢?在template-argument里它最终可能会终止于noptr-abstract-pack-declarator
    
    noptr-abstract-pack-declarator:
    	noptr-abstract-pack-declarator parameters-and-qualifiers
    	noptr-abstract-pack-declarator [ constant-expressionopt ] attribute-specifier-seqopt
    	... 
    
    也就是说eclipse(...)只能是在id之出现。而template-parameter可能会终止于两条路径:首先type-parameter可能会完结于
    
    type-parameter:
    	type-parameter-key ...opt identifieropt
    	type-parameter-key identifieropt = type-id
    	type-constraint ...optidentifieropt
    	type-constraint identifieropt = type-id
    	template-head type-parameter-key ...opt identifieropt
    	template-head type-parameter-key identifieropt = id-expression 
    
    都是出现在identifier之的。其次,parameter-declaration最后可能会终结于declarator-id
    
    declarator-id:
    	...opt id-expression 
    
    这个...也是出现在id之的。 这两个概念在模板里至关重要,之前我在GCC里看代码就是吃亏在对于他们和函数参数的三方博弈里看的头昏脑胀。
  5. 一个早上看的我是头昏脑胀,依旧是一头雾水。昨天去投票,常言道:遇事不决选女人。对于候选人一无所知的情况下就选女人当国家的掌舵人吧,因为侮辱性不大,伤害力也不大。能把国家折腾到哪去呢?女人也不太敢打仗,圣母心重看不得别人受苦,也不太敢采取激烈手段,所以,当年《围城》里就是这个观点以后社会的领导人都是女人,因为她们是天生的政治家。
  6. 我尝试template-argument的一个可能路径
    
    template-argument:
    	constant-expression
    	type-id
    	id-expression 
    type-id:
    	type-specifier-seq abstract-declaratoropt	
    abstract-declarator:
    	ptr-abstract-declarator
    	noptr-abstract-declaratoropt parameters-and-qualifiers trailing-return-type
    	abstract-pack-declarator 
    abstract-pack-declarator:
    	noptr-abstract-pack-declarator
    	ptr-operator abstract-pack-declarator 		
    noptr-abstract-pack-declarator:
    	noptr-abstract-pack-declarator parameters-and-qualifiers
    	noptr-abstract-pack-declarator [ constant-expressionopt ] attribute-specifier-seqopt
    	... 	
    
    就是template-argument=>type-id=>type-specifier-seq abstract-declarator=>abstract-pack-declarator=>noptr-abstract-pack-declarator=>... 那么这个从语法上看是合法的 为什么会爆出错误error: pack expansion does not contain any unexpanded parameter packs,我决定使用gdb来探究一下。出乎意料的是在众多的DiagnoseUnexpandedParameterPack[s]和类似的函数并没有被调用,这个是我臆想中的clang的处理方式就是按照标准把这些场景都检验一遍,但是这个是在diagnose的方式下,也许是为了debug之类的需求吧?并不是程序运行的做法,反而是使用一个标志量来判断:
    
    bool containsUnexpandedParameterPack() const {
        return getDependence() & TypeDependence::UnexpandedPack;
      }
    
    它的逻辑是什么呢?注解上说
    
      /// A type that contains a parameter pack shall be expanded by the
      /// ellipsis operator at some point. For example, the typedef in the
      /// following example contains an unexpanded parameter pack 'T':
      ///
      /// \code
      /// template<typename ...T>
      /// struct X {
      ///   typedef T* pointer_types; // ill-formed; T is a parameter pack.
      /// };
      /// \endcode
    
    这里的说法似乎在暗示expand是通过ellipsis operator来做的,这个真的是耳目一新,让人匪夷所思。我找了标准并无概念。因为ellipsis本身并不是operator,作者的意思不是这样子的。 有趣的是使用这个经典例子
    
    template<typename ...T>
    struct X {
        typedef T* pointer_types; // ill-formed; T is a parameter pack.
    };
    
    代码就在declarator之后调用DiagnoseUnexpandedParameterPack来检查错误。现在这个对我来说还是太复杂,我想吐了。休息吧。
  7. 遇到clang里这个数据结构DeclarationName空空如也只有一个指针大小,却有如此多的信息,不是指针,因为你敢于对指针的后面三个bit来做mask吗?我是不敢,难道是因为这个是指向的结构的偏移?初步看来这个是一个指向IdentifierInfo的结构,它和GCC的设计的一个显著区别就是没有使用GCC的那种面向对象的继承设计,仅仅用一个64bit大小的结构来记录众多的信息,而且摒弃了众多宏的访问弊端,一律使用方法来防止程序员的随心所欲,因为宏是万恶之源,一旦程序员尝到甜头就欲罢不能你也如打开了潘多拉盒子再也封禁不了别人任意访问了,因为总有很来的程序员觉得你能这样定义宏我也可以,我是堂堂正正跟你学的,结果里面藏的秘密被人一览无余导致泛滥的访问。这个是最违背设计者的思想的。设计者当初使用方法来访问后来的程序员也就萧规曹随不再越雷池一步了,除非万不得已需要开放其他新方法,估计这个可能性是没有的,如果有就是结构设计的重大缺陷。为什么GCC那种大一统的union背后重重叠叠的继承不好呢?我不敢说不好,我只是再猜测这么多年的风靡一时的面向对象似乎有被扬弃的思潮,不知道是为什么?难道是我们生活的世界本来就不是依靠继承关系来描述?或者这个思路有什么局限性?我不知道,也许有些事情走到极端就物极必反吧?抑或是太多的累赘?过多的继承也许太笨拙不利于以后的修改?因为我的感觉这个如同封建时代的血统论,一切讲究出身,一个类生下来就决定了它的命运,你是继承自父辈的缺点与优点背负了沉重的枷锁,那个时代的编程感觉有些托大认为顶层设计在一个项目诞生之处就对于每个类的生命周期都要预计到,仿佛一个软件工程在设计之初就要把所有的方方面面考虑清楚才敢动手写代码。而且假定了设计者都是大师能够全程掌握工程的开工与结束。这个思想不能说不对,但是现实往往是做不到,尤其当今的开源项目很多时候来来去去起起伏伏,当初怎么可能预计到后来的发展方向和遇到的问题,也没有资源时间让你把所有的细节都考虑成熟了再开工,那么这种继承论就有它的局限了,谁能保证帝王将相宁有种乎的变革潮流?就是说继承很大程度说的是什么都可以变唯有祖先不能变,祖宗之法不可变。这个假设其实本来是针对一个既有项目的后来人维护的角度看问题,并非一个新项目应该有的思想,使用方法来迭代要灵活的多,因为方法的改变并没有暴露内部的数据结构,而继承其实多多少少就是内部数据的暴露。比如你从种姓制度社会的姓氏就推断出它家族财产的多寡,不是吗?是的,这个就是clang代码和GCC的一个显著区别:反对继承!打倒封建血统论!比如: DeclarationName通过一个神奇操作就是bit-mask来同时兼容这些类IdentifierInfo, DeclarationNameExtra, CXXSpecialNameExtra, CXXOperatorIdName, CXXDeductionGuideNameExtra, CXXLiteralOperatorIdName,然而这些类貌似有些微相似的类没有层峦叠嶂的继承关系,他们也许个别之间有朋友friend关系,但是绝无继承祖先的同门关系,为什么?他们没有共同的方法吗?对,没有继承而是composition。看来软件领域的封建帝王思想已经岌岌可危了,一场新兴的民主革命正在或者已经开展很久了,我这个后知后觉的局外人才慢慢感受到世界的潮流趋势的改变。为什么呢?不使用继承实际上是一个进化的思想,最初人们使用面向对象的继承也是进化的思想,但是人们认为继承是一种一成不变的,比如人从海生动物进化而来就必然还保留着某种海洋动物的特征,比如幼胎时期的鳃的痕迹,这个没有错。可是人们忽略了进化与异化的巨大改变,现代陆生动物和祖先的海生动物到底还有多少相似度呢?这种名义上的继承有多大意义呢?如果陆生动物后来又返回海洋生活它并不一定就是复活了当初的鱼鳃,而是照样用肺呼吸比如鲸鱼海豚,那么你硬要强调它们当初来自于海洋这一点是不是要让后来人误认为它们的水生特性继承自海洋鱼类的特征呢?我想这个就是一个典型的反继承的例子,因为没有这个必要使用继承,还不如把特征做成一个个小功能,谁有需要就加在它身上来的灵活,这个摆脱了继承带来的庞大的负担,因为继承是看似免费的实现,因为语言本身帮你实现了背后的继承工作,就好像嘴里含着金钥匙诞生的富家子自己不用努力就得到了父辈的一切财富一样,而后天的组合是一种有目的的奋斗工作的结果,而且更加的不受前人的约束。所以,这个并非是反进化论,而是反封建世袭制。所谓的只反贪官不反皇帝,继承的思想还是有的,只不过不再是那么明目张胆的世袭,而是通过内部特征的加载,仿佛现代贵族不再直接把巨额财富传承子女而是通过一些所谓的基金会巧妙的绕开继承税来输送给他的后代。这样做更有精准的避税功能。说了这么多我还是不懂的那个神奇的指针变换怎么就能够掩码三个bit仍能够是正确的指针,大概是精巧的数据结构的设计要考虑align的问题,在一个大内存不管静态动态的连续内存上分配才能做到吧?其实我不需要关心这个细节这个超出了我的能力也无意义。我关心的是怎么使用而已。而这种设计思想似乎比GCC的架构来的优越一些吧?具体是怎样我也不知道。总之是现代的设计。

九月二十二日 等待变化等待机会

  1. 怎么debug呢?我先从一个基本的例子出发看看clang为什么能够判定这个是错误的 很明显的所有的编译器都能指出这个是不能匹配的,clang的这个函数是在HandleDeclarator/ ActOnFunctionDeclarator/ CheckFunctionTemplateSpecialization/ DeduceTemplateArguments/ DeduceTemplateArgumentsByTypeMatch/ isSameOrCompatibleFunctionType里报错的,它的机制似乎是在函数参数替换之后比较两个函数的原型来判断是否这个替换是合法的。我想使用老办法用打印函数原型来理解问题。眼睛疼,休息吧。
  2. 我打算尝试使用类型的dump方法来打印,需要重新编译clang,才意识到设定环境变量LLVM是要指向安装目录下的lib/cmake/llvm因为它需要那个.config文件才能正确编译链接。

九月二十三日 等待变化等待机会

  1. 核心就是这个一千行的函数DeduceTemplateArgumentsByTypeMatch它异常的复杂,虽然GCC有比它长一两倍的函数,可是它的逻辑的复杂度要更高,因为代码已经是非常条理清晰规整的一千行远比拷贝粘贴来的胶水代码来的更加的有实质内容。我还没有准备好一行行的看,打算先把函数传入参数做打印来看看大概。这里就是clang的弱项了因为编译极其缓慢,不知道有没有什么选项能够加快编译呢?打算使用这个来记录编译的时间:start=$SECONDS ; make ; echo $(( SECONDS - start ))最后结果是1097秒十五分钟!
  2. 这段文字实在是难懂,到底说的是什么呢?这个估计比律师的法律条文解读还要困难和歧义。
    The resulting substituted and adjusted function type is used as the type of the function template for template argument deduction. If a template argument has not been deduced and its corresponding template parameter has a default argument, the template argument is determined by substituting the template arguments determined for preceding template parameters into the default argument.
    看到这里我似乎有些明白我的例子有问题,就是说如果我已经可以从函数的实参推理出了参数类型似乎就没有必要再使用default参数了,是吗?就是说默认参数是最低的优先级要放在最后?也就是说这个函数DeduceTemplateArgumentsByTypeMatch是最后才被调用了。比如 首先从函数foo的模板参数和函数的实参来推测,到了最后再来讨论是否有必要调用那个deduce函数?换言之,default是到了山穷水尽的最后的选择? 我对于这一点还是有些疑惑具体是什么我也想不清楚,标准说的没有错,但是这个default是否解决了呢?因为函数不支持partial specialization,那么我用类来说明: 结果GCC和clang都认为第一个static_assert不成立,clang给的错误很清晰 为什么呢?难道我的模板特例化不成立吗?为什么这里默认参数又提高了优先级呢?
  3. 据说标准不允许模板函数的partial specialization,可是我还是没有找到根据在哪里。
  4. 看来我对于基本概念还是没有理解,比如我还不明白基本的特例化是什么意思: 那么我的特例化到底有什么意义呢?我为什么一定要传递两个模板参数呢?24.09.2021模板实参的个数必须是在primary定义的个数,这个不管你有多少种特例化都是不能变的,除非是parameter pack可以不固定,当然使用了默认参数也可以让实参个数上去不一样,但是需要明确一点的就是不是说模板型参的个数就能改变模板实参的个数,编译器最终还是要检查型参个数一致才放行的。 这里有两条我要体会:
    • Default arguments cannot appear in the argument list
    • If any argument is a pack expansion, it must be the last argument in the list
    这两条好像都是针对我的例子的吧?我还是无法体会真正的含义。

九月二十四日 等待变化等待机会

  1. 中文里对应parameter和argument似乎是一个词,所以,这个可能就是理解上的错误了。比如昨天的问题,如果一个模板需要两个参数,那么哪怕你在特例化下只需要一个模板类型参数,但是你最终传递的还是需要两个参数。这里中文很难表达清楚,英文似乎就比较清晰了,这个parameter你特指的是模板参数声明里的型参,而argument才是模板实例化时候的实参。也就是说不管你需要几个型参最后实例化的时候除非你的模板是parameter pack,否则实参个数是必须正确的,除非有默认参数。所以,昨天的问题是这么理解的,首先当你实例化的时候,编译器先根据模板名来查找primary的模板确定模板参数类型以及个数,这里立刻就引发错误如果个数不够而且又没有默认参数来补齐的话,只有通过了这一关之后才会去在各个特例化中挑选最接近的,这里就牵涉到一个我还始终只闻其名不明所以的partial-ordering,就是说特例化在模板列表里的排列决定了匹配的结果,这个是以后需要理解的,目前我还不够资格。
  2. 标准这里的注解透露出一个信息就是依靠模板函数签名是不能断定特例化的。
  3. 我觉得这个例子可以解决我的疑惑,这里不存在所谓的primary template的概念,甚至我都不太敢确定这个是否是所谓的特例化,应该是的吧?似乎让我更糊涂了。我需要休息一下。 我发现似乎模板参数里的计算是被忽视的,比如我定义一个计算出来的类型而且即便不使用它也是无效的,这个代码是编译不过的 就是说你认为你可以计算出那个匿名的模板参数,但是编译器拒绝承认即便你压根不需要它也不能通过编译。查看clang的AST输出让人有些意外,因为模板的匿名参数被当作了无类型参数NonTypeTemplateParmDecl,这个说明了什么呢? 但是实际上如果把计算类型放在函数参数里是可以的
  4. 这个是clang的基本常识!要使用-Xclang来传递参数,因为-cc1使用的不是driver而是cfe,那么关于builtinin includes,我还没有答案,但是看到的是clang使用默认GCC的搜索路径,那么有没有可能自己指定呢?实际上它是有添加clang的搜索路径的
    
    #include "..." search starts here:
    #include <...> search starts here:
     /usr/lib/gcc/x86_64-linux-gnu/7.5.0/../../../../include/c++/7.5.0
     /usr/lib/gcc/x86_64-linux-gnu/7.5.0/../../../../include/x86_64-linux-gnu/c++/7.5.0
     /usr/lib/gcc/x86_64-linux-gnu/7.5.0/../../../../include/c++/7.5.0/backward
     /usr/local/include
     /usr/local/src/clang-dev/clang-12-debug-build/lib/clang/12.0.1/include
     /usr/include/x86_64-linux-gnu
     /usr/include
    
  5. 我希望在less里看到颜色,不知道为什么clang在-Xclang -ast-dump下忽然都有了颜色,但是如果我要输出std::err的部分2>&1就又没有颜色了,最后看到这个 unbuffer /usr/local/src/clang-dev/clang-12-debug-build/bin/clang -v -c -Xclang -ast-dump -std=c++17 1001.cpp 2>&1 |& less -R才又看到颜色。

九月二十五日 等待变化等待机会

  1. 感觉有些疲惫打算做一些轻松的事情,就是编译clang,主要是因为这个基本上都是等待,不怎么费脑子。这里是我所敬仰的一位大师级人物的编译指示,这个是新版的。大师使用的是苹果笔记本吧和我的不同,但是可以参考。我的目的是编译所有的静态库,很显然的我第一次没有enable clang结果编译的都是llvm的库,结果把我的磁盘都用过了,因为编译一个debug版本居然用掉了几十G的空间。llvm实在是又费磁盘又费内存,一不小心就要出问题。大师提到了一个编译选项就是-DCMAKE_BUILD_TYPE=RelWithDebInfo就是带有debug信息的release build,这个可能更加的适合我因为debug版本太大太慢了,但是没有debug信息又很不方便。尝试一下。这个是我的配置:
    
    ~/Downloads/cmake-3.18.4/bin/cmake -DCMAKE_C_COMPILER=/home/nick/opt/gcc-10.2.0/bin/gcc -DCMAKE_BUILD_TYPE=RelWithDebInfo -DLLVM_ENABLE_PROJECTS="clang;llvm;libcxx;libcxxabi" -DBUILD_SHARED_LIBS=false -DLLVM_BUILD_DOCS=true -DLLVM_BUILD_EXAMPLES=true  -DLLVM_USE_LINKER=gold -DLLVM_ABI_BREAKING_CHECKS="FORCE_OFF"  -DDEFAULT_SYSROOT=/home/nick/opt/gcc-10.2.0/  ../llvm-project/llvm
    
    编译的时候总是报错因为libstdc++.so.6的版本不对,这个才让我意识到我需要指定我gcc-10.2.0的运行库路径,这个可以通过查看运行库的版本理解问题: objdump -p /home/nick/opt/gcc-10.2.0/lib64/libstdc++.so.6以前就遇到过这个问题。所以要添加这个sysroot看看行不行?
  2. 对于反复出现的错误
    ../../../bin/llvm-tblgen: /usr/lib/x86_64-linux-gnu/libstdc++.so.6: version `GLIBCXX_3.4.26' not found (required by ../../../bin/llvm-tblgen)
    我决定做一点debug,使用make VERBOSE=1发现这个错误是发生在那些动态产生代码的编译工作里,比如
    
    cd /mnt/sda1/Shared/llvm-dev/llvm-build/include/llvm/IR && ../../../bin/llvm-tblgen -gen-attrs -I /mnt/sda1/Shared/llvm-dev/llvm-project/llvm/include/llvm/IR -I/mnt/sda1/Shared/llvm-dev/llvm-build/include -I/mnt/sda1/Shared/llvm-dev/llvm-project/llvm/include /mnt/sda1/Shared/llvm-dev/llvm-project/llvm/include/llvm/IR/Attributes.td --write-if-changed -o /mnt/sda1/Shared/llvm-dev/llvm-build/include/llvm/IR/Attributes.inc
    
    这个和你配置的cmake命令完全没有用,我才意识到有的人建议使用LD_LIBRARY_PATH来指向我的包含有最新版动态库的路径,这个我相信是兼容我的ubuntu18.04的GCC-7.5动态库的,但是我是不愿意用我自己编译的GCC-10.02覆盖系统默认的编译器的因为这个绝对是最最糟糕的而且是灾难性的,所以,我只是在这个编译过程中设置这个而已。我其实本来可以老早解决这个问题,不幸地的是之前我设置LD_LIBRARY_PATH的时候居然有笔误以至于一直误导我的想法!真的是一个字幕的代价!
  3. 现在看来设置CC=gcc CXX=g++ -DCMAKE_C_COMPILER=gcc -DCMAKE_CXX_COMPILER=g++是等效的吧,因为都是针对cmake的配置和clang无关的。
  4. 编译至少花了两个多小时。下午花了好几个小时回这个邮件实在是非常的吃力的一件事,我又做了不少的实验。

九月二十六日 等待变化等待机会

  1. 这个wiki的gcc-patch步骤似乎更好一些。不过还是这个changeLog的format说明现在看起来比较让人明白。否则实在是对不起大侠对我的帮助。不过我还是搞不懂有没有什么工具帮我产生这些format。
  2. 对于GCC的ChangeLog我始终不理解,不知道问题出在哪里?

    Components

    • git_description - a leading text with git commit description
    • committer_timestamp - line with timestamp and an author name and email (2 spaces before and after name)
      example: 2020-04-23␣␣Martin Liska␣␣<mliska@suse.cz>
    • additional_author - line with additional commit author name and email (starting with a tabular and 4 spaces)
      example: \t␣␣␣␣Martin Liska␣␣<mliska@suse.cz>
    • changelog_location - a location to a ChangeLog file
      supported formats: a/b/c/ChangeLog, a/b/c/ChangeLog:, a/b/c/ (where ChangeLog file lives in the folder), \ta/b/c/ and a/b/c
    • pr_entry - bug report reference
      example: \tPR component/12345
    • changelog_file - a modified file mentined in a ChangeLog: supported formats: \t* a/b/c/file.c:, \t* a/b/c/file.c (function):, \t* a/b/c/file1.c, a/b/c/file2.c:
    • changelog_file_comment - line that follows a changelog_file with description of changes in the file; must start with \t
    • co_authored_by - GitHub format for a Co-Authored-By
  3. 我发现clang的确是很强悍,对于这个模板函数只有在specialization的时候才判断对错是非常的高明的。所以这个还是GCC的bug,因为还是老问题那个const被丢掉了才导致没有发现错误。

九月二十七日 等待变化等待机会

  1. GCC里这一类密而不宣的设计让人抓狂,这个谁能猜出来作者的想法呢?
    
    /* If non-NULL for a VAR_DECL, FUNCTION_DECL, TYPE_DECL, TEMPLATE_DECL,
       or CONCEPT_DECL, the entity is either a template specialization (if
       DECL_USE_TEMPLATE is nonzero) or the abstract instance of the
       template itself.
    
       In either case, DECL_TEMPLATE_INFO is a TEMPLATE_INFO, whose
       TI_TEMPLATE is the TEMPLATE_DECL of which this entity is a
       specialization or abstract instance.  The TI_ARGS is the
       template arguments used to specialize the template.
    
       Consider:
    
          template <typename T> struct S { friend void f(T) {} };
    
       In this case, S<int>::f is, from the point of view of the compiler,
       an instantiation of a template -- but, from the point of view of
       the language, each instantiation of S results in a wholly unrelated
       global function f.  In this case, DECL_USE_TEMPLATE for S<int>::f
       will be non-NULL, but DECL_USE_TEMPLATE will be zero.  */
    
    这个DECL_USE_TEMPLATE在parsing specialization阶段居然没有用了,那么怎么知道我们现在在干什么呢?
  2. GCC和clang比较起来还是一个针对c语言的优势,毕竟编译速度和内存占用对于传统的c语言和嵌入式是很重要的。我曾经尝试在BeagleBlackBoard这类单板机上运行的Linux上编译内核还是可以的,可是clang/llvm肯定是不可能的。那么我使用目前看来最简单的parser/sema相关的库链接的简单测试程序编译需要的时间是多少呢?一分钟!编译结果是四百多兆,当然这个是debug编译,可是和GCC的cc1plus之类的十几兆是不在一个数量级的,而且cmake生成的makefile似乎根本没有文件依赖性每次都是全部编译,难道静态链接需要这样子吗?头文件没有改变为什么需要全部编译呢?
  3. 重新编译clang需要借助之前的笔记,现在感觉笔记做的越详细事后重新来过化得时间就越少,所以,做笔记是事半功倍的!这里我发现官方的ubuntu-16编译的lib/cmake/llvm/LLVMConfig.cmake设置的开关可以作为我的参考来配置。但是首先我的疑问就是官方编译的ubuntu-16版本的clang依赖的是我默认路径的libstdc++.so.6而并没有版本的高要求,这一点是怎么做到的?是在ABI的配置上做文章吗?因为我一开始还以为要静态链接运行库的办法,现在看来需要关闭对于运行库高版本的依赖来做到。
  4. 我是否应该先尝试这个-DGCC_INSTALL_PREFIX,没有用的。编译完了我的硬盘也用完了,居然用了129G!天哪!我安装又要将近100G,而且我的硬盘已经。我只好安装头文件:make install-llvm-headers install-clang-headers

九月二十八日 等待变化等待机会

  1. 这个是不得已的想法,因为clang编译实在是太大太慢了,并不是我怀有不切实际的想法,而是没有办法的办法,就是看看编译文件找出端倪。这个是我在CMakeLists.txt看到的: 这里我想总结一下我应该的开关如下:
  2. 我记得我在标准c++代码里看到过cout<<nullptr直接打印nullptr的,可是现在好像不行了难道。。。这里说的我不太明白。
    A null pointer constant is an integral constant expression (5.19) prvalue of integer type that evaluates to zero or a prvalue of type std::nullptr_t. A null pointer constant can be converted to a pointer type; the result is the null pointer value of that type and is distinguishable from every other value of object pointer or function pointer type. Such a conversion is called a null pointer conversion.
  3. 不能不编译tool,否则没有办法得到clang的库。
    
     ~/Downloads/cmake-3.18.4/bin/cmake -DLLVM_USE_LINKER=gold -DLLVM_ENABLE_PROJECTS="clang" -DCMAKE_BUILD_TYPE="RELWITHDEBINFO" -DLLVM_TARGETS_TO_BUILD="X86" -DBUILD_SHARED_LIBS=false -DLLVM_BUILD_STATIC=false -DLLVM_STATIC_LINK_CXX_STDLIB=true -DLLVM_ABI_BREAKING_CHECKS="FORCE_OFF" -DLLVM_INCLUDE_TOOLS=true -DLLVM_BUILD_TOOLS=false -DLLVM_INCLUDE_UTILS=false -DLLVM_BUILD_UTILS=false -DLLVM_BUILD_RUNTIMES=false -DLLVM_BUILD_RUNTIME=false -DLLVM_BUILD_EXAMPLES=false -DLLVM_INCLUDE_EXAMPLES=false -DLLVM_BUILD_TESTS=false -DLLVM_INCLUDE_TESTS=false -DLLVM_INCLUDE_GO_TESTS=false -DLLVM_INCLUDE_BENCHMARKS=false -DLLVM_BUILD_BENCHMARKS=false -DLLVM_INCLUDE_DOCS=false -DLLVM_ENABLE_OCAMLDOC=false -DLLVM_ENABLE_TERMINFO=false -DLLVM_ENABLE_ZLIB=false -DLLVM_ENABLE_BINDINGS=false -DCMAKE_INSTALL_PREFIX=/mnt/sda1/Shared/llvm-dev/llvm-install ../llvm-project/llvm
    
    libclang-cpp.so这个是动态库而且有2G这么大!

九月二十九日 等待变化等待机会

  1. 实践以后才能明白它在说什么。原来这个LLVM_BUILD_LLVM_DYLIB 指的是把所有的库集中起来编译一个动态库,而你可以使用LLVM_DYLIB_COMPONENTS来指定你需要的组件的列表,这个是LLVM的库,并不影响clang的工具库。
  2. 网上很多我怀疑是机器做的诗,为了应景我也凑了一首打油诗

    晚舟必归

    晚霞映满天, 舟行碧水间。 必是故人来, 归心难熬煎。
  3. 昨天的问题是我草率的认为编译有问题,其实问题不大,因为对于crc32函数未定义仅仅是是否链接libz的问题,llvm有一个自带的实现,所以,LLVM_ENABLE_ZLIB=false,此外出现terminfo的函数未定义的问题并不是运行库或者标准库链接的问题,而是可以使用LLVM_ENABLE_TERMINFO=false禁止掉,但是这个禁止不了似乎是自动探测的,系统有也改变不了,所以,只能链接的时候加上-ltinfo我看到注解里似乎这个是苹果才有的一个问题。另外我也想通了所谓的libclang-cpp.so这个超级动态库是把所有的静态库集合起来的,我根本不需要。至于说官方如何做到只是一个很薄的动态库的原因我也不知道,我查看过配置文件似乎都是一样的,虽然官方没有config.h,但是。。。这个就是clang和gcc的很大的不同的地方,gcc可以凭输出的配置和版本准确的还原,而clang天知道怎么编译出来的。当然GCC是一个monolith,这样做比较容易,也许也不得不这样子,否则debug太苦难了,这个做法和Linux Kernel也很像,也许是流行的,但是clang编译也许太复杂了吧。
  4. 在公园散步诗兴大发做了这些:

    晚舟必归

    晚霞满眼映天边, 舟行荡漾碧水间。 必有经年远客访, 归心惴惴难熬煎。
    	

    晚舟必归

    晚风吹面彻骨寒, 舟车劳顿不离鞍。 必有秋风送秋客, 归雁南飞报平安。
    	

    秋夜有感

    晚风微雨路灯遥, 夜幕垂肩树更高。 寒冬未至秋未尽, 枯蝉蟋蟀尽自嚎。
    	

    秋夜有感

    秋枫有叶自窈窕, 夜灯无伴更孤高。 微雨无心迎面唾, 化作莹泪挂眉梢。
    	

    秋夜有感

    微雨秋枫夜妖娆, 晚风拂面意兴高。 一年一度中秋夜, 秋蝉声声叹离骚。
    	

    秋夜有感

    夜幕低垂繁星逃, 小雨洗净夜更高。 遥想故乡夜何似? 碧海银沙听海涛。
    	

    秋夜有感

    奈何中华言凿凿, 岂料加国逃夭夭。 不计美国听渺渺, 我自敞怀谆谆教。
    	

    秋夜有感

    夜深才感霜露寒, 四海漂泊难自安。 纵使梦中归故里, 近乡情怯复蹒跚。
    	

    秋夜有感

    少时弄海潮, 弱冠意气高。 人生磨难少, 怎知父母好?
    	

    秋夜有感

    秋风秋雨秋夜寒, 身正影正心更坚。 但使残躯归故里, 报得春晖心才安。

九月三十日 等待变化等待机会

  1. 大侠给我指出我的格式不对,可是我始终不理解gcc-verify是什么玩意?传统的git里没有这个命令啊!这里提到了它可是它是怎么工作的呢?我折腾了许久才开窍。
    1. 首先,这个必须是你从git://gcc.gnu.org/git/gcc.git里checkout以后
    2. 运行contrib/gcc-git-customization.sh它帮你创建了这个alias
    3. 然后你在local递交的就可以使用git gcc-verify来检验了
    4. 想知道它的工作原理吗?
      
      git  gcc-verify --help
      'gcc-verify' is aliased to '!f() { "`git rev-parse --show-toplevel`/contrib/gcc-changelog/git_check_commit.py" $@; } ; f'
      
      原来有这个contrib/gcc-changelog/git_check_commit.py在工作,那么我为什么老是有这个错误ERR: cannot find a ChangeLog location in message就应该可以找出来了吧?唉,折腾到现在才有了一点点眉目,总算没有白辛苦。休息一下吧?
  2. 	

    老父谆谆教诲

    四海为家无妨, 告老还乡正常。 随遇而安灵活, 亲情永世莫忘。
    	

    有感老父教诲

    瓢泊海外虽无妨, 衣锦还乡却非常。 大风歌罢汉高祖, 犹盼猛士守四方。
  3. 散步公园中,偶有所感,口占一绝
    	

    偶感

    隅居若等闲, 翱翔蓬蒿间1 借得鲲鹏翅, 扶摇上云巅。
    1:这个出自于《庄子:逍遥游》,我辈凡夫俗子就如同斥鷃一般在凡人琐事间腾跃上下几仞之间以为人生之巅峰,和扶摇羊角而上九万里绝云气负青天的大鹏相比是多么可怜可笑啊。可这就是普通人的现实生活。
  4. 	

    然则虚度半生无所得 必有好事者挖苦揶揄, 为防人讥讽不如我自 己先改了把他们想改 的路都堵死

    隅居又一年, 扑腾蓬蒿间。 空想鲲鹏翅, 梦里上云巅。
    感觉突然之间我能写诗了,也许就像这位大妈说的艺术上成熟了

  5. 大侠的意思是这个函数insttype = tsubst (TREE_TYPE (fn), targs, tf_fndecl_type, NULL_TREE);有问题。可是这个是我花了两个月debug的起点,我一开始就是从这里开始的,但是我当时是认为函数声明就错了,这里再怎么纠正都来不及了。但是我在之前追踪大侠的问题认识到这个设定,那么一个函数声明使用模板和不使用模板的区别在哪里呢?我还是老办法从打印着手看怎么处理。如果是模板的话,要使用DECL_TI_ARGS来获得函数参数,而非模板使用TYPE_ARG_TYPES

十月一日 等待变化等待机会

  1. 大侠几乎是手把手在教我做最基本的规则,我实在是有些无地自容,看来使用gcc format-patch -1 rev是一个好的办法因为我拷贝粘贴发生错误实在是太不应该了。
  2. 每一步都是一个陷阱,我一直搞不懂为什么我的ubuntu-18.03环境下总是运行python程序有问题,比如contrib/mklog.py是一个11.2.0的新版本,10.2.0的版本是shellscript运行虽然没有问题,但是产生的格式有些不同,但是新版本总是报错:
    
    Traceback (most recent call last):
      File "./contrib/mklog.py", line 39, in <module>
        from unidiff import PatchSet
    ModuleNotFoundError: No module named 'unidiff'
    
    然后我使用pip install unidiff安装成功却始终解决不了问题,这个让我困惑了很久,只好继续使用10.2.0的版本,直到今天我被迫看这个脚本才注意到它的开头是#!/usr/bin/env python3这个才让我意识到可能是python3的问题,因为我的ubuntu-18.04默认是python2,那么我怎么安装都是错误的版本,我这才安装正确的python3-unidiff,终于解决问题。
  3. 大体上工作流程是这样子的:
  4. 现在来看大侠们的讨论我才有点明白他们想干什么了。原来这个函数maybe_rebuild_function_decl_type就是比较TYPE_ARG_TYPESDECL_ARGUMENTS的不同然后重新组建。我有了一个新的想法就是把TEMPLATE_TYPE_PARM也纳入我的保留cv-qualifier的范畴,在我看来模板参数和dependent type一样是不可预知的可能的non-top-cv-qualifier,这个需要测试,这个又是漫长的一天。但是GCC相对于clang的优点在于编译速度和消耗的内存少的多,在gdb里debug也相对有一点点的优势就是能够打印出tree的类型,虽然这个很多问题,但是毕竟debug开发是一个重要的过程。

十月二日 等待变化等待机会

  1. 这个例子是c++20才支持的新feature就是说作为lambda它不能出现在所谓的error: lambda expression in an unevaluated operand这个新功能clang即便使用-std=c++20也不能支持,而我的修改破坏了这个新feature,所以要debug看看 我发现了一个现象就是在GCC的driver下以上程序没有问题,可是使用cc1plus就会crash,这个是什么原因呢?
  2. 今天学到了一个c++的内部函数__is_same,这个可以让我不需要使用type_traits里的库来比较类型,比如
  3. 另一个失败的测试例很好理解就是c++已经把volatile作为deprecated了,所以,我在保留cv-qualifier的时候要把它剔除掉。
  4. 怎样用email发送commit呢?首先要安装git-email, 然后需要设置smtp服务器,这个可以在 ~/.gitconfig里设置
    
    [sendemail]
    	smtpEncryption = tls
    	smtpServer = smtp.gmail.com
    	smtpUser = yourname@gmail.com
    	smtpServerPort = 587
    
    然后可以使用命令git send-email --to=mailid@example.com 001.patch,但是这里有一个问题就是gmail需要默认的更严格的安全设置,所以可以在这里打开关闭。
  5. 这里我终于领悟到大侠们的工作流程都是直接使用这个git format-patch直接发email那么都没有我遇到的这些问题了。
  6. 这个是我的第一次实践发完整的流程

十月三日 等待变化等待机会

  1. 我发现这个错误GCC没有拒绝,这个是我上一次没有发现的,clang拒绝它是正确的,而且给出了很好的原因。 我的新代码可以改正这个问题,而且我发现对于volatile我不需要做任何事情,原本就解决好了,因为参数里的volatile已经被摒弃了。

十月四日 等待变化等待机会

  1. 大神级的人物在讨论的时候凡人能不能看懂是无关紧要的。比如关于lambda的unevaluated context的问题是这个代码的关键。但是看论文是很吃力的比如连例子我都看不懂 大神在这里是为了说明lambda可能会由于模板参数而出现在函数的签名中这样会导致额外的链接,可是我连这个函数的意义都看不大清楚它的参数是一个指向字符的数组的指针吧,可是我有些不敢肯定,因为lambda和模板参数N同时出现用逗号隔开说明什么,虽然我已经反反复复告诫这个builtin comma operator是和初始化无异,但是我始终将信将疑,最后只好把这个参数放到clang的AST-dump来解析,它说对于 类型是const char (*)[3] 所以是数组指针。
  2. 那么大神的下一个例子说的是lambda出现在以下数组长度定义里是否会出现在函数签名里呢? 可是我的难点在于理解这个函数的参数是什么类型呢?毫无疑问是类似的数组指针,但是为什么数组长度里两个()()让我看的心里发毛,难道是什么函数不成?这里还是依赖clang的AST解决了疑惑,因为这个实际上是在定义以及呼叫这个lambda,依赖它的返回值来定义数组的长度,那么为什么要加()呢?第二个好理解因为它是函数的调用操作符或者常说的call operator,那么为什么需要第一个括号([]{ return N; })呢?我的理解是优先级的问题,因为如果不使用的话后面的优先级很高就把lambda的函数体部分{}过去了,这样子就无法解析lambda了。这个简单的例子里有着多少的千言万语啊。
  3. 我一开始对于lambda至简形式没有概念,就是说lambda的脊梁骨是什么?参数括号()是必须有的吗?这一点我一开始没有意识到。
    
    lambda-expression:
        lambda-introducer lambda-declarator compound-statement
    lambda-introducer:
        [ lambda-captureopt ]
    compound-statement:
        { statement-seqopt }    
    lambda-declarator:
    	lambda-specifiers
    	( parameter-declaration-clause ) lambda-specifiers requires-clauseopt   
    lambda-specifiers:
    	decl-specifier-seqopt noexcept-specifieropt attribute-specifier-seqopt trailing-return-typeopt	      
    
    注意到了吧,这个lambda-declarator是可选的,而其中的分支lambda-specifiers全部是可选的,所以,lambda的最简单形式是[]{},那么这个是否就是所谓的unevaluated context?
  4. lambda里真的是非常的复杂,我之前认为parameter pack是一个复杂的地方,那么lambda就是另一个非常复杂的地方,比如,如何赋值给一个lambda呢?这个语法我是怎么也找不到标准的支持,最后查看clang-13的这个选项-std=c++2b -Xclang -ast-cump来输出语法树,我模模糊糊的认为这个也许是user-defined-conversion之类吧就是说把原本lambda改成了lvalue?我不确定。
    
    using A=decltype(+[]{});
    A a=[]{};
    
    这里的+很重要否则编译错误 ,而且这个所谓的lambda在unevaluated context也就是[]{}需要-std=c++20的支持。如果我是真的有一个lambda要返回值怎么办呢?那么就使用所谓的trailing-return-type
    
    using A=decltype(+[]()->int{return 0;});
    A a=[]()->int{return 5;};
    
    而这里如果你要省略这个()你必须要使用-std=c++2b,否则就会报怨什么参数在返回值之前之类的parameter declaration before lambda trailing return type

十月五日 等待变化等待机会

  1. 终于我意识到我的问题是这个为什么会发生?
    
    decltype([]{}) (*s1)[3];
    decltype([]{}) (*s2)[3];
    static_assert(!__is_same(decltype(s1), decltype(s2)));
    
    作为对比这个是标准支持的做法
    
    typedef decltype([]{}) C;
    C (*s3)[3];
    C (*s4)[3];
    static_assert(__is_same(decltype(s3), decltype(s4)));
    
    因为新的修改后的标准说:
    An unnamed class or enumeration C defined in a typedef declaration has the first typedef-name declared by the declaration to be of type C as its typedef name for linkage purposes ([basic.link]).
    这个不太好理解,但是阅读原作者的论文就明白其用意是不暴露lambda在链接中导致的很多问题,比如:
    A typedef declaration involving a lambda-expression does not itself define the associated closure type, and so the closure type is not given a typedef name for linkage purposes.
    换言之typedef等于是掩盖了lambda。那么如果没有typedef的话,lambda作为unique unnamed closure type肯定就是一个个定义的实例都是不同的了,所以,以上的行为是预料之中的。
  2. 由此引申我认为out-of-line defintion不适合牵扯lambda的类型的函数,比如这个 和这个 都不应该被接受,应该报错说找不到声明,而这一点上clang还不成熟。
  3. 	

    无题

    一场秋雨一场寒, 夜半披衣凭雨栏。 疫情阻隔一年半, 千家万户盼团圆。
  4. 	

    无题

    秋雨连绵五更寒, 披衣倚栏望雨穿。 浮生一万五千日, 多少往事似雨烟。
  5. 我现在知道pr92010和函数的签名的关系是不大的,而更主要的是lambda的unevaluated context的问题,那么它是否应该有那个fix呢?这个实际上是比较混乱的,在我看来我的fix能够正确的设定函数签名就可以替代它的fix,但是依然不能解决它自身的那个问题。我现在单单理解这段代码就很困难,这个是我记录下来的: 第一行代码
    1. 首先,这里的遇到的第一个函数是[]{}也就是<lambda()>,它的参数是__closure
    2. 第二个函数是spam它的声明形式是void spam(decltype (<lambda>) (*)[sizeof (T)]),它的参数是s
    然后是第二行代码
    1. 第一个函数是void foo(),很明显它没有参数。
    2. 这里编译器重温了第一行代码的各个函数<lambda()>也就是这里 它的参数有些出乎我的意料居然是this,其实是意料之中的,因为lambda本质上是一个类似functor的实体。
    3. 然后是最让我莫名其妙的地方,这个函数是谁?而且没有位置标识static constexpr void<lambda()>::_FUN(),而它的参数是空的,这个是自然,因为这个_FUN()是不带参数的,可是这个是closure吗?而且还是static的?
    4. 接下来的这个就是更加让我莫名其妙了constexpr<lambda()>::operator void (*)()() const,这个是谁啊?也没有标识。难道是内部的实现吗?它的参数是this我想不出来它是在哪里定义的。难道它是不应该出生的那个?因为unevaluated context允许吧?
    5. 然后又是重复出现了<lambda()>和它的参数this,但是我主意它没有location信息,这个好像是私生子啊?
    6. 然后又是重复了没有位置标识static constexpr void<lambda()>::_FUN()和它的空参数,这种重复我已经司空见惯,因为GCC里有不少这样子的冗余代码反反复复的做tsubst,不停的做模板参数替换。
    7. 不出意料的再次重复static constexpr void<lambda()>::_FUN()和它的空参数
    8. 再次重复之前的constexpr<lambda()>::operator void (*)()() const和它的参数this
    9. 然后我在这里看到了奇怪的现象似乎是栈发生了错误,也许溢出也许怎样,但是我想休息一下再debug了。
    单单理解问题的本质就需要无数的时间,表面上看似乎是这个全局变量current_function_decl的设置的问题,但是它不过是类似入栈出栈的机制,我更倾向于认为是lambda的类型的问题,到底unevaluated context体现在哪里我还是一头雾水,我虽然读了论文的前半段,可是依然不理解到底这个unevaluated的context指的是什么?
  6. 论文的中心思想是
    The reason is that we remove many restrictions on lambda expressions, yet we still want to keep closure types out of the signature of external functions, which would be a nightmare for implementations.
    我完全同意这就是一场噩梦! 那么和unevaluated context是什么关系呢?原来这个是从unevaluated operand的context引申来的概念:
    In some contexts, unevaluated operands appear ([expr.prim.req], [expr.typeid], [expr.sizeof], [expr.unary.noexcept], [dcl.type.decltype], [temp.pre], [temp.concept]). An unevaluated operand is not evaluated.
    这里并没有明确指出lambda的存在,这个是什么原因呢?我想这就是论文的核心,究竟lambda可以出现在以上哪些语境context里呢?我认为作者之所以煞费苦心就是为了不再预设任何限制lambda出现的语境的限制,难道不是吗?我现在渐渐理解了lambda就可以想像是这么一个结构
    
    struct lambda._anon_xxx{
    	auto operator()()const{}
    };
    
    这里的._anon_xxx就是保证每个Lambda匿名且都唯一存在的内部标号。那么这样子就理解了我看到的static constexpr void<lambda()>::_FUN()constexpr<lambda()>::operator void (*)()() const就是lambda的成员函数,后者就是那个万能的auto operator()()吧?但是它好像是一个成员函数指针?而前者是static的更像是lambda通用的函数,也许是做所有lambda的统一管理的?
  7. 我实在是看不下去了,打算找个没人的地方去吐了。这个实在是太复杂了。我开始有一种不安的想法,会不会c++发展的前途让后来人望而却步呢?这个语法复杂到学习十几二十年吗?我曾经看到很多人评论说c++对于一个从学校出来的年轻人需要学习一两年或者两三年,我当时是有些震惊而不敢作声,这个是天才的标准吗?譬如乾坤大挪移心法修炼到第一层普通资质悟性高者需要7年,差一点的要14年。第二层又是7年和14年的周期。。。总共七层,能够练到第七层的需要上百年,世上哪有这样子的天才?
  8. 和我的想法一样还是提交一个新的bug

十月六日 等待变化等待机会

  1. 我还是怀疑栈的问题,但是再一次debug之前我还是想先看看gdb如何能够设定栈的大小吧?可是找不到,却找到一份gdb的秘籍。尤其是那个~/.gdbinit的文件据说能够帮助打印stl的container,我以前经常需要,如果知道就好了。不过呢,仔细看发现其实就是一些类似于小的alias的快手而已并不是那么神奇,比如你要打印一个string的内容其实就是调用它的函数,只不过这里有一个小的技巧,在string里有重载了这个函数
    
    void _M_data(pointer);
    pointer _M_data(void) const;
    
    结果我一开始打印gdb始终报怨缺参数,让我困惑了一阵子,后来才意识到这个是gdb的选择问题,因为我定义的string变量并非常量让gdb选择了第一个函数,所以,我也只能是强制转换它为常量来调用p ((const string*)&s)->_M_data()
  2. 正常的栈的大小是不大可能突破的因为ulimit -S -s显示有8M,所以,一定是什么其他原因或者我眼花了。我重新调整了一下,看到大侠的留言与确认,捉摸了好半天才明白GCC的driver往往会传递参数-quiet给cc1plus
  3. 这样子后者的crash就看不到了,这个不是driver的参数而是cc1plus这样的compiler的参数。这个很不好办,我指的是在测试例里你不可能要求打开这个来测试,大家都是测试driver的,无法绕开driver直接测试compiler,因为GCC支持多语言前端,所以。不过我现在才终于明白官网关于debug GCC的说明,我之前也不明白为什么要这个说明,因为我一直依赖于直接debug编译器cc1plus为什么要使用gcc呢?当然我并不是一开始就明白的,花了我好长时间,如果一开始看到这个wiki也不至于在vfork里挣扎那么久,但是这个说明其实是揭示了driver的一个特殊参数--wrapper的应用,因为它指示driver替它调用下面的compiler,这个做法应该是相当有用的吧?只是我之前一直怀疑它的用途,既然我已经能够知道cc1plus何必要再用gcc呢?现在想来是有用的,起码你能够还原某些只有在driver里才能配置的环境与参数,比如我之前遇到搜索目录的配置就没有办法,居然要先把预处理的结果作为cc1plus的输入文件来debug,我想这个就可以避免了吧?
  4. 我目前看到的和昨天的似乎深入了一步。大侠为什么看到了我没有看到的东西呢?我一直以为是打印的部分有什么玄机跟踪了半天也没有看出有什么不同,但是大侠说是这个函数有问题regenerate_decl_from_template。看来我还是有问题。

十月七日 等待变化等待机会

  1. 真的好似忽如一夜春风至啊,原来这个crash是这么的简单而且似乎人畜无害,因为它就是developer的噩梦,和用户没有什么关系,因为普通用户根本不大可能直接调用cc1plus,也就不可能看到这个问题了,所以,可以说这个是一个development/debug的范畴的问题。而且另一个难题是测试例都无法写,我想过使用自定义spec来强制打开cc1plus的silent默认设置,但是问题是由于大量打印信息的出现GCC测试系统并不能捕捉错误信息,所以,我觉得这个测试例是无解的因为正常测试都是看不到问题的。
  2. 现在一切似乎都很清楚了。最先导致创建这个rebuild_function_method_type的诱因是pr92010揭示的Core1001/1322的老问题,但是解决的途经是不对的,因为直接导致ICE的检查说参数类型不对并不是根源,不应该在模板参数替换发现了错误才去想办法补救,正确的做法是在一开始就防止它的发生。
    
    +         if (processing_template_decl
    +               && (TREE_CODE (type) == TYPENAME_TYPE
    +                  || TREE_CODE (type) == DECLTYPE_TYPE
    +                  || TREE_CODE (type) == TEMPLATE_TYPE_PARM
    +                  )
    +            )
    +               type_quals = (CP_TYPE_CONST_P(type)?TYPE_QUAL_CONST:0) |
    +                       (CP_TYPE_VOLATILE_P(type)?TYPE_QUAL_VOLATILE:0);
    
    我再回滚了92010的新代码之后问题都可以解决。但是故事并没有完,这个护栏强调gcc_assert (same_type_ignoring_top_level_qualifiers_p (type, parmtype));随着c++20的lambda with unevaluated context的出现已经不正确了,比如任何的lambda重建就是不同的类型 这里的的函数调用部分当parser根据函数原型的参数类型来创建新的类型做conversion的时候一定会发现两者的类型不同,因为这个本来也不是必要的,为什么一定要一样的呢?难道不允许类型兼容吗?这一点我不是很确定,但是我以为lambda的出现改变了很多程序员的世界观,它就是编译器实现者的噩梦。

十月八日 等待变化等待机会

  1. 一个彻底的大挫败,大侠早就回复了我,可是我怎么信箱老是没有收到呢?其中最大的问题是我看了好几遍才明白,就是我的改变是在GCC解析定义的时候就对一个模板的参数声明不一致而提出抗议,这一点我以前以为是好事,现在看来是不合适的,因为所有的编译器对于没有实例化之前都是不做这种严格的拒绝的,因为模板参数的类型是,也就是说一个Tconst T是否一致只有在实例化才能知道,但是问题是我今天才意识到所谓的CANONICAL是编译器来衡量类型是否兼容可转换的尺度,而比较类型的时候这个是严格的,那么我在声明期间区分了dv-qualifier导致他们的CANONICAL就是不同类型的,这里也许是GCC实现者故意忽略const的原因吧?难道我的想法一开始就是错的?只有事后补救才是正途?这个让我非常的崩溃,我还是虚心看看大侠的做法再说吧。
  2. 看来只有在git下工作才是正解,因为所有的 补丁都是这样子做的,已经没有人使用纯粹的diff/patch了。
  3. 这里是一个显示git untracked file的好办法: git status --porcelain | awk '/^\?\?/ { print $2; }'
  4. 长期以来我一直误以为编译trunk有问题,其实这个是误解,因为我在configure的时候是debug编译就打开了-Werrorbootstrap stage2 所以,我要加上这个开关--enable-werror=no
  5. 我的脚本总是出错,我也没时间debug了,先存下来吧:
    
    /home/nick/Downloads/gcc-dev/gcc/configure CFLAGS='-ggdb3 -O0' CXXFLAGS='-ggdb3 -O0' LDFLAGS=-ggdb3 --prefix=/home/nick/Downloads/gcc-dev/install --disable-bootstrap --enable-static --disable-shared --disable-checking --enable-languages=c,c++,lto --disable-multilib --enable-gcc-debug --enable-cpp-debug --enable-werror=no --build=x86_64-unknown-linux-gnu --target=x86_64-unknown-linux-gnu --host=x86_64-unknown-linux-gnu
    
  6. 对于bootstrap我现在有些担心,如果我的代码有问题应该是通不过bootstrap的检验度的,所以,第一次编译一定要使用正确的代码,或者说是released的代码,那么如果我禁止了bootstrap这个行为会是怎么样的呢?

十月九日 等待变化等待机会

  1. 我想总结一下这个问题,就是花了两三个月终于证明我的想法虽然好但是GCC目前代码的现实也许还是大侠的方法更好一些,主要就是这个例子的问题。 这个看上去是一个用户定义和声明不一致的错误,但是实际上编译器都容忍它,
    1. 首先,在没有具体实例化的情况下不能绝对说就是错误的,比如模板参数是普通类型的int时候模板的声明和定义并没有差别。
    2. 其次,即便是复杂类型int[3],clang/MSVC++选择接受,因为从声明来看是可以成立的。当然这里大侠的意思是说标准没有对这类看似错误的方式有规定,实现者可以自由选择,GCC选择依赖定义类型,所以对于这个报错,但是对于下面这个类似的表示接受 当然类似的,clang/MSVC++表示反对。我对于此感觉也无可无不可,反正标准没有规定完全由实现者来牵就用户的行为,能做到哪一步都是好的。
    3. 但是以上的两个例子却恰恰导致我的方案不行,因为我在声明时候保留了const,导致GCC在解析定义的时候查找声明而拒绝,也就是说即便没有实例化我也不能接受,这个成为一个致命伤,我明白这里的含义就是原则上模板在未实例化以前编译器甚至都可以不对其做语法检查,这个当然是极端的理想,实际上从效率方便角度看所有的编译器都会做基本的检查,但是我的做法打破了这个原则,这个是大侠指出的致命伤。
    4. 那么我能不能在模板定义查找声明的时候放松参数类型的检查呢?这个很难,因为参数类型是所谓的CANONICALIZED,也就是说没有含糊的空间,这个是为了后期计算类型是否兼容以及转化做准备的,这里没有放松的可能的。所以,GCC目前的做法就是一律剔除cv-qualifier,这样子检查总是能够通过,而在确定实例化和特例化的时候再根据定义的类型重新造出函数类型来和特例化做比对。这就是为什么现在GCC只能支持特例化和out-of-line的定义要一致而不是像clang/MSVC++那样根据模板声明来一致的原因。
  2. 学习concept,因为这里面其实很复杂。
  3. 我定义了一个concept但是却不起作用,也许哪里写错了。 10.10.2021是的我理解错了requires的用法,它是一个很神奇的东西,它只接受一批批的expression,就是说你可以写一些你期待的语句,这里和concept后跟的能够转化为true/false的statement是不同的做法。 所以一个正确的做法是这样子的:
    1. 直接使用一个判断语句来表明你的要求
    2. 第二种是你可以更加的直白的写一些小孩子才会写的代码,比如 当然这里的问题是编译器不会替你去检查数组的长度,只是看语法上有没有问题,所以,你哪怕传进来一个指针也是一样。
  4. 我老是忘记这个literal operator,我也不确定这个叫法对不对,我是觉得那个user-defined-literal是另一个东西,比如这个在string里定义的函数就是很有用的一个,它有着某种raw literal的功效:11.10.2021这里要怎么告诉编译器你只需要引进这个函数呢?using std::operator""s;,这里我有些诧异因为我一直以为它就是定义在string头文件里的,可是看样子这个是std名字空间里特别定义的,不知道在哪个头文件里。是的,实现在bits目录下,那么声明在哪里呢?看到它在string_view这个头文件里,但是是在literals::string_literals名字空间下,是很深的除非在什么地方加入到顶层的std名字空间里,不过这个是细节我肚子饿了。 这个代码我一开始看的一头雾水,似乎不得要领,看笔记和例子才回忆起来,它就是一个高级的思路,就是函数的各种各样的重载的妙用。这里要回忆一下通常的string的ctor是什么样子的,这里有什么问题吗?没有长度!因为它依赖的是字符串的所谓的null-terminated特性,这个就是问题,对于指定长度这个工作一般的程序员是不可能接受的,凭什么我要多一个参数呢?string literal的长度不是天然可以在编译阶段就知道的吗?为什么要运行期再来计算一遍呢?这个就是其中的妙用,因为添加一个根本没有人愿意使用而且有歧义的ctor是解决不了这个问题的,这个就是为什么需要引入这类literal suffix的函数或者说操作符的原因,只要是它们指定了函数参数就强制编译器替它们计算长度传入适当的string的ctor,这个就是其中的奥妙。
  5. 这里我总觉得什么地方有问题,但是就是想不清楚。比如如果用一个concept来约束参数类型。 或者是用直接的类型来约束并且使用特例 然后我定义一个变量lambda来给它们调用

十月十日 等待变化等待机会

  1. 我以前印象中gdb set args是把所有的命令行参数都换掉了,现在发现仅仅是把可执行程序的参数换掉,这个当然是有道理的,因为所有的debug信息都已经读到内存里了,各个参数也是依赖这个,如果连可执行程序都变了那还不如退出重新执行一个命令呢!所以这个才是正确的,我以前的印象肯定是错的。
  2. 另一个关于lambda的疑团似乎也有了眉目,作为lambda你既可以把它看作是一个结构也可以把它看作是一个方法,所以,我觉得这里才是问题的核心所在,如果我把一个lambda的数组作为函数的参数来传递会发生什么呢?
    1. 首先我们有一个定义和约束它的参数必须是数组的指针,这是因为如果你定义你的约束为数组的话永远都不成功,因为数组类型在被函数创建签名的时候已经转化为了指针才行,这一点我其实也没有想的很清楚,到底谁先谁后呢?
    2. 然后我们定义一个数组并作为参数传递给函数
    3. 但是如果我们希望按照数组的引用来传递参数可以吗?比如直接调用foo(l);,这要求我们改变约束,可是这样子似乎不行,这里首先是一个lambda的特性就是始终唯一性,比如这个永远如此 因为每一个lambda的实例都是不同的匿名的类型。那么从这一点我们似乎可以推论你不可能把一个lambda数组进行传递,因为它们不可拷贝因为类型就改变了。 换言之这个是编译器在说 注意到这个类型decltype(+[]{})是可以互相转换的,其中,decltype(+[]{})可以被任意其他转化为目标类型,而decltype([]{})永远不能被转为目标类型。 这个就是根本的原因!
    4. 所以,我尝试了一个上午大概这个是唯一最接近的做法吧?我现在也想不清楚到底要怎么做了。
  3. 这里引出一个关于convertible的问题,这个我一开始没有明白以为copyable/assignable/swappable就是convertible,实际上它们是完全不同的,说明里说的太好了就是想像有一个虚拟虚假函数To test() { return std::declval<From>(); }这样子才能够称得上是把类型From转化为To是可行的。这个说的太好了。那么任意两个类型能否可转换取决于什么呢?关于内置类型POD编译器早有定论,用户类型除非自定义了转换函数比如 注意这个is_convertible的参数是有顺序的! 我后来才看到这里的更准确的解释:
    User-defined conversion function is invoked on the second stage of the implicit conversion, which consists of zero or one converting constructor or zero or one user-defined conversion function.
    这里提到second stage of implicit conversion,那么什么是first stage呢?还有就是ctor和function两个可以同时存在那么要采用哪一个呢?
    If both conversion functions and converting constructors can be used to perform some user-defined conversion, the conversion functions and constructors are both considered by overload resolution in copy-initialization and reference-initialization contexts, but only the constructors are considered in direct-initialization contexts.
    这一段我读来如同天书,似乎并没有回答我的问题,也许是有先后吧?就像函数重载是排列有顺序的找到一个就可以不应该一个个都符合的都运行一遍,所以,我的问题是无厘头的。看了后面的例子我才恍然大悟,有顺序也没有顺序,同时存在就是歧义ambiguious,所以要使用direct-initialization消除歧义。我决定摘抄这个例子,它写的很好:
    1. 首先,在To里定义了conversion constructor,同时在From里则定义了conversion operator,这两个都可以把一个From对象转换为To的一个实例,那么。。。
      
      struct To {
          To() = default;
          To(const struct From&) {} // converting constructor
      }; 
      struct From {
          operator To() const {return To();} // conversion function
      };
      
    2. 场景一是所谓的direct-initialization,这个是我第一次听说这个名词
      
      	From f;
          To t1(f); // direct-initialization: calls the constructor
      // (note, if converting constructor is not available, implicit copy constructor
      //  will be selected, and conversion function will be called to prepare its argument)
      
      所以,这里说的是必须使用ctor,没有歧义。而且注解里解释说如果没有定义ctor的话要使用所谓的copy ctor,但是会先调用conversion operator(function)。这里让我想起来我在哪里看到的一种说法作者认为ctor实际上是一种特殊的conversion operator,当然这个是一个个人看法,其实看起来是有道理的。
    3. 而下面的场景是使用copy-ctor的场景,所以有歧义。
      
          To t2 = f; // copy-initialization: ambiguous
      // (note, if conversion function is from a non-const type, e.g.
      //  From::operator To();, it will be selected instead of the ctor in this case)
          To t3 = static_cast<To>(f); // direct-initialization: calls the constructor
          const To& r = f; // reference-initialization: ambiguous
      
    4. 后面那些例子我不想看了太复杂了,我以为任何一个c++程序员都熟悉virtual可是我感觉很陌生,我从来没有听说过什么叫做virtual dispatch,难道说虚拟函数不就是这个机制吗?也许是各家的名词不同吧?微软以前叫做什么late-binding之类的吧?实在是有些灰心丧气,学了十几年几乎和刚开始呀呀学语的小孩子一样的无知。不看了。
  4. 这一个早上把我累坏了,因为太多的信息了,根本记不住啊!

十月十二日 等待变化等待机会

  1. 其实这个根本就是基本常识,只不过因为有了lambda就看上去有些眼花缭乱。传统的做法是这样子的:
    1. 首先普通人是这么做的一个类型声明 这个揭示了它的本质类型是什么,虽然lambda是唯一不可复制的类型,但是typedef有把它固定化的魔力。现在Ary3就是一个很普通的函数指针的长度为3的数组。
    2. 其次,我们使用普通的函数声明和定义 第二行是人类容易读懂的写法,第三行才是机器容易读懂的形式,这里就已经看到了函数签名里要把数组参数类型转为指针的结果了: void (void(*const*)())
    3. 那么当我们在实际运用这样一个函数的时候就会遇到这样的简单的问题,我能够限制传入的参数类型是不同长度的数组还是说压根就是一个指针呢?不能。 很明显的虽然lam4是一个长度不同的数组但是转化为指针之后根本上函数参数是兼容的,达不到我们要求限制函数参数类型的目的。所以,虽然参数类型我们做了严格的限制,但是由于函数签名自动把数组转化为指针的缘故无法达到类型检查的目的。这个是司空见惯的问题。
    可是假如我们想使用concept来限制模板参数来达到函数参数类型的限制可以吗?其实也基本上不行,但是有一点点的作用。
    1. 首先,定义一个concept并用它来限制模板函数的参数类型
    2. 可是当你直接把参数lam3代入函数的时候编译器爆出了一系列的错误,比如这个是不行的bar(lam3);,因为编译器不明白模板参数是什么,这个也是比较奇特的你必须指示它 注意到模板参数必须要符合concept的要求,但是实际参数因为以上的数组转为指针的缘故已经没有区别了,所以,作为模板函数比普通函数稍稍的好一点就是强制要求调用者心里明白参数类型的限制。
    3. 那么究竟这个模板实例函数的类型是什么呢? 这个说明了什么呢?我们的concept试图做和普通类型定义一样的函数类型限制。两者是等价的。
    结论:不是concept不行,是函数参数数组转指针的固有原因导致你无法限制函数参数数组长度的一致性。因为函数只看到指针并不知道长度,这个是c语言已经固有的问题了,c++也许可以用模板参数来解决一部分,但是无法彻底解决。
  2. 大侠的补丁可以解决这个问题了,虽然它和其他的测试例本质上是一样的,但是牵扯到concept和lambda还是有参考意义的:

十月十三日 等待变化等待机会

  1. 又发现了一个新的bug。大侠指出的对,它和数组无关,但是肯定是成员函数相关的,它出现在所谓的out-of-line-definition的时候lambda违反了它应该是unevaluated-context的新c++20特性。
  2. 又提了一个bug,从错误信息就能看出这个是requires的问题: GCC目前对于函数参数lambda是没有问题的,下面是证明:

十月十四日 等待变化等待机会

  1. 关于昨天的问题,我想一定是在这个函数里有问题cp_parser_template_argument_list
  2. 大方向也许不错,但是具体的是cp_parser_postfix_expression因为decltype(+[]{})里的这个加号+
  3. 和我期待的不一样,看来parsing似乎没有什么错,因为在cp_parser_decltype里加了这个标志量cp_unevaluated_operand禁止了lambda的evaluate,在有些地方我看到有尝试使用RAII的技术把这个标志量的设定取消做成一个简单的类。所以,我还是要回到开始的部分,为什么模板参数没有被替换。
  4. 秋雨有感

    绵绵秋雨无心至, 飘飘落叶刻意离。 秋色无边晴方好, 昏鸦枝头向天啼。

十月十五日 等待变化等待机会

  1. 我感觉这个问题的复杂程度远远超过了我的想像,比如在解析A<decltype(+[]{})>的时候这个所谓的lambda究竟是否要被当作在class A的scope呢?GCC似乎是这样子的,否则无法解释这个结果 也根本无法理解这个超级怪物到底是什么,这个应该是超过人的理解能力的 我决定放弃了。
  2. 睡眠零碎断续。。。

    秋夜有感之一

    梦醒五更心彷徨, 半生漂泊多荒唐。 往事如梦难追索, 长夜漫漫盼月光。

    秋夜有感之二

    梦醒四更夜正凉, 索句不得卧在床。 半生浮沉寻常事, 世纪更迭人断肠。

    秋夜有感之三

    秋夜五更夜更长, 被窝虽暖夜正凉。 掌上看尽天下事, 坐困蒙城作夜郎。
  3. 秋色有感之一

    落英缤纷红叶生, 小径悠悠叹雨声。 枝头乌鸦啼不尽, 万籁俱寂待秋风。

    秋色有感之二

    风驻雨霁意踟蹰, 红叶乌鸦不独孤。 秋色无边万籁寂, 人在画中更何图?

    秋色有感之三

    落叶纷纷满地黄, 蒙特利尔好风光。 更盼严冬大雪日, 白雪红枫胜苏杭。
  4. 大侠非常的看顾提到了我微薄的贡献
  5. 第一次遇到objdump-C开关就是demangle不起作用,google到了有人说-Cr而不是常用的-Ct让我猜想我可能遇到了传说中的versioned的函数吧?实际上这个r指的是引用,不知道为什么clang的函数编译后不太支持-C,clang对于编译ABI之类的做了很多手脚,应该对于一些链接也是有特殊处理吧?比如这个函数是怎么编译成的呢?
    clang::Module::getFullModuleName[abi:cxx11](bool) const-0x0000000000000004
    另一个老问题就是之前我遇到过的undefined reference to `typeinfo for clang::ModuleLoader',这个我始终搞不明白,有人说这个是虚函数未定义的缘故,可是这个是在头文件里实现,虽然我以前是用-fno-rtti解决了但是不理解为什么会发生?

十月十六日 等待变化等待机会

  1. 重新捡起之前停下的clang的改造工作,说是改造那是高抬自己实际上就是重新写个简单的Makefile来编译,因为clang的编译是个非常复杂的问题,复杂在于支持多平台以及工具链,对于我来说根本不关心,所以本地编译应该是很简单的,这个是我之前精简的parser需要的库的列表:
  2. 终于按照之前的编译方式做了一个Makefile来编译我的clangParser,编译了291个.o文件和8个.a文件再链接LLVM的9个.a文集,可执行程序大小469M,编译时间42分钟,这个实在是太慢了,让我实验一下并行编译。很显然快了不少但是耗费内存如此之多我的eclipse直接就crash了也没有办法统计编译时间了。
  3. 秋日有雨

    蒙城秋雨泼入帘, 阴云压顶到眼前。 凭栏遥望无限远, 一缕乡愁入眼帘。

十月十七日 等待变化等待机会

  1. 我只能说unevaluated lambda就是太过于强大了以至于它是实现者的噩梦,一个看似简单的问题让我刚开始就放弃了,我发现的问题是早已存在的,至少半年了。因为它的难度陡增了几倍吧?因为之前的问题尽管复杂但是还没有到混乱的地步,这个是在仿佛体制外突然开辟的新天地一下子把原有的机制大破,实现者只能在原有的架构不改变的情况下设定所谓的unevaluated_operand之类的类似于入栈出栈的全局标志量来表明当前的状态,那么如何在原来的流程里精准的插入你的新代码真的是很难。我感觉这一领域的问题是极度的复杂。总有一天会有一个总爆发也许在c++23/26/29或者哪一天开发者们彻底绝望不得已开辟第二战线,这个不是不可能的。目前的局势是不可持续的,尤其要应付新的c++标准的增加,在和clang/MSVC的竞赛中,GCC已经快要丧失在支持新标准的优势了,当人们意识到GCC最后沦为一个纯粹依赖于其对于c语言坚如磐石的支撑而存在的时候,会被c++开发者所青睐吗?你不能仅仅因为是Linux Kernel一个重要的支持就独立运作,这个也许是GCC存在的唯一重要理由了吧?当然这个一定是十年后当新的c++标准更加的出神入化的时代才会有的问题。这个问题相对来说还是不太严重的,之所以这么说无非就是它仅仅是报错机制的问题,归根结底是error.c里的各种输出问题,具体来说就是lambda导致的死循环,这一点任何程序员都能看到:dumptype =>dump_aggr_type =>dump_scope =>dump_type =>dump_aggr_type =>dump_parameters =>dump_type =>dump_aggr_type,一旦进入这个死循环只有耗尽栈溢出了。那么问题的源头在哪里?我觉得不应该怪error.c的这个输出模式,一定是输入类型lambda的问题,我感觉既然lambda在unevaluated context下它的<DECL_CONTEXT是什么就值得考虑了,我模糊意识到我似乎看到过类似的问题,这些fakelambda应该是不属于任何人的,所以,我觉得创建这些lambda的时候不应该设定它们的context似乎它的context没有设定,不过我也没有办法深入追踪,我只能放弃。。这个说起来容易,做起来难!很难。而且也不一定能解决具体的问题,因为具体的问题还有很多,你面对一百个错误,即便改正了其中一个甚至九十九个你看到的结果依然是不对,这个就是困难的地方。如果一个复杂社会常年积累下来的社会问题,改革者大都是摸索前行,有时候即便理性分析告诉你这个是迈向成功的必须走出的第一步,但是在一个以结果为导向的社会体制架构下大家看不到你的改变的任何积极意义的时候是不会给你支持的,就像当年奥巴马喊出了change的口号,大众期待的目光中却发现个人的生活并没有发生什么积极的变化就都失望了,因为一个积重难返的社会问题积累耗费很多代,同样革除也需要很多代,然而人类是没有耐心与牺牲精神的,为什么前辈人的错误要后辈来承担代价?同样的后辈人在没有同意当代人债留子孙的主张的情况下在还没有出生的情况就背负了当代人的欠债。这个就是当今的社会。

十月十八日 等待变化等待机会

  1. clang的parser的写法的确是和GCC有很大不同,因为它一开始就是一个基于人性化的角度出发。所谓人性化就是一开始就假定程序员会犯错误,而编译器怎么帮助程序员纠正错误是一个重要的方面,就是说不是简单的返回错误还要尽可能猜测错误的可能发生原因,这个是GCC那个年代所不可想像的,那个时代是所谓的英雄时代,一个程序员可以独立写一个操作系统,一个人可以独立完成一个库的工作,程序员追求的是多快好省,而且当时的硬件软件环境也不允许把这个放在比较高的位置。我第一次看到它使用一个validator的类来在不同场合下猜测纠正比如拼写错误或者可能的改正方法。另一个有趣的现象就是clang把lex/token的概念扩展到了一个不同的层次,传统意义上的lexer/parser的概念似乎不是我想像的那样子,GCC至少这个代码写的很分散,而clang全部放在一个巨大的switch/case里让你可以找到所有的逻辑,这个当然是好,而且效率也是高的,你要是看到GCC的无数多的peek_next_token就会觉得这个是好主意。和GCC显著不同的是模板的处理,GCC是看到模板就要一口气把它解析成功,这个自然有一点点效率的优势,但是我之前的那个patch就栽在这里,原因是你在很早的时候就要对于模板做出裁判,而clang是把所有的模板的处理放在滞后来做,这个是有优势的,因为很多特例化以及像那个out-of-line的definition之类的最好是不要急于下结论,而GCC其实也总是使用很多类似于tentative再回滚,可是为什么不对于整个模板做这个模式的处理呢?有些事情不到结尾你是不能下结论的。而且这样子更加的有助于错误的信息提示,因为在顺序处理的过程中很多时候是看不清楚错误的根源的,你压根不理解程序员想定义什么做什么。这个就是我看了两行代码得出的心得。
  2. 我决定先保存一下这个Makefile,它依赖于llvm的库,而把clang的所谓的静态库自己编译并使用一个最最简单的输出AST的方式来验证基本的语法正确性。我在想Sema是否真的需要,其他的LLVM的库我能够mock一下,毕竟我并不想真的产生目标代码,我仅仅对于clang的parser部分代码感兴趣而已,不是吗?
  3. 预处理很显然是各自有各自的玩法,这个我以前就感觉到了,不同编译器定义的各自的宏是没法兼容的,除非标准有规定,那么搜索路径也是各自编译器部署的自由发挥。这个是clang的相对应的命令和输出结果: 相对应的GCC的搜索路径就多了一个
  4. clang的这个开关-dump-raw-tokens应该是很有用的,可以看到很多的lex内部操作,不过看起来这个是纯粹的c-lex,就是我看到的那个内部的c语言的实现部分,不知道能不能输出c++的token?哦,是-dump-tokens
  5. 找到一个很酷的功能-emit-html就是可以把代码输出为html格式,确实很实用!而这个输出颜色的开关也不错-fcolor-diagnostics

十月十九日 等待变化等待机会

  1. 看到clang代码里的注解让我产生歧义理解,虽然我明确记得标准是不允许函数返回一个函数的,但是搜到这个帖子还是让我大吃一惊,提问者水平是很高的,他提出了一个严肃的问题:c++语法是否禁止这样子呢?看来他是对的,从单纯的语法来看似乎是允许的。而回答者更加的高,图文并茂解释翔实,可以说是stackoverflow里的一流经典帖子,值得收藏,我决定本地留下一个拷贝。
      为了加深记忆和不至于剽窃大侠的工作,我还是用自己的话来复述问题吧。
    1. 首先,针对的是这样一个看起来平淡无奇而又震耳欲聋的类型: int f(char)(double),它是什么?这个问题真的不容易回答啊!我头脑中很模糊,它是一个类型吗?合法吗?
    2. 或者我们要回到更加原始朴素的类型就是int f()(),这个是什么?一个类型?代表什么?
    3. 作者从参考网站找到函数的语法定义,我们先重温一下什么是函数的声明?为了突出terminal,我把原来的语法修饰了一下以便更清楚:

      语法

      注解

      noptr-declarator ( parameter-list ) cvopt refopt exceptopt attropt (1)
      noptr-declarator ( parameter-list ) cvopt refopt exceptopt attropt -> trailing (2) (since C++11)
      noptr-declarator any valid declarator, but if it begins with *, &, or &&, it has to be surrounded by parentheses.
      parameter-listpossibly empty, comma-separated list of the function parameters
      attroptional list of attributes. These attributes are applied to the type of the function, not the function itself. The attributes for the function appear after the identifier within the declarator and are combined with the attributes that appear in the beginning of the declaration, if any. 这是一个很复杂的东西,我从来没有用过,举例[[noreturn]][[gnu::unused]], [[deprecated("because")]],关于它仅仅是修饰返回值这一点我还是比较难以理解。(since C++11)
      cvconst/volatile qualification, only allowed in non-static member function declarations绝对不要小看这个要求!
      refref-qualification, only allowed in non-static member function declarations这个我似乎从来没有接触过!(since C++11)
      excepteither dynamic exception specification(until C++17) or noexcept specification(C++11) Note that the exception specification is not part of the function type (until C++17)那么c++20到底还承认不承认它是函数类型的一部分呢?我感觉函数签名和函数类型这两个是不同的概念!我以前当它们是同义词是不对的。
      trailingTrailing return type, useful if the return type depends on argument names, such as template <class T, class U> auto add(T t, U u) -> decltype(t + u); or is complicated, such as in auto fpif(int)->int(*)(int)我想起来了这个我在stackoverflow上看过一个帖子就是这个返回值的问题的,因为到底t+u转换的类型是什么有时候还是让编译器来决定比较好,否则int+double,或者signed+unsigned之类是很头疼的,有时候不仅仅是警告的问题很可能是错误的问题。并且有些时候是模板函数太过复杂程序员有必要减轻编译器的困难直接标明返回值比较好。
      requires这个不是函数声明的语法部分,但是我的理解是它是所有声明的一部分,既然函数名字是declarator那么它也是一个可能的组成部分 As mentioned in Declarations, the declarator can be followed by a requires clause, which declares the associated constraints for the function, which must be satisfied in order for the function to be selected by overload resolution. (example: void f1(int a) requires true;) Note that the associated constraint is part of function signature, but not part of function type.这就是我说的函数签名和函数类型的不同之处,到底函数重载或者说模板特例化是依赖于函数签名还是函数类型?我以为是前者,所以,早期的模板函数特例化似乎是依赖于函数类型吧?这个是我之前debug那些GCC的感觉,因为在遥远的c语言时代似乎两个概念是同样的,所以,在模板声明阶段就可以做出一个可以依赖替换模板参数的函数类型,也就是说declarator和早期的c语言的函数类型一样?这个是我的猜测,而clang等待到最后再处理模板就有这个优越之处因为现代c++的函数太复杂了! (since C++20)
    4. 重温语法就让我吐血,现在才回到主题:提问者认为这个类型int f(char)(double)可以理解为一个函数名f期待一个参数(char),返回值为一个类型为int(double)的函数类型。这个理解当然是有原因的,因为你在c语言里要怎么声明一个返回这样一个函数指针呢? 我对于c语言这类声明是深恶痛绝因为太过于烧脑子了,比如普通人都是这么做的
      
      typedef int(*FunType)(double);
      FunType f(char);
      
      但是换作正宗的c语言的语法是这样子的
      
      int(* f(char))(double);
      static_assert(__is_same(decltype(f), int(*(char))(double)));
      
      我估计谁看到这个都要头晕吧?所以作者有这个想法是顺理成章的,问题是这个类型为什么是合法的?作者高就高在这里他对于语法有精深的研究这里我摘录他的分析:
      1    declarator:
      2       ptr-declarator
      3       noptr-declarator parameters-and-qualifiers trailing-return-type
      4    ptr-declarator:
      5        noptr-declarator
      6        ptr-operator ptr-declarator
      7    noptr-declarator:
      8        declarator-id attribute-specifier-seq opt
      9        noptr-declarator parameters-and-qualifiers
      10       noptr-declarator [ constant-expression opt ] attribute-specifier-seq opt
      11       ( ptr-declarator )
      12    parameters-and-qualifiers:
      13       ( parameter-declaration-clause ) cv-qualifier-seqAfter
      

      Roughly speaking, after 1->2, 2=4, 4->6, 4->6 you should have ptr-operator ptr-operator ptr-operator Then, use 4->5, 5=7, 7->8 for the first declarator; use 4->5, 5=7, 7->9 for the second and third declarators.

      然后作者就证明了这个解析是合法的int f(char)(double)真的吗?我觉得这里是有错误的,因为后面的两个不对,因为noptr-declarator不可能终结,只有7->8可以终结,而其他9,10,11都是自身的循环,所以这一点来看它是不成立的?是吗?
    5. 那么我们来看看正解是怎样的。首先,回答者先明确这个是被明文禁止的,其实就是回答了提问者的问题,标准在哪里有提到的问题。
      Functions shall not have a return type of type array or function, although they may have a return type of type pointer or reference to such things. There shall be no arrays of functions, although there can be arrays of pointers to functions.
    6. 其次,回答者给出了通常的解决办法,这一点很好。就是实用std::function,当初我还纠缠于boost的和std的异同,其实,两者是同源同宗的等价的吧?也许有些微的差别,我不知道,因为这个没有编译器的语法新支持,是纯粹的标准库的实现应该是等价的,仅仅是头文件的不同吧? 这里就隐藏了函数类型里指针的问题,具体实现当然是超出了我的能力。
    7. 作者使用的这个图我很喜欢,收藏起来学习。我不知道他是用什么工具产生的?所以说提问者的解析是不对的,回答者的解析是可信的,不会产生三个ptr-operator并排,只有7->9才是正解。当然结论不变语法确实是允许。但是semantics不允许这样解释。
    看完这里我都要吐了,毛主席以前教导过考试作弊不是坏事因为不会就抄一遍也是一种学习的手段,这就是当年文革造反派交白卷的由来,其中的积极意义的一面就是抄袭别人的文章也是一种学习。所以从这个意义来看剽窃也是一种学习的手段,只要不是有利益的目的应该允许剽窃。
  2. 看到一个不明所以然的东西,还没有时间去尝试这个tree-sitter,只能用不觉名厉来形容,因为我还看不懂它要做什么,这里看到它实用的语法是以前遇到过的c++的EBNF语法网站,这个值得收藏。我现在觉得这个是今天发现的最棒的工具!我打算无耻的收藏它!

十月二十日 等待变化等待机会

  1. 一个很小的问题,但是记忆力是不够的,总是忘记,就是GCC的terminal是怎么定义的,或者说lex部分是在libcpp里的头文件cpplib.h里的宏定义的,这个是一个很好的定义enum并间或把对应的字符联系起来的办法,因为c/c++语言缺乏所谓的reflexive的能力吧,这个词是我听说的并不太理解。
    
    #define TTYPE_TABLE							\
      OP(EQ,		"=")						\
      OP(NOT,		"!")						\
      ...
    #define OP(e, s) CPP_ ## e,
    ...
    enum cpp_ttype
    {
      TTYPE_TABLE
      ...
    #undef OP
    
    所以,这里我们仅仅是定义了enum cpp_type,如何利用它来输出对应的字符串呢?同样利用宏来做
    
    struct token_spelling
    {
      enum spell_type category;
      const unsigned char *name;
    };
    #define OP(e, s) { SPELL_OPERATOR, UC s  },
    static const struct token_spelling token_spellings[N_TTYPES] = { TTYPE_TABLE };
    #undef OP
    
    这里定义了一个结构来对应enum和字符串的map,这个是特别用来输出的,那么比较的表怎么定义呢?我能问出这样的问题就暴露了我的天真与幼稚,如果不是c语言支持如此复杂的charset和各种各样的编码,比如那个我始终不明所以的digraph/trigraph之类,否则任何人都可以轻易的写一个预处理器,这里还没有涉及宏处理呢,仅仅是所谓的execution character set就是一个可变量,每次看到这个东西就让我想起来很多年前听说的类似汉芯造假级别的骗经费的故事,大概就是说要实现自主知识产权的使用汉字编写代码,注意不是c standard的UTF-8,那么当然是要使用国标编码了,我能够想像的他们的说词肯定就是源代码使用自主知识产权的汉字编码,这里不仅仅是以前我以为的使用UTF-8的变量名使用汉字的搞笑做法,而是彻底的源代码编码使用GB18030这类的,这个估计就是外国人没有合适的编辑器打开源代码文件发现一片的乱码然后就向国家科委之类的官僚说这个是什么发明创造领到一笔科研经费,这个当然是传说,可是在远古时代未必没有人这么干过,因为这个在编译的时候需要设定-finput-charset= 总之在预处理里工作量其实是非常大的,不是代码运行耗费的工作量而是写这些处理程序是很繁琐的,大概现代的程序员没有一个愿意耗费时间去造轮子做这个无用功。关于以上的说明我是找到了切实的代码佐证的,在libcpp里的charset.c里使用iconv来先转换编码然后再使用源码所谓的内定的UTF-8来做预处理的。而且有一个看上去让人害怕的所谓的ucnrange之类的部分居然来检查各种编码的合法性,我看不懂只能猜测大概是针对各种编码有哪些组合是不可能的吧?或者是c/c++各种不同标准中编码的疆界?总之,绝对没有什么人愿意触动这里的代码,实在是太繁琐了,而且毫无意义的浪费时间。编码是个力气活,而且吃力不讨好的古代人才干的力气活。
  2. 实际上有些问题是很基本的问题,但是我从来没有认真读过书一直想不到是怎么回事。比如编译器严格区分为预处理器与解析器,那么意味著在前者处理的结果基础上才能开展后者,可是clang真的是这么做的吗?预处理器的token一定是不变的吗?比如早期c++98对于模板的结尾的>>是不允许的必须要加空格,那么操作符的歧义只有在第二阶段才能辨识,那么这些基本的如操作符的解析都要放在后者才能做到吧?可能这个就是clang里分为两层lex和preprocessor的概念原因吧?两者是有区别的。
  3. 这个又是一个简单而基本的问题,怎么在BNF里表达否定呢?答案似乎是不行,虽然EBNF有这样子的表达式,但是这位大侠说的好:
    Context free grammars are not closed under "difference" or "complements". So while you might decide to add an operator "subtract" to your BNF, the result will not be a context free grammar even if it has a simple way to express it. Consequence: people don't allow such operators in BNF grammars used to express context-free grammars.
    话是这么说的,但是实际上人们怎么对付这类语法的呢?其实,一个简单的想法就是不要趟预处理的泥水集中在语法层面的处理而已。

十月二十一日 等待变化等待机会

  1. 秋日有雨

    落叶飘飘漫天飞, 秋雨乍至有心回。 悲天悯人秋亦老, 凛凛寒风不尽吹。
  2. 制作了另一个bnf的语法文件,这个是一个简单的测试程序

十月二十二日 等待变化等待机会

  1. 假如世界失去auto会怎么样?很多年前有一个著名的广告语大概就是这个样式的问句,事实上是如今世界照样前进。这个世界上没有什么是替代不了的,如果有的话,那么多加一些auto吧?假如你想定义个能够调用自己参数的函数要怎么定义?而这个参数恰好也需要调用自己的参数呢?用c程序的说法是一个回调函数的参数也是一个回调函数,我尝试使用lambda,结果我自己都看的头晕了 这个庞然大物似乎很复杂,因为我本来需要的就是这么一个简单的函数调用:lambda(lambdaParam, lambdaCall);可是你需要这么复杂吗?这个是不是很违反人性?人性就是追求简单啊: 这样子不是简单多了吗?你需要管参数是什么类型吗?有编译器真好,可是编译器的实现者真的日子不好过啊。
  2. 据说html里id和name是没有区别的,但是我在Geany编辑器里看到它之为后者生成标记,可以作为文档的目录来用,所以,这个也许是一个小小的好处吧?23.10.2021但是html5说应该使用id来替代name因为后者过时了。应该听谁的?还是改回去吧?
  3. 实际上真正免费的事情是没有人热心干的,一个简单如验证html错误的开源工具都很难找到,tidy的textarea处理有很大的问题。我找到一个网站列表一些使用boost的应用,也许可以参考。

十月二十三日 等待变化等待机会

  1. 早上起来使用tidy清理一些笔记的html错误,顺便想统计一下字数
    
    cat  /mnt/sda3/diabloforum/public_html/2021.htm|wc -m
    1038922
    
    居然今年到现在为止有一百万字了,虽然很多是拷贝粘贴的摘要,可那也是一个辛苦活啊。能够有百分之几的增益也是可观的。

十月二十四日 等待变化等待机会

  1. 还是保存一个这个lambda in unevaluated context的文档吧?其中有不少的例子。
  2. 但是关于昨天的auto,如果全部是虚招子编译器也是无所适从的,比如我花了好久才意识到一个基本的常识就是默认参数并不能够告诉编译器参数的类型是什么因为转换类型几乎是无限多的。比如 如果你去调用这个foo();你会得到编译错误error: no matching function for call to ‘foo()’ 这里你认为你给了编译器提示参数类型par应该是可以从默认参数推理出来,其实不然,因为第一我很怀疑默认参数是否参与参数类型的推理,很可能压根就没有这一步骤。其次,就算有也是徒劳的因为可以转化的类型是非常多的。所以,以上只有这么改正给编译器一个提示 那么我们再进一步再包装一层lambda如何呢? 这里我一开始以为默认参数会被考虑,可是函数的类型里实际上是不包含默认参数的,也就是说decltype(foo)就是包含一个参数的,所以,你只能骗一骗编译器使用一个无用的空lambda来充数decltype(call){},如果我们最终调用lambda(foo);的时候发生了什么?当然不用担心当我们实际调用的时候会正确调用最终的call且慢!这里会有一个大问题,你的call还是你期待的那个call吗?不!除非你遵守lambda的unique原则!:。所以,这里要重新定义一下call
    
    auto call=/*+*/[](){
        std::cout<<"really call"<<std::endl;
    };
    
    只有你设定你的call是一个真正的唯一不变的lambda你才能保证最后lambda(foo);最终调用的是call,当然如果不是的话程序会crash,因为调用的lambda是一个已经被消灭的临时变量。
  3. 关于clang的编译我几乎就是一无所知因为之前我只是使用它的cc1plus根本不需要产生可执行代码于是这个基本的常识我竟然不知道,就是clang不会默认帮你链接c++标准库,所以你必须加上-lstdc++,多么基础的东西啊!我不知道。g++是默认的吗?这个我也不清楚,难道是直接静态链接标准库?有时间再看吧

十月二十五日 等待变化等待机会

    1. 我又提了一个bug,因为之前的那个requires的问题似乎不被认可我自己也产生怀疑了。 那么这个似乎是做实了因为在11.2之前还是ICE,trunk似乎fixed了但是仍旧有特例化找不到的问题。
    2. 这里让我学习了什么是这个convertible_to概念,它是它的大表兄is_convertible的一个延展,虽然后者是一个类型模板,
    3. 这里的教育意义在于它有两个要求: 首先满足std::is_convertible_v<From, To>
    4. 其次,要求这个requires的表达式要成立:
    5. 这个其实相当的不好理解,因为我对于这个std::declval就始终没有理解对,以前只是记住需要这个虚拟函数的语法成立。但是最最核心的是我没有理解它的真正的意义是把类型变为rvalue-reference,这一点和std::move似乎正好相反很相似只不过后者It is exactly equivalent to a static_cast to an rvalue reference type. ,它们有一点共同相似的就是都是听上去高大上,但是实现起来却如此的简单,比如前者的最经常的应用场景就是跳过类的ctor而直接调用它的成员函数取得它的返回值,听上去多么复杂的工作却依赖于这么简单的一个add_rvalue_reference来实现的。
    6. 那么究竟这里我学到了什么呢?怎么解释这个判断呢?
    7. 注意前者的类型是
    8. 而后者是一个rvalue_reference!这一点没有想到吧?
    9. 这个是一个等价的表达式
    10. 那么我为什么关心这个莫名其妙的后者呢?因为它是concept里的convertible_to的实现部分。 这个虚拟函数要求起码的类型匹配就是说,可是实际上这两个类型并不是十分的匹配
    11. 那么这个后果是否就会导致我之前定义的concept失效呢? 26.10.2021这类问题有一个模式就是concept都是一个partial specialization,而其中的模板参数是一个lambda in unevaluated-context,或者说decltype里的lambda。我现在追踪到这个函数总是失败build_x_unary_op,当然这个是一长串函数的起点,单单看语法就知道这个路径有多长了。
    12. 那么我是否要重新定义我的concept呢?然而令人意想不到的是这不仅可以而且很奇怪,奇怪就在于说它成功了,而我仅仅是使用convertible_to的实现,我的猜测是我避免了模板特例化
    13. 我感觉头很疼了,去呼吸一下新鲜空气吧?
  1. 秋日有雨

    独立寒秋听雨声, 细若游丝飘如风。 满目萧瑟天灰朦, 无尽秋意在心中。
  2. 这个是有一点出乎我的意料的,因为它的特例化没有问题:
  3. 而这个东西难倒了clangMSVC++,我全都去提交bug了一番。 这里我之所以使用麻烦的std::is_same而不使用内嵌函数__is_same是因为MSVC++不支持这个内嵌函数。总之三大编译器对于函数参数的类型都是对的,可是对于模板特例化只有GCC做出了正确的解析。
  4. 我感觉这个就是所谓的常识,不过我心里还没有建立。就是一个模板类的声明是否代表它的特例化的声明: 那么如果我直接使用这个类是没有问题的 这似乎就是问题的终结,但是假如我多此一举定义一个特例反而会导致成员函数foo没有声明了 这个特例化会遮挡了最基本的模板定义,反而导致错误了。

十月二十六日 等待变化等待机会

  1. 一个人老了就是表现在对于很多代码的头疼,比如这个让我看的就是很头晕,因为在gdb里没有意识到这个递归因为它被一个本地的为了少写两行代码的简单的宏包裹了,直到我在gdb里转晕了才看到源代码大概是这样子的(我为了突出重点省略了其他参数...):
    
    tree tsubst_copy_and_build (tree t,  tree args,...){
    #define RECUR(NODE)  tsubst_copy_and_build(NODE, ...) ...
    	...
    	return build_x_unary_op(..., RECUR(TREE_OPERAND (t, 0)),..);
    	...
    }
    
    这种写法简直是欺负人啊,反正我是坚决反对,c++标准里有些细节是关于参数调用的顺序的,我不知道这个是否有相关性,但是仅仅就我在gdb里来说这个就是噩梦,当然我可以理解这个是还原语法里的递归,但是有没有什么更好的办法呢?这里使用gdb实在是很头疼,一不留神在那个参数的递归里就转了好多圈,回来的时候已经糊涂了到底是在哪了?但是看起来也没有什么更好的办法吧?
  2. 制造工具和使用工具是两个截然不同的范畴,也就是当初智人区别于猿猴的标志性能力。发现bug和修复bug是差别了好几个数量级的难度。

十月二十七日 等待变化等待机会

  1. 28.10.2021没有学基础理论是不行的,还是要从基础学起,靠自己琢磨是不可能理解的。 我头疼了一天才意识到这么一个简单的道理,看来我是太愚钝了,这个就是一个常识:从BNF直接转换成程序是有困难的。这个是概念LR parser (Left-to-right, Rightmost derivation in reverse)是所谓的LR parsers are a type of bottom-up parser that analyses deterministic context-free languages in linear time.。而与之对应的LL parseran LL parser (Left-to-right, leftmost derivation) is a top-down parser for a restricted context-free language. It parses the input from Left to right, performing Leftmost derivation of the sentence. 比如
    
    attribute-specifier-seq: 
    	attribute-specifier
    	attribute-specifier-seq attribute-specifier
    
    这两个rule你执行了第一个并不知道是否就应该结束,因为你有可能错过,可是如果你执行了第二个假如失败了你难道再返回去吗?我可以把这个简化为最简单的一个语法为:
    
    A:
    	'a'     ###1
    	A'a'   ###2'a'A ###应该要改成Right-Lookahead吧?
    
    那么这个A是一个non-terminal,而'a'代表terminal就是a,这个可以解析regex的[a]+,那么对于字符串"aaaa"你到底要怎么执行两条规则呢?执行#2成功3次失败1次,然后执行#1成功1次,这个做法太笨拙了。
  2. 我意识到我从来就没有明白课堂上的东西,现在也全忘了。这篇论文是不是应该读一下呢?这个是Knuth当年的论文。我仿佛就是一个秋天忙忙碌碌的松鼠收藏了很多的松果但是都没有吃,结果冬天来临还是被冻死了。看了第一页明白了LR和LL的由来。总共三十多页。

十月二十八日 等待变化等待机会

    1. 这个是关于LL和LR的解释定义: 假定一个语法其中的ε代表空集合
      
      S=>AD, A=>aC, B=>bcd, C=>BE, D=>ε, E=>e  (2)
      
    2. 它的语法树是这样子的
      
      b  c  d  e
       \ | /  /
         B   E
          \ /
       a   C   ε
        \ /   /
         A   D
          \ /
           S
      
    3. 那么下面这个
      
      S=>AD=>aCD=>aBED=>abcdED=>abcdeD=>abcde	(4)
      S=>AD=>A=>aC=>aBE=>aBe=>abcde	(5)
      
    4. 这里就是Knuth说的
      In order to avoid the unimportant difference between sequences of derivations corresponding to the same tree, we can stipulate a particular order, such as insisting that we always substitute for the leftmost inter- mediate (as done in (4)) or the rightmost one (as in (5)).
    5. 似乎在老爷子眼里两者的区别是不重要的,是吗? 但是这里的关键在于老爷子指出如果我们解析abcde这个字符串按照从左到右(Left-to-right),先总是解析最右边(Right-most-first)的Intermediate的话因为(5)就是这样子总是先拆解最右边的,那么顺序就是(5)逆序(Reverse Order)
    6. 这里作为一个鲜明的对比来看,(5)是有优势的因为它是从树的枝叶开始按照从左到右的顺序好像栈一样的逐层的向上解决语法树。而(4)的算法顺序似乎不明显。
    1. 大段大段的晦涩难懂的数学公式看的我头疼,还是从这个例子来理解吧?我感觉我就是一个标准的猿猴只能理解具体的例子,毫无抽象思维的能力。这个是语法
      
      S=>aAc, A=>bAb, A=>b (6)
      S=>aAc, A=>Abb, A=>b (7)
      
      这两个语法代表等价的语言{ab2n+1c}。
    2. 但是令人惊奇的结论却是两个语法的属性并不相同
      Grammar (6) is not LR(k) for any k, since given the partial string abm there is no information by which we can replace any b by A; parsing must wait until the "c" has been read.
    3. 结论是惊人的因为
      On the other hand grammar (7) is LR(0) , in fact it is a bounded context language; the sentential forms are {aAb2nc} and {ab2n+1c}, and to parse we must reduce a substring ab to aA, a substring Abb to A, and a substring aAc to S.
    4. 由此大师得出的结论是:
      This example shows that LR(k) is definitely a property of the grammar, not of the language alone.
      翻译成大白话就是说即便是语言本身是LR(k),如果语法写的不好也可能就不是LR(k)了。看来语法定义是相当的重要,不代表说语法树一样那么这个语法就是正确的了,因为不适当的语法有可能增加parser的设计难度。
    5. 大师的这句结论要随后才能体会
      The distinction between grammar and language is extremely important when semantics is being considered as well as syntax.
      grammar对应language,那么semantics对应syntax,这个是两个层次的对应。很深的概念啊。
    6. 这个是一个语法:
      
      S=>aAd, S=>bAB, A=>cA, A=>c, B=>d  (8)
      
      对应的语言是{acnAd} U {acn+1d} U {bcnAB} U {bcnAd} U {bcn+1B} U {bcn+1d}
    7. 大师给的例子说如果字符串是{bcn+1d}的话要怎么处理?这个我看的很费劲,回来再看吧。
    8. 29.10.2021翻译大师的话: {acn+1d}{bcn+1d}的话是可以从右往左解析的,因为两个可以分别解析为aAdbAd,但是从左往右解析却有含糊性,你看不到最右边的d之前你没有办法做出决定,而这个中间隔着的c有任意多个,所以,这个语法是无界的,自然也就不是所谓的LR(k)了。
  1. 出门散步得诗一首:

    秋日有感

    猎猎东风寒冬至, 秋色斜阳意迟迟。 帝国衰败成落日, 中华崛起会当时。

十月二十九日 等待变化等待机会

  1. 关于LR(k)的定义的数学表达我看不太懂,只有这个大白话比较容易理解。和以前的认识也没有很大的出入,仅仅就是要求这个识别是唯一性的。
    A grammar is L R ( k ) if and only if any handle is always uniquely determined by the string to its left and the k terminal characters to its right.
  2. 大师举了一个反例说明这个不是bounded right context但却是LR(0)
    
    S=>aA, S=>bB, A=>cA, A=>d, B=>cB, B=>d  (9)
    
    的确这个从左往右毫无悬念,如果是a开头一定是走S=>aA,而b开头一定是走S=>bB,但是从右往左到底是走A路线还是B路线只有当看到最左面的字符是a还是b才能确定。所以,和(8)呼应,一个语言是LR(0)结果不合适的语法导致从左到右无法做到却可以从右到左做到。而现在正好反过来了。这进一步说明语法很重要!
  3. 大师下一个有趣的例子揭露了我的意识中的盲点,我还是按照以前的自动机原理去想问题,我们的parser不是简单的只有栈内存的自动机,而是可以数数的比如:
    
    S=>aAc, S=>b, A=>aSc, A=>b  (10)
    
    这个之所以是LR(0)的原因是我们不需要也不可以判断直到我们看到最右边的b,而这期间我们只需要记住有多少个c,或者使用简单的栈来记忆就是记住一个奇偶性的数值也可以,所以,符合左边所有的字符都是可以唯一确定语法而无需有任何右边的字符的定义:LR(0)
  4. 大师的论文虽然是基础课,可是看这种论文实在是太辛苦了,每天大概能看两三页,不知道能不能看完这三十多页。
  5. 大师的证明实在是太高深了看不懂,我现在试作看看这个语法(2)怎么翻译:看不懂证明!
  6. 老爸诗云:

    闲居细品人生

    注定是尺莫求丈, 三分人事七分旁。 粗茶淡饭有真味, 乐天知命岁月长。

    和老爸闲居品人生

    蛰居蒙城日月长, 尺寸之间成圆方。 春花秋月冬日雪, 盛夏美景赛苏杭。
  7. 试着理解大师的这个语法:
    
    S=>BC, B=>Ce, B=>ε, C=>D, C=>Dc, D=>ε, D=>d
    
    那么我们现在看看它组成的语言是什么?让我们遵循LR的原则每次都先拆解最右边的intermediate。
    S
    BC S=>BC
    BD C=>D BDc C=>Dc
    B D=>ε Bd D=>d Bc D=>ε Bdc D=>d
    Ce B=>Ce ε B=>ε Ced B=>Ce d B=>ε Cec B=>Ce c B=>ε Cedc B=>Ce dc B=>ε
    De C=>D Dce C=>Dc Ded C=>D Dced C=>Dc Dec C=>D Dcec C=>Dc Dedc C=>D Dcedc C=>Dc
    e D=>ε de D=>d ce D=>ε dce D=>d ed D=>ε ded D=>d ced D=>ε dced D=>d ec D=>ε dec D=>d cec D=>ε dcec D=>d edc D=>ε dedc D=>d cedc D=>ε dcedc D=>d
    制作这个表格相当的辛苦就是为了得到所有的LR(3)里的所有的长度为3的前缀字符串:
    εεε cεε ceε cec ced dεε dcε dce deε dec ded eεε ecε edε edc
    这里我要么理解错了要么我的表格计算有误因为大师没有这个dcε
  8. 我还是看不懂大师的证明,唉,明天再看吧?

十月三十日 等待变化等待机会

  1. 论文就是这样子,只要中间一个概念定义看不懂后面就全看不懂,一篇严谨而充实的论文往往是环环相扣没有什么东拉西扯的废话,前后呼应。所以,看不懂就是看不懂了。
  2. 大师有一个语法描述fully parenthesizing algebraic expressions involving the letter a and the binary operation + :
    
    S=>a, S=>(S+S)         (25)
    
    并且大师有三条改造条件来创造一个新的语法:
    Given any such string we will perform the following acts of sabotage:
    1. All plus signs will be erased.
    2. All parentheses appearing at the extreme left or extreme right will be erased.
    3. Both left and right parentheses,will be replaced by the letter b.
    然后假如我们有一个字符串是((a+a)+a),那么大师定义如下:
    Here B, L, R, N denote the sets of strings formed from (25) with alterations (i) and (iii) performed, and with parentheses removed from both ends, the left end, the right end, or neither end, respectively.
    这个是我的理解:
    
    B=>aaa, L=>aaba, R=>aaa, N=>aaba
    
    我从逻辑上可以理解大师的语法肯定是对的:因为B本来就等价于去掉左边L的和右边的R,可是问题是这个语法是怎么执行的呢?还是看不懂。
    
    B=>LR, L=>LNb, R=>bNR, N=>bNNb
    
  3. 感觉还是反回头来尝试最基本的binson/flex,这个是入门的尝试:
    1. 使用bison可以定义%define api.pure full.y文件里
    2. main里可以使用extern FILE *yyin;来直接打开文件给lexer,并且你可以检验一下反复调用yylex()函数返回的token是否正确。
    3. 直接调用yyparse查看返回值来检验成功与否。
    这个简单的测试就立刻意识到问题了,比如作为一个ANSI C的parser仅仅是语法正确,那么语义的产生才是最大量的工作,目前的GCC就是语法和语义部分纠缠的太紧密了吧?
  4. 老爸诗云

    他乡久了成故乡, 情随事迁都一样, 哪有地方不养人, 五湖四海任你闯。

    我的附和

    男儿有志走四方, 客居加国成故乡。 但凡草木服水土, 哪有花开不芬芳?

十月三十一日 等待变化等待机会

  1. 我以前下载过这个yacc的grammar,这个是parser的,这个是lexer的。我以前没有实践过,今天折腾了大半天,主要搞不清楚怎么退出的问题。
  2. 我只好先使用前辈的现成的Makefile来运行看看。首先,编译的依赖性设置比较啰嗦,这里有一个小细节,bison编译.y文件的时候如果直接使用这个开关-o可以不要去改文件名,那么debug的时候源代码文件应该不至于找不到。其次,这个完整的开关-d -t -v -y --report-file=xxx可以输出很详细的状态,这个大概就是大侠证明编译正确的理由吧?最后链接的时候我没有意识到大侠使用了-ll链接了系统的lex库,这个避免了本地定义yywrap,而有意思的是编译使用gcc因为是c语言,最后链接需要g++,我没有想到它需要把c++的运行库包含进去。这个也许是我手动改的结果吧?因为前辈用了一个古老的头文件iostream.h,这个简直是太老了,我只好用现代c++来替代,可能就导致链接必须使用c++的运行库?

十一月一日 等待变化等待机会

  1. 我觉得到现在为止我的无用功并非完全的无用,我之前疑惑的是语法要怎么解决这些BNF的循环,而现在看到的是这些是上个世纪六七十年代的问题,但是并不等于现代的程序员明白其中的道理,实际上人不需要重新造轮子,但是一个合格的工程师也许应该明白造轮子的方法。这个是继承与创新的问题,没有继承是无本之木。这里看到的是所谓的GLR的算法,就是当语法有模糊的时候也就是说不是严格的LR(1)的时候就叫做冲突(conflict),实际的解决方法是要维持效率做一个取舍,但是有可能会遗漏一些正确的选择,所以,有这个类似于深度/广度搜索的算法,也许能够解决语法是LR(2)直至LR(k)的问题。但是这里我看的有些糊涂就是说这样子产生的编译器实际上是内嵌了这个搜索算法而在实际的解析中按照预设的搜索算法分进合击,当然预设的一些搜索是殊途同归的话是直接就合并了。那么这里是要和语义分析(semantics)结合才能做到吗?还是纯粹的语法解析?也许都有吧?难道不是吗?反证要等到一个action消失就自动放弃,产生语法错误的一个来源就是action返回错误啊。另一个点点的收获就是看到同样这一句语法在两个不同的项目中做了不同的处理,在一个更加贴近实际的项目里被大幅度的改造了,因为要适应所谓的递归搜索,而在那个没有任何改造的项目里bison爆出了很多的conflict,所以,我的感觉是简单的BNF语法需要很细致的改造才能避免这些conflict,也就是说人类手写的很多语法看起来不一定就是正确的LR(1)语法。这个只是我的揣测,难道c++委员会写的c++bnf并非是LR(1)的语法?这个让人难以置信,应该是我理解有错,不过标准已经声明这个BNF语法就是一个说明性质的,要怎么证明一个语法是LR(1)呢?是否使用bison没有conflict就说名了吗?作为c++语法并没有指望编译器一定要做到LR(1)啊?这个是两回事,也许语言是LR(1)但是语法不一定,因为要找到正确的LR(1)的BNF语法也是一个很不简单的工作,至少这个是我读Knuth的论文的唯一的微薄的一点收获,只看懂了前面五六页就看不下去了。
  2. 我真的要感谢stackoverflow上的这些霸王贴,这个是我起的外号因为它们大概可以永远霸占这些经典的问题,它们的作用很多时候甚至超过了wiki,因为后者是一个全面的介绍但是很多时候提问者的问题是其中很针对性的具体问题,所以,这个是回答怎么判断一个语法是否是LL(k)或者LR(k)的方法!但是看懂这个回答真的不容易,反正对我来说很不容易,首先我完全忘记了所谓的FIRST/FOLLOW SETS,这个也许在自动机课堂上教授做过,可是我完全忘记了。对于一个non-terminal的FIRST和它的FOLLOW的terminal的集合如果发现有和其他的non-terminal有重合那么就是有FIRST/FIRST或者FOLLOW/FOLLOW,或者和它自己的FIRST/FOLLOW冲突,让我摘抄加深记忆:
    • FIRST/FIRST conflicts, where two different productions would have to be predicted for a nonterminal/terminal pair.
    • FIRST/FOLLOW conflicts, where two different productions are predicted, one representing that some production should be taken and expands out to a nonzero number of symbols, and one representing that a production should be used indicating that some nonterminal should be ultimately expanded out to the empty string.
    • FOLLOW/FOLLOW conflicts, where two productions indicating that a nonterminal should ultimately be expanded to the empty string conflict with one another.
    这些定义非常的晦涩难懂,如果没有例子讲解简直就是天书一样的难懂。看这些评论才能明白FIRST/FIRST-CONFILICTS需要的是同一个symble的不同productions而不是发生在两个symbols。这个是一个很重要的误区吧?
    A FIRST/FIRST conflict occurs when two productions for the same nonterminal have overlapping FIRST sets. Even though X and Y contain b in their FIRST sets, there is no nonterminal in the grammar with two productions, one of which starts with X and one of which starts with Y.
  3. 太多了,太多的所谓的快速检验法,但是系统的呢?

    Some simple checks to see whether a grammar is LL(1) or not. Check 1: The Grammar should not be left Recursive. Example: E --> E+T. is not LL(1) because it is Left recursive. Check 2: The Grammar should be Left Factored.

    Left factoring is required when two or more grammar rule choices share a common prefix string. Example: S-->A+int|A.

    Check 3:The Grammar should not be ambiguous.

  4. 首先还是复习一下定义

    In LL(1)

    • First L stands for scanning input from Left to Right. Second L stands for Left Most Derivation. 1 stands for using one input symbol at each step.
  5. 检验LL(1)语法很简单就是画表
    For Checking grammar is LL(1) you can draw predictive parsing table. And if you find any multiple entries in table then you can say grammar is not LL(1).
    问题是怎么画?
  6. 这位前辈高人的帖子找时间来看一看!

十一月二日 等待变化等待机会

  1. 我看到bison有一个神奇的功能是产生所谓counter-example,其中打印出来很漂亮的树形使用了一个看不懂的字符好像回车符问题是很多时候不知道这个字符的编码怎么办呢?这里的方法很好:
    
    var="$(echo -n '↳' | od -An -tx1)"; printf '\\x%s' ${var^^}; echo
    
    或者
    
    echo -n '↳' | od -An -tx1 | sed 's/ /\\x/g'  
    
    可是问题是我打印出来的e2 86 b3和unicode代码\U21B3不一样?这里是否是编码的不同呢?难道是UTF-8编码的问题吗?
    
    echo $'\xe2\x86\xb3'
    ↳
    
  2. 这个扯得太远了,我本来是想说这里下载的平铺直叙的语法是有问题的。比如这个c++的yacc文件,这个parser.y里的语法是完全没有任何的改变,看来这个做法是过于天真了,以前很多人在stackoverflow里说官方的BNF不可能产生编译器说的原因是很多其他原因,当然有语法语义本身解析的模糊性,但是没有人提过这个语法是说明性的并没有设计成你要实作的符合LR(1)或者LL(1),其实,我对于编译器到底应该是否可以使用LL(1)来实现,难道clang就是吗?GCC是LR(1)吗?从我读的理解就是后者是更有效率,能够控制内存防止worstcase而且无需猜测或者backtrack之类的。 又一次的大神的帖子是指路明灯,这里说的太好了!
    GCC/Clang are not strict recursive descent parsers; they allow backtracking (reparsing an arbitrarily-long text), among other deviations from theoretical purity. Backtracking parsers are strictly more powerful than non-backtracking parsers (but at the cost of no longer being linear time).
    而最有价值的是这三点原因:
    • the preprocessor, which is not particularly complicated but cannot be described by anything remotely resembling是的,这个实在是浪费时间的工作,没有人愿意花时间在这上面,而且搜索路径取舍的决策都是人为主观的决定。 a formal parsing framework;

    • template instantiation, which intermingles semantic analysis into the parsing process是的,我感觉c++的模板完全就是另一个语言需要另外的一个编译器,这个也是我从一开始探索编译器的直接原因,也许在语法稳定以后大家集中精力开发一个超级强大的模板库是未来的编程方向,而不是不断的扩张语法编译器的功能。你看boost在没有编译器支持的情况下实现了大部分c++的新feature,随后编译器添加的支持只不过是为了减少损耗的跟进而已。 (and needs to be done in order to discover the correct parse);

    • name resolution, which some might not consider to be part of parsing, but you're not going to move on to the next step until这个不是单纯的syntax的正确与否而是类似于bison里的action返回结果干预编译器解析的过程 you know which syntactic object a particular identifier refers to. (If you think name resolution is simple, reread the 13 dense pages of the C++ standard in section 6.5 (Name lookup)要去看看!, and then move on to the 35 pages on resolving overloaded names, section 12要去看看!!.)

    这里要补充定义Recursive Descent Parser
    In computer science, a recursive descent parser is a kind of top-down parser built from a set of mutually recursive procedures (or a non-recursive equivalent) where each such procedure implements one of the nonterminals of the grammar. Thus the structure of the resulting program closely mirrors that of the grammar it recognizes.[1]
    所以,这里就很明白了,如果GCC/clang是所谓的LR/LL之类的实现那么根本就不用人去开发直接使用yacc/bison生成就好了,关键是有很多的原因这样做不行。我的理解是把语法解析转换为状态机来补缺补漏对于人来说太困难了,所以不如直接用函数来实现,当然这里就有很多不严格的状态被遗漏,这个是优点因为是捷径,也是缺点因为有可能出错。这个朴素的道理是三岁小孩子都明白的,我难道是今天才想到吗?当初上编译器课的作业就能够体会的,真是多馀。
  3. 这里说Antlr是可以产生LL编译器的。看来我的概念还是不对,因为这里说LR编译器需要大量的内存,所以大家才发明LALR来解决这个问题。能不能这样理解这里的解说:LR是最基本的而且可以以所谓的linear的时间来解析所有的计算机语言因为它们都是deterministic,否则就是语法(语言?)本身有含糊问题。但是代价是耗损巨大的内存尤其是语法有很深的递归的时候,为了解决这个问题就发明了SLR就是简单的不要异想天开的想当然!SLR和LALR的表是一样的,只不过它的输入语法有限制,这个导致它不被计算机语言待见把很多状态合并?看这里解说SLR和LALR都是实现Knuth的理论的LR(1)编译器,或者说实用版,它们的区别在于产生的算法在计算lookahead set的区别,SLR比较简单不用考虑状态只是针对语法来产生,这个我想普通人都是这么想的。而LALR就要考虑其他因素。换言之,LALR的总的状态就少了因为它可以内部合并我是怎么得到这个结论的?。总而言之, SLR的语法要求更高更严格,因此LALR的语法可能对于SLR来说就有冲突。这些都是没有什么实际意义的空理论概念,大多数人用脚趾头都能想出来的。真正的难点是看实际的例子。我现在头疼了。
  4. 总之在看bison产生的反例之前还是把wiki的那些例子理解了再说吧? 这里说了一句关键的
    If some nonterminal symbol S is used in several places in the grammar, SLR treats those places in the same single way rather than handling them individually.
    也就是说SLR是在没有产生所有的状态机之前就计算lookahead set
  5. 我最喜欢的一首古筝曲《赤伶》 microfono

    昨晚想去看电影结果电影院不开门还淋了一场雨得小诗一首

    昨夜北风吹, 梦见百花摧。 平明探究竟, 红叶漫天飞。
  6. 这个是
    A grammar that has no shift/reduce or reduce/reduce conflicts when using follow sets is called an SLR grammar.
    定义。
    The lookahead sets calculated by LALR generators are a subset of (and hence better than) the approximate sets calculated by SLR generators.
  7. 这里有些基本的概念还是要温习一下:
    1. LR parser是bottom-up parser就是从识别terminal开始。
    2. Bottom-up parsing patiently waits until it has scanned and parsed all parts of some construct before committing to what the combined construct is.
      难道Top-down就不是这样子吗?
      The opposite of this is top-down parsing, in which the input's overall structure is decided (or guessed at) first, before dealing with mid-level parts, leaving completion of all lowest-level details to last.
      的确如此。Top-down一开始就靠猜的吗?
    3. 这就是要害了,因为如果语法开始部分有很多重叠的话,靠猜就困难了。
      If a language grammar has multiple rules that may start with the same leftmost symbols but have different endings, then that grammar can be efficiently handled by a deterministic bottom-up parse but cannot be handled top-down without guesswork and backtracking.
    4. 这里是为什么LALR优越于更加一般性的LR的关键
      All conflicts that arise in applying a LALR(1) parser to an unambiguous LR(1) grammar are reduce/reduce conflicts.
      怎么理解呢?这里暴露了我根本没有明白基本概念!
      LALR(1) = LA(1)LR(0) (1 token of lookahead, LR(0))
      原来这里LALR(1)1指的是lookahead1,而它的原型机还是LR(0)!
      那么LR(0)是什么意思呢?这里的定义
      To avoid backtracking or guessing, the LR parser is allowed to peek ahead at k lookahead input symbols before deciding how to parse earlier symbols.
      所以,我觉得我的混乱就来自于这里,我一直认为LR(1)的1指的是每次只读一个symbol/token,但是这里其实关于lookahead的概念我还是没有建立,还是模模糊糊。难道LR(0)指的是压根不读取任何token吗?显然不是应该是不用lookahead就能做reduce/shift的决策?那么这样要求语法是不是太严格了呢?
    5. 我的问题在于最基本的parsing table怎么创建都不知道!
      Algorithm to construct LL(1) Parsing Table:
      1. Step 1: First check for left recursion in the grammar, if there is left recursion in the grammar remove that and go to step 2.
      2. Step 2: Calculate First() and Follow() for all non-terminals.
        1. First(): If there is a variable, and from that variable, if we try to drivederive??? all the strings then the beginning Terminal Symbol is called the First.
        2. Follow(): What is the Terminal Symbol which follows a variable in the process of derivation.
      3. Step 3: For each production A –> α. (A tends to alpha)
        1. Find First(α) and for each terminal in First(α), make entry A –> α in the table.
        2. If First(α) contains ε (epsilon) as terminal than, find the Follow(A) and for each terminal in Follow(A), make entry A –> α in the table.
        3. If the First(α) contains ε and Follow(A) contains $ as terminal, then make entry A –> α in the table for the $.
          To construct the parsing table, we have two functions: 
           
      所以,核心是这么几条:
      1. 首先把left-recursion去掉换成包含ε的新的non-terminal
      2. 计算所有的non-terminal的First()Follow() set。这里没有任何可以投机取巧的办法,等于是你把这个语言的所有可能的集合都计算出来了,这个工作量是最大的部分。
      3. 最后是如何依据First()/Follow()来画表:行是non-terminal,列是terminal,因为表的entry是production,所以,算法肯定是你在计算First/Follow过程里就要填表否则你怎么直到是哪一个production产生的哪个terminal呢?除非你要建立production对应的?看来是吧否则就太复杂了。核心就是从哪个non-terminal能够通过哪个production得到哪个terminal。所以,parsing table就是这么简单。小技巧就是我们引入了ε之后那么凡是有它的non-terminal的Follow()就也被加入了它的First(),这个是很明显的,学校里肯定学过,我全都还给教授了。
    6. 但是关于去除left-recursion的算法我是一直糊涂的,因为在我看来引入一个新的non-terminal来去除left-recursion就是引入了ε,而很多语法是不高兴的,因为有conflict吧?但是看起来这个是必须的一步?
  8. 头疼死了,去健身房活动一下吧?我感觉这个方法还是很有效的就是即便看不懂那么抄一遍也能有个印象,一遍看不懂,抄上几遍说不定就领悟了。这个就是当年郭靖那种资质不高的人的学习武功的笨办法。

十一月三日 等待变化等待机会

  1. 这里的结论是说如果parsing table里的任何entry里有两个production rule,就意味着有conflict,那么LL(1)语法不能解决。实际上难道LR(1)语法不也是这样子的吗?只要parsing table有冲突不就是不符合了吗?总而言之计算LL(1)的算法就是计算First/Follow然后决定哪一个terminal由哪一个non-termiinal的哪一个production rule来决定。其中LL(1)不支持Left-Recursion,要实用ε。这就是昨天的总结。
  2. 另一个关于这幅图的意思就是说Top-Down-Parser(TDP)算法的核心问题就是不确定性,这个是由LL算法决定的,所谓的不确定性是相对于LR算法的线性worstcase来说的,因为TPD有可能是要靠猜的必然就有失误而回滚就是backtracking。而其中不需要backtracking的就要求语法是很严格的不含糊没有conflict,所以TPD没有backtrack就要把语法做好,理所当然的它的对应的语言就限制多了,范围小了。
  3. 这里是Recursive Descent的定义
    A recursive descent parser is a kind of top-down parser built from a set of mutually recursive procedures (or a non-recursive equivalent) where each such procedure implements one of the nonterminals of the grammar. Thus the structure of the resulting program closely mirrors that of the grammar it recognizes.
    其实,我本来不需要定义就能明白的,但是猛一读这个定义就糊涂了,一会儿说mutually-recursive,一会儿说non-recursive我就糊涂了。然后才明白这个还是英语理解的问题,你当然是后者更好,可是不一定做不到啊。并不是要追求这样子,现在才明白说GCC/clang都是recursive descent parser其实就是我们人类普通编程的习惯,说的高大上我就不懂了,原来就是一个小马甲而已的标签。
  4. 既生瑜兮何生亮?为什么有了First还需要Follow?这里说的太好了:其实我们需要的归根结底都是First,只不过在去掉了Left-Recusion引入了ε之后对于一些Non-Terminal的First要考虑它的Follow当它有ε的话。换言之,如果没有ε我感觉根本就不需要计算Follow,除非算法里要求实用Follow来决定何时Reduce?这个算法我还是没有掌握先放着吧。
  5. 这个是朴实而无用的基本概念:没有递归那么CFG产生的语言肯定是有限的,否则就是无限的。这个难道不证自明吗?我感觉很难证明。其实很容易使用反证法证明,就是假定一个语法是递归的而且是有限的,那么不妨定义语言的集合是固定长度的集合S,那么我们首先要排除无用的递归这类没有什么意义的语法比如:A=>A这个是无意义的递归。那么递归的那个non-terminal必然是某种形式的A=>BAC,这里我们认为一定可以把原来的语法转化为这种形式,因为根据递归的定义就是这个A必然在其后的derived的某个production里出现, 且慢,有没有可能A不出现在它自己的derived的后续中呢?比如
    
    S=>ABC
    A=>B
    B=>a
    C=>Aa
    
    不过这里所谓的递归就已经暗示了循环否则也不叫递归了,这个本身就是所谓的mutual recursive里才能叫递归。所以,纠结这个挺无聊的,因为递归基本上就是无限的。所以不证自明。
    那么我们不妨把这些合并并把A的前面的都代替为B,后面的代替为C,当然它们不能同时为ε。然后,下一次递归一定会产生一个新的字符因为它的长度是可以无限增长的,必定会超过假定的固定长度,因为B,C不能同时为ε啊。这个证明确实没有什么意义。
  6. 这个计算First的算法还是挺清晰的,应该人人都能想到,还是记录一下好:
    1. If x is a terminal, then FIRST(x) = { ‘x’ }
    2. If x-> Є, is a production rule, then add Є to FIRST(x).
    3. If X->Y1 Y2 Y3….Yn is a production, 
      1. FIRST(X) = FIRST(Y1)
      2. If FIRST(Y1) contains Є then FIRST(X) = { FIRST(Y1) – Є } U { FIRST(Y2) }
      3. If FIRST (Yi) contains Є for all i = 1 to n, then add Є to FIRST(X).
  7. 计算Follow比First复杂一些是因为它依赖后者:
    1. FOLLOW(S) = { $ } // where S is the starting Non-Terminal
    2. If A -> pBq is a production, where p, B and q are any grammar symbols, then everything in FIRST(q) except Є is in FOLLOW(B).
    3. If A->pB is a production, then everything in FOLLOW(A) is in FOLLOW(B).
    4. If A->pBq is a production and FIRST(q) contains Є, then FOLLOW(B) contains { FIRST(q) – Є } U FOLLOW(A)
  8. 这种小儿科的作业我也懒得理他,这个纯粹是入门级的C程序员才会做的,现代c++程序员需要使用现代化的工具。当然我还是要去想一下再说。留着以后娱乐吧?
  9. 这幅图还是比较有经典意义的,因为它是普遍的正确,当然它没有揭示其他这个类parser的彼此关系。 这个定义是显而易见的,关键是后面这句话:
    A general shift reduce parsing is LR parsing. The L stands for scanning the input from left to right and R stands for constructing a rightmost derivation in reverse.这个是这两天天天念叨的,耳朵都磨出茧子了。可是还是忘了!
    1. Many programming languages using some variations of an LR parser. It should be noted that C++ and Perl are exceptions to it.到底这么一个简单的问题我头脑始终不清楚,我已经老早就知道GCC/clang是Recursive Descent Parser,那不是明白说是TopDownParser吗?否则早就用bison/YACC来做了,这个不是我昨天就明白的吗?
    2. LR Parser can be implemented very efficiently.这个是当然的,因为使用状态机当然是最快的。
    3. Of all the Parsers that scan their symbols from left to right, LR Parsers detect syntactic errors, as soon as possible.最先我对于这个还是疑惑,明明要把所有的最右边的解析才行怎么比LL快呢?除非说的是worstcase,因为要是比bestcase,那么LL肯定是有更快的,因为它看到第一个就可以判断,当然啊,这个情况是否LR也能判断呢?
    这里有趣的是揭示了基本的算法:LLparser依赖的是First/Follow,而LRparser依赖这两个函数:Closure(), Goto(),这里我得到的提示是LR本质上似乎是一种DFA的搜索算法,但是令人迷惑的是LR的R是从最右边,我以为这个是因为Knuth论文说了创建表之后具体解析是从最右边,但是创建表是依照了从左到右的状态机按照DFA的原则来搜索,而这个搜索的路径的逆序正好是将来解析实际子串的顺序。Knuth的观察是相当的深刻的,大师对于计算机的栈的原理是有着深邃的认识。我感觉创建LR表的过程似乎就是LL的算法,总之我的脑子很混乱,解析语法和最终解析实际字符串的过程是时空的交叉了。脑子凌乱了。
  10. 真正要用脑子来学习的核心的核心是这个shift/reduce算法,可是我的脑子却混乱了。休息一下吧?饱餐战饭回过头来看其实很简单,压根不需要研究就能明白所有的状态机都要达到这样一个目的: 对于所有的状态它对于每一个可能的terminal/non-terminal都能够没有含糊的到达另一个状态。这个基本就是废话,如果transition不是唯一的那么是不是就不算状态机呢?状态是人为设定的,有没有可能增加虚假的状态达到这个目的呢?这个应该不是语法能够解决的,如果有歧义那就是语言的歧义,是状态机无法解决的。因为这个LR的parsing table 实际上就是搜索语法树的DFA的状态来的,只不过对于不同的transition我们给了一些美好的名字来形容它。对于transition的输入参数是terminal就是叫做action,而输入参数是non-terminal就就叫做goto,而很特别的是在这些action/goto的transition里如果涉及到一个non-terminal的结束就叫做Reduce,否则就是Shift。
  11. 现在回头来,我们知道深度搜索的一个大优点就是对于内存损耗的控制,所以,如果LR采用的是类似于这样的原理,理所当然的应该是有效率的,那么为什么反而在实际应用中不愿意采用呢?因为它的状态太多了。TopDown虽然有worstcase的耗用大量资源的问题,但是它靠猜测预测能够节省很多的开销?

十一月四日 等待变化等待机会

  1. 尽管这个免费的教程错误百出但是总的来说大错不错:我的理解就是有歧义的语法就是说同样的输入在LL和LR算法产生不同的parse tree。不可能说你使用同样的LL算法结果产生两个不同的tree,如果是那样的话就是conflict,你需要解决,解决的办法是或者修改语法或者用人为的办法解决conflict,比如总是选择冲突的一个选择,总之就是做到deterministic。所以,只有当同时使用LL和LR才有可能出现不同的树来判定问题出在语法上。本身语言不一定有歧义,或者有也可能使用语义规则来解决,这就是c++标准里各种各样的人为的规定吧?所以,语言有歧义的情况是理解的问题,语言本身的模糊性可以用语义来解决,应该不可能用语法来解决?算了这个太超前了。
  2. 能不能这么理解:因为LL的解析本质上是一个类似于贪心算法的深度搜索,因此要避免死循环所以不能有所谓的left-recursion。而LL(1)如果要避免选择困难就要去除common prefix。这里说LL(1)是没有递归的原因是什么呢?是因为它是deterministic的而且去除了left-recusion,所以,是不可能有递归了?这里是定义:
    A grammar is left-recursive if and only if there exists a nonterminal symbol A that can derive to a sentential form with itself as the leftmost symbol. Symbolically,
    
        A⇒+Aα,
    
    where⇒+ indicates the operation of making one or more substitutions, and α is any sequence of terminal and nonterminal symbols.
    这个和我的想法是一样的,就是说left-recursion就是有一个固定的prefix的terminal来触发递归,当然right-recursion就是一个固定的suffix了,这里的prefix/suffix都是ε就是所谓的direct-left/right-recursion。所以,回过头来说LL(1)是以没有递归的代价达到没有回滚的。这种语法相当的有限吧?因为按照没有递归语言就是有限的说法这个简直就不可能是计算机语言,这个结论似乎连我自己都不信,肯定是错的?
  3. 一篇好的帖子犹如一篇好的论文这是一篇巨大的帖子我要专门阅读总是指引你开拓视野:这个most-vexing parsing是我似曾相识的吧?我的确有印象这个问题,其实两者的核心都在于一个函数指针类型和变量初始化的混淆。比如wiki的例子是
    
    void f(double my_dbl) {
      int i(int(my_dbl));
    }
    
    这里的i到底是变量还是函数声明呢?而我遇到的是大师的提醒 这里更加的有隐蔽性,因为编译器这里并没有提醒你有vexing parsing的警告,你可能后来遇到编译错误的时候完全一头雾水。当然这里也是很简单的就是我以前说的要遵循现代c++初始化的规矩使用大括号{},比如:std::thread t(SayHello{});这样子就不会有歧义了。 我看到wiki的类似的解决方法很让我吃了一惊,居然有这么多的组合:
    
    //Any of the following work:
    TimeKeeper time_keeper(Timer{});
    TimeKeeper time_keeper{Timer()};
    TimeKeeper time_keeper{Timer{}};
    TimeKeeper time_keeper(     {});
    TimeKeeper time_keeper{     {}};
    
    毫无疑问我的问题是一模一样的有这么多的组合。
  4. 回过头来看这篇宏文,作者可以说是深入浅出,叙事条理清晰,论据充分翔实,是难得好文,值得收藏:
    1. 首先,提问者的问题是相当的好,我也看到了这个论断,但是我没有提问者的好奇心来质询,我想当然了。这个也是我这几天的困惑的根源,因为我从一开始就接受了wiki的论断:
      LL grammars, particularly LL(1) grammars, are of great practical interest, as parsers for these grammars are easy to construct, and many computer languages are designed to be LL(1) for this reason.
      这个是真的吗?
    2. 问题虽好,但是回答起来真的不简单,否则也不会是wiki的论断了。首先,作者的结论是否定的:
      Most computer languages are "technically" not LL because they are not even context-free. That would include languages which require identifiers to be declared (C, C++, C#, Java, etc.), as well as languages with preprocessors and/or macro facilities (C and C++, among others), languages with ambiguities which can only be resolved using semantic information (Perl would be the worst offender here, but C and C++ are also right up there).
      这么多的编程语言连context-free都不算,当然这个是因为有预处理和宏替换。
    3. 那么排除了预处理与宏替换这些的骨干的语言算不算呢?其他不说,单单c++的模板是绝对不可能的,因为它是一定需要语义分析才能完成解析的。这里提到的这个Van Wijngaarden grammar语法有些难理解,我只能模模糊糊的理解就是语法树的节点有属性?
    4. 这里道出了我的问题:
      In any case, a common parsing strategy is to recognize a superset of a language, and then reject non-conforming programs through a subsequent analysis of the resulting parse tree; in that case, the question becomes whether or not the superset is LL.
    5. 要回答以上的问题先要明确目的:
      The goal of parsing is to produce a syntax tree, not solely to recognize whether a text is valid or not.
      这个也不矛盾吧?创建的过程就是一个验证的过程,验证仅仅是最最基本的要求,当然对于编译器来说不是目的,但是对于某些工具,比如我向往的语言验证器就是目的,能不能不用语义分析就验证语法的正确性呢?这个显然是包含很多以上的歧义问题,但是最起码它达到了大部分的编程的目的,起码语法是可行的?这个可能吗?
      So it seems to be reasonable to insist that a proposed grammar actually represent the syntactic structure of the language.
      能做到吗?
    6. 作者给出了两个例子深入说明其间的问题,即便放宽语法规则的限制得到一个有错误的结果还是有很深层次的问题,这里我的理解就是操作符优先级的问题,这个看起来和括号不匹配似乎是同样的可以容忍超级语言,但是我的感觉是如果允许创建不同的语法树这个就整个改变了初衷了,因为之前说过LL或者是LR的结果是不同的树的话就暴露了语言的歧义,那么你得到这样的宽泛的结果有很么意义呢?所以这个superset of languange是缺少实际意义的。简单说就是如果你以为一个人说的英语听不懂是因为时态等等的小的语法错误,可是实际上他说的让人怀疑他是在说法语还是西班牙语,那么这个宽泛的结果就无实际讨论的意义了。
    7. 大侠说的这些我还是没怎么听到过:
      This point is important because an LL grammar cannot handle left-recursive productions, and you need a left-recursive production in order to correctly parse a left-associative operator.
      操作符优先级真的很重要吗?不过至少是导致了语法树的不确定性,让原本没有歧义的语言语法出现了歧义,所以,兹事体大。结论就是不能够依赖于LL来解决这个问题。所以,变成语言就不可能是LL(1)语法相符的?对吗?作者甚至提到一些强大的语言如Haskel, Prolog还可以自定义操作符优先级,那么这个不依赖于语义分析就根本不可能解决了。
    8. 作者最后问出了终极问题:如果我们撇开以上是否真的有编程语言是符合LL的语法要求的呢?
      In order to avoid cheating, though, we should really require that the LL parser have fixed lookahead and not require backtracking. If you allow arbitrary lookahead and backtracking, then you considerably expand the domain of parseable languages, but I contend that you are no longer in the realm of LL.
      所以,核心的问题就是你如果真的限制了lookahead的长度而且不允许回朔,那几乎就没有什么编程语言了。Java and Python were both designed to be LL(1) "pseudo-parseable".,这个是出乎我的预料的,原来java当初设计就是瞄准这个去的。据说C#还是需要回朔的。所以,结论是LL语法相容是java/Python的特权,c/c++等等是做不到的。
    看完这个巨帖我也累死了。休息吧?
  5. 最后补充一下这个经典问题:Is C++ context-free or context-sensitive? 我喜欢这个回复帖子的这个论断:

    Now, (ignorant) people (usually not language theorists, but parser designers) typically use "not context-free" in some of the following meanings

    • ambiguous
    • can't be parsed with Bison
    • not LL(k), LR(k), LALR(k) or whatever parser-defined language class they chose
    这个作者口气太不客气了,一杆子打死一船人。然而话糙理不糙,还是有道理的。作者的言辞是犀利的:

    First, you rightly observed there are no context sensitive rules in the grammar at the end of the C++ standard, so that grammar is context-free.

    的确我就没有想过这个问题啊,语法是没有任何反映出是context-sensitive的特征,当然牢牢记住标准自己都说了语法是说明性的当不得真的,没有人能够照着语法把编译器做出来,因为你还需要标准里大段大段的规定来解疑释惑去除歧义。
  6. 很多计算机语言比如c++也和人类语言相似的一点就是,语言本身是一个浩大的实体,人们希望用一些简洁的规则来描述它进而能够自动化的分析验证操作,然而这个和大多数语法一样的不可能,因为这么复杂的客观世界能够用几条简洁的规律来描述是不现实的,人类曾经以为牛顿三定律加上万有引力定律就能够描述万物运行的规律,结果后来才发现在微观高速的情况下就不适用了。语法也是一样,只能是一个多多少少宽泛的实际语言的超级集合,能够解决大多数的简单现象,而不得不添加一大堆的例外规定来删除符合语法不符合语义的部分,甚至于根本就解决不了的问题。计算机语言本身没有歧义,有歧义的只能是计算机语法。这个正如客观世界是精确的,不精确的只是数学本身,因为它是一个对客观世界的抽象。
  7. 让我感到有些意外的是LR(k)计算First/Follow集合之前并没有要求把left-recursion去除掉?这个难道是LL语法的独特要求吗?换言之,计算First/Follow其实并不在乎left-recursion而是LL的Left-most derivation的本质问题的要求?

十一月五日 等待变化等待机会

  1. 之前我的糊涂在于我没有意识到我打印的字符是unicode中的需要三个字节的字符,我一直以为这些符号应该是两个字符的,这个想法不知道是从哪里来的?
  2. 那么怎么看bison里的S/R,R/R conflict呢?这个是我最初的起因,直到这里才有些开窍,看来即便是明白了一些基本原理要解决实际问题还是有相当距离。通过实例来学习是最容易的办法。这个是实例的语法的bison输入文件:
    
    %token e b a
    %start S
    %%
    S:
            e B b A
            | e A
            ;
    A:
            a
            | %empty
            ;
    B:
            a
            | a D
            ;
    D:
            b
            ;
    %%
    
    那么使用bison强大的-Wcounterexamples的命令得到的结果是这样子的:
    
      First example: a • b b A
      Shift derivation
        S
        ↳ B         b A
          ↳ a D
              ↳ • b
      Second example: a • b
      Reduce derivation
        S
        ↳ B     b A
          ↳ a •   ↳ ε
    
    这里要怎么理解呢?如果不是这里的解说我是不可能明白的。这里的一个小细节折腾了我好久,就是输出-Wcounterexamples的开关和输出状态机的开关--report=all是不能并存的,只能一次一次得到!这里行为比较奇怪,前者不会产生report文件,只有后者才能看到状态机和反例。 我的运行结果和这个教程有些微区别也许是bison版本或者压根就没有说是bison说不定是yacc,总之我依赖我自己的结果重新梳理一遍:
    1. 这个是第一个反例说明语法期待shift:
      
            First example: a • b b A
            Shift derivation
              S
              ↳ B         b A
                ↳ a D
                    ↳ • b
      
      这里的输入最后的A让我很困惑,当前的在第一个a之后为什么结尾是一个A呢?这个顺序是怎么样的呢?如果拿掉这个A我也许还比较可能推断出是怎么回事:还是很费解,无法真正理解这里的冲突是怎么计算出来的???
    2. 这个是完整的输出文件
    06.11.2021 我还是不能理解这里的冲突是怎么回事,唯一给我的启示是如何修改的方法是彻底去除B,因为这个中间元素既没有递归也不是包含terminal的终点,也就是说它没有存在的必要。能不能说有些conflict的根源就是语法包含了不必要的non-terminal。比如本来可以是terminal的非要生出一个多余的步骤就有问题。原本B和D就可以合并为一个。但是我如果这么合并的话是不对的: B: a| a b;而把B在第一条rule里扩展我觉得这个教授这个样子的改法:
    
    S 	→ 	e a D b A
    S 	→ 	e a b A
    S 	→ 	e A
    A 	→ 	a
    A 	→ 	ε 
    D 	→ 	b
    
    和我的改法并没有区别,可是为什么我的还是有冲突?
    
    S 	→ 	e a D b A  
    S 	→ 	e A
    A 	→ 	a
    A 	→ 	ε 
    D 	→ 	b
    D 	→ 	ε 
    
    我后来发现只有这样改才可以
    
        1 S: e a D A
        2  | e A
        3 A: a
        4  | %empty
        5 D: b b
        6  | b
    
    这里的区别就在于我把D后面的terminal即b纳入了D的规则,原来看起来问题并不是ε是否出现在什么地方,而似乎是某个规则的后面的terminal和规则本身的terminal是否有重合有关系。比如规则D → b | ε 的情况,如果它后面是可以跟随同样的b的话就会有冲突,换言之,从规则D的角度来看它不知道自己要产生多少个b,它的那个规则可以产生1个或者0个b,这个能不能说是First(D)=Follow(D)造成的呢?我还是要去看这个LR parsing table产生的算法才行。
  3. 我怀疑我的电脑被入侵了,追查一个IP指向这个digitalocean.com也许都是表面现象,但是我的网络带宽全被它夺占了却是事实,而且似乎就是依赖与hotmail服务器有关的吧?也许是邮件?总之邮箱打不开而使用iftop/nethogs之类的可以看到发送给这个IP: 134.209.207.29

十一月六日 等待变化等待机会

  1. 准备花哪怕两天时间也要看懂LR parsing table的算法。
  2. 一个早上的结果就是语言本身模糊的情况是像对比较少的,大多数的冲突来自于语法的不合理。而语法相对应的不同的解析思路导致了有各自的模糊的可能性。这些都是老调重弹的废话。目前最最容易举例的语言的模糊性就是操作符在没有优先级的情况下的模糊问题,因为二元操作符的操作元是典型的同质的non-terminal,而对于LR还是LL的最右边还是最左边当然就会产生不同的parsing tree,这个就是语言本身的模糊问题。这里让我想起了以前说c/c++语法里并不用显性的定义操作符的优先级,因为在语法里通过定义不同操作符的操作元为不同的变量名就可以表达这个优先级。这里又是一个语法灵活性的例子。同样的语言可以有这么多种的描述的语法,而其中对错也不一定保证。
  3. 让我有些头晕的是LR是从最右边的non-terminal的derivation开始,但是实际parsing的时候正好是它的逆序,也就是说reduce non-terminal的时候是从最左边遇到的terminal开始的,是最左边开始。。。
  4. 阅读LR算法可以有另一个方式就是看bison的代码,看bison的代码可以有另一个方式就是看bison的文档。为什么一个简单的事情我要回避而采取这么往往饶的方式呢?可能是我当初看wiki里描述算法就感到吃力吧?正面进攻有时候并不轻松不如先炮火准备并两翼迂回,最后再正面攻坚,即便是对于一个防御看似不那么坚固的小据点也采取这样的方式,这是举轻若重的莫名其妙。在bison文档里说
    Any kind of sequence can be defined using either left recursion or right recursion, but you should always use left recursion, because it can parse a sequence of any number of elements with bounded stack space. Right recursion uses up space on the Bison stack in proportion to the number of elements in the sequence, because all the elements must be shifted onto the stack before the rule can be applied even once.
    不过这个仅仅是bison的偏好,不代表你的语法必须是这样子吗?不过。。。c++语法的bnf是说明性的并没有打算作为bison/yacc的输入文件。
  5. bison文档里举的经典的语法的例子要比之前的抽象的语法要容易理解,部分原因这个是所有c/c++程序员熟悉的语法模糊的问题:
    
    stmt:
            expr
            | if_stmt
            ;
    if_stmt:
            "if" expr "then" stmt
            | "if" expr "then" stmt "else" stmt
            ;
    expr:
            "identifier"
            ;
    
    这个经典的例子我曾经不止一次的看到各个大侠们用来回答R/S conflict的原因,而这个要结合bison给出的counterexample的解说似乎更清晰: 我现在似乎有强迫症因为bison的选项有color而我的输出没有,我就看源码似乎根本没有实现,但是这个是难以想象我就怀疑是配置,结果追查到也许是所谓的libtextstyle没有,这个似乎不是一个单独的库而是gettext里的某些功能,找不到明显的安装包,就去编译gettext源码发现这个基础库非常的严格,编译很不简单也许有太多的其他库依赖它吧?GNU里有这个所谓的libtextstyle,但是它不是一个库?只有文档? 看样子我的理解错了因为ubuntu的gettext就是那个可执行程序的工具包,并无法提供开发包,我还是去github下载源码来编译吧。 wow,花了这么多时间编译一个库?gettext的编译很有问题,其中的examples始终编译有问题,我最后放弃了因为我可以直接去编译libtextstyle库,
    
    ./configure --disable-option-checking '--prefix=/usr/local'  '--build=x86_64-pc-linux-gnu' '--host=x86_64-pc-linux-gnu' '--target=x86_64-pc-linux-gnu' '--disable-java' '--disable-pascal' 'build_alias=x86_64-pc-linux-gnu' 'host_alias=x86_64-pc-linux-gnu' 'target_alias=x86_64-pc-linux-gnu' --cache-file=/dev/null --srcdir=.
    
    安装后发现确实是很漂亮: 不过原版的颜色有些看起来不是很清楚我就改了一下颜色。
      Example: "if" expr "then" "if" expr "then" stmt  "else" stmt
      Shift derivation
        if_stmt
        "if" expr "then" stmt
        if_stmt
        "if" expr "then" stmt "else" stmt
      Example: "if" expr "then" "if" expr "then" stmt  "else" stmt
      Reduce derivation
        if_stmt
        "if" expr "then" stmt                        "else" stmt
        if_stmt
        "if" expr "then" stmt
    
    我本来想要使用什么工具来拷贝console上的输出,后来发现有一个输出为html的选择,实在是太棒了。 那么回到最初的问题,到底冲突是什么意思呢?对于这个输入的这个部分"if" expr "then" stmt "else" stmt面临两个选择,如果看到下一个lookahead的terminal是"else" 如果使用shift,那么会把整个if-then-else作为前面的if-then的stmt。但是如果reduce,这个是按照"if" expr "then" stmt把这个non-terminal的 stmt缩减结果就是把当前的这个if-then-stmt变为最开始的if-then-stmt-else-stmt的then后的stmt。我觉得这个是每一个编程的都明白的道理,只不过我现在不明白的是bison是怎么能够计算出这样的冲突。这个要看bison的代码或者看wiki上的算法才能理解。
  6. 最近感到台海风云变幻想起了前两天的偶得

    有感台海局势

    塞北深秋枫叶红, 海峡遥望景不同。 台岛井蛙不知海, 曲士冰雪辨夏虫。

    《庄子集释》卷六下《外篇·秋水》北海若曰:井蛙不可以语于海者,拘于虚也;夏虫不可以语于冰者,笃于时也;曲士不可以语于道者,束于教也。

    意思是:你不要跟夏天的虫子谈冰,它不懂;不要跟井底之蛙谈大海,它没见过不懂;不要跟凡夫谈高深的道的学问,他不懂。


十一月七日 等待变化等待机会

  1. LR(1)的算法是建立在LR(0)上的,所以,今天决定先解决它,这个本身是极其复杂的一个算法,因为其中的依赖的概念和定义就有很多:
    1. 首先,什么是Items,说白了它是原本的rule加上一个当前处理位置的点dot的一个状态吧?换言之,就是RHS上带一个光标。而对于ε的处理是特殊的做法
      Rules of the form A → ε have only a single item A.
    2. 什么是Item sets呢?当然顾名思义就是item的集合。但是这里要对于item的计算要特别处理光标在non-terminal之前必须要扩展non-terminal,相当于计算Item set集合的时候要包含这些non-terminal的所有rule里光标处于开始位置的items。所以,计算itemset要包含这个扩展的过程。
    3. 这个closure of item sets实际上在我看来就是递归反复计算expansion non-terminal的item set的穷尽过程。如果expansion-non-terminal是一个函数的话, closure这个函数就是递归调用它的过程。归根结底是用closure来计算最终的item sets,当然这里需要不断的移动光标计算下一个closure。
    4. 为什么需要augmented grammar呢?这个大家谁都明白的,上学的时候就讲过,因为我们设置的开始状态也许有多条rule导致我们无法统一可接受的状态是哪一个,所以等于是人工添加了一个虚拟唯一的起始状态来调用我们自己规定的起始状态并且设定它的后边是输入结束标志符EOF。而且我们把这个添加的规则标号为特殊的0号以便容易识别(0) S → E eof这个在bison里的形式就是0 $accept: S $end其中S是你指定的%start S
    5. 以上都是为了创建LR(0) parsing table所作的预备工作,真正的算法还是相当复杂的: 第一步实际上还是从我们人为创建的起始状态来计算所有的item sets,我们把光标放在这个0号规则的右边开始位置来一步步扩展并移动光标来得到所有的item set的closure。
    6. 但是实际上计算closure过程也是要明确item set之间转换的过程。这里的parsingtable的行是所有计算得到的itemset的标号,列是所有光标移动经过的元素,当然就是我们可能涉及到的所有的non-terminal和terminal,那么对于每一个状态我们通过移动光标到下一个状态,如果经过的是terminal的情况称之为shift,而对于是non-terminal的则称之为reduce,之所以叫做reduce实际上因为我们反过来做的就是把将来parser遇到的实际的terminal缩减为non-terminal的逆过程。当然表的entry就是对应到达下一个状态的标号。实际的创建表的过程还有一个额外的信息要计算,就是要把我们之前使用的rule的标号作为所谓的reduce的标号,当然这个是除了我们自己添加的0号以外因为这个实际是我们最终目标状态。
    7. 那么最核心的问题是发生冲突怎么办?这里给出的一个最简单的R/R conflict的问题还是把我蒙了一下子,因为我使用bison没有爆出冲突,后来才明白原来这个是LR(0)的Reduce/Reduce conflict是可以使用LR(1)靠添加一个Lookahead来解决的,但是对于那个R/S conflict却是无法依靠LR(1)来解决的。这种冲突发生的机理非常的深奥不是什么简单规则就能够判断的。比如我猜想也许那个LR(0)的R/R是因为两个规则的前缀是相同的?这个只是猜测估计不对。总之,单单LR(0)的算法就已经很复杂了,LR(1)其实更加的复杂,虽然据说状态被缩减合并了,可是我今天已经没有能力再看了。仿佛进攻的炮火准备已经把炮弹打光了。说老实话,看这个算法并不是特别难,但是要和bison来对照还是有困难,尤其是我注意到那个结尾的bison的标志符[$end]有时候出现有时候不出现,而且加上[]是什么意思呢?看过几行bison的代码它内部使用bitsets来代表状态机,如果不配合输出来看的话很头疼。
  2. 本来预计今天对于LR(1)发起总攻可是昨天为了编译gettext得到bison需要的libtextstyle耗费了很多时间耽误了大半天。这个彷佛是攻城前肃清外围据点的行动遇到了难啃的硬骨头实际上并不是攻城所必须要解决的,但是如果不拔掉又犹如芒刺在背一样的不舒服,不得已而为之。

十一月八日 等待变化等待机会

  1. 首先,要明确一个概念LR(k)是可以通过所谓的back substitution转换为LR(1)的,这就说明了后者的重要性,因为LR(k)是强大到可以识别所有的deterministic context-free language的。
    A canonical LR parser or LR(1) parser is an LR(k) parser for k=1, i.e. with a single lookahead terminal. The special attribute of this parser is that any LR(k) grammar with k>1 can be transformed into an LR(1) grammar. LR(k) can handle all deterministic context-free languages.
    首先不知道这个断言的出处与正确性,权且相信,但是要明确的不是parser本身可以做到,也就是说不要以为你号称是一个LR(1)的parser就能打遍天下无敌手,不,是任何LR(k)的语法可以转换为LR(1)的语法!所以,这个工作是很不容易的,需要你去处理bison的种种的冲突!此外,我还没有看到什么是back-substitution是怎么样子的,也许就是一个修改语法的过程吧?而所谓的所有的Deterministic Context-Free Language(DCFL)是一个所有CFL的子集,换言之就是存在一个对应的Deterministic Push-Down Automaton(DPDA)如果能够对于任何一个状态和任何一种输入和任何一个栈里的字符串的组合有唯一性且确定拥有。这个对应的定义很铉酷可以拿来忽悠人,虽然我看不懂:
    A (not necessarily deterministic) PDA M can be defined as a 7-tuple:

    M = (Q, Σ, Γ, q0, Z0, A, δ);

    where
    • Q is a finite set of states
    • Σ is a finite set of input symbols
    • Γ is a finite set of stack symbols
    • q0 ∈ Q is the start state
    • Z0 ∈ Γ is the starting stack symbol
    • A ⊆ Q, where A is the set of accepting states
    • δ is a transition function, where

      δ: (Q x (Σ ∪ {ε}) x Γ) --> P(Q x Γ*)

      where * is the Kleene star, meaning that * is "the set of all finite strings (including the empty string ε) of elements of Γ", ε denotes the empty string, and P ( X ) is the power set of a set X.

    M is deterministic if it satisfies both the following conditions:

    • For any q ∈ Q, a ∈ Σ ∪ {ε}, x ∈ Γ, the set δ(q, a, x) has at most one element;
    • For any q ∈ Q, x ∈ Γ, if δ(q, ε, x) ≠ ∅ then δ(q, a, x) = ∅ for every a ∈ Σ;

    我看不懂公式,但是我抄写一遍花的时间比理解还要多。两个判断标准的最后一个判断我无法理解。
  2. 我厚颜无耻的拷贝这个表因为写数学公式很有用。这个网站是有很多有用的工具的,我就顺便搂了一些子符表。包括它的链接验证工具。
  3. 这里说的状态机里是说从一个状态A到另一个状态B本身就是一种状态吗?就是说一个超级状态机或者说是一个描述状态机的状态机?因为wiki批注说那个描述无法理解确实是有道理的,因为作者没有解释的很清楚。结合刚才抄写PDPA的定义就可以看出来,因为输入symbol本身也是状态机组成元素之一,如果状态机本身的元素
    For any q ∈ Q, a ∈ Σ ∪ {ε}, x ∈ Γ, the set δ(q, a, x) has at most one element;
    做不到的话,我们就加大栈里的字符长度a,所以,不过这里我想不清楚到底是输入还是栈里的长度?总而言之和这个有关系。
  4. 秋去冬来有诗为证

    云涨云消云卷云舒望不停, 风吹落叶满地金。 秋去冬来日渐短, 吟风月慰我心。
    我一时好奇google要如何把这首小诗翻译成英文呢?比我想像的好太多了,相当的不错!
    
    The cloud rises and the cloud disappears,
    The wind blows the fallen leaves all over the ground.
    Fall to winter is getting shorter,
    Yin Feng comforted my heart to the moon.
    
  5. LR(1)建立parsing table的过程和LR(0)是类似的,这里我注意到了在LL算法里往往为了避免死循环而先把left-recursion改成ε的语法。而这里是不需要的因为我们在计算item set的时候就是要看它是否已经被添加过了,这里的递归可以立刻被发现而终止:
    [T →  T + n]
    因为这里扩展T的时候立即就发现它已经添加过了。这个是相比LL的一个优势。
  6. 但是LR(1)毕竟比LL(1)算法要复杂的多因为前者依旧要计算First/Follow set而且这里不是仅仅的non-terminal,而是整个itemset的First/Follow也就是说它是要计算整个closure了,这个计算量要比LL多得多了。
  7. 半梦半醒之间偶得

    金风飒爽满人间, 一片红叶挂天边。 红似晚霞艳如火, 恰似仙子舞翩跹。
  8. 午饭后去公园偶得

    白云蓝天映眼帘, 落叶满地近身前。 暖阳熏得人欲睡, 坐看风起似等闲。

十一月九日 等待变化等待机会

  1. 我感觉要理解R/R conflict只能去实际作一次建立parsingtable的过程。
    1. 对于语法
      
          (1) E → 1 E
          (2) E → 1
      
    2. 我们添加augmented rule 0:
      
      	(0) S → E eof
      
    3. from start rule 0, we place dot at beginning of RHS of rule and get item set 0
      
      	S → • E eof
      	+ E → • 1 E
      	+ E → • 1
      
    4. for item set 0 move dot over terminal 1, we get item set 1
      
          E → 1 • E
          E → 1 •
          + E → • 1 E
          + E → • 1
      
    5. for item set 0 move dot over non-terminal E, we get item set 2
      
      	S →  E • eof
      
    6. for item set 1 move dot over terminal 1, we get identical item set 1
      
          E → 1 • E
          E → 1 •
          + E → • 1 E
          + E → • 1
      
    7. for item set 1 move dot over non-terminal E, we get item set 3
      
      	E → 1  E •
      
    8. 现在建立transition table,就是描述上述建立的关系的表
      Item Set detail '1' E
      0
      
      S → • E eof
      + E → • 1 E
      + E → • 1
      
      1 2
      1
      
      E → 1 • E
      E → 1 •
      + E → • 1 E
      + E → • 1
      
      1 3
      2
      
      S →  E • eof
      
      3
      
      E → 1  E •
      
    9. 现在我们把transition table里的non-terminal列copy得到goto table感觉这个表没有什么用???
      Item Set detail E
      0
      
      S → • E eof
      + E → • 1 E
      + E → • 1
      
      2
      1
      
      E → 1 • E
      E → 1 •
      + E → • 1 E
      + E → • 1
      
      3
      2
      
      S →  E • eof
      
      3
      
      E → 1  E •
      
    10. 把transition table里的terminal列copy得到action table,而其中的action是shift,然后把任何item set里包含一个这样标号为m形式为A → w •的rule,而且m>0的话,填充action为rm
      Item Set detail '1'
      0
      
      S → • E eof
      + E → • 1 E
      + E → • 1
      
      s1
      1
      
      E → 1 • E
      E → 1 • satisfy r2
      + E → • 1 E
      + E → • 1
      
      s1 conflict with r2
      2
      
      S →  E • eof satisfy r0
      
      r0
      3
      
      E → 1  E • satisfy r1
      
      r1
    11. 总结一下,就是发现冲突是在创建action table的过程中,我们建立transition table的目的是找到所有的shift-action,也就是当我们的光标指定的symbol是terminal的时候,那么我们transition的目的地item set标号成为我们未来的action table里的shift代码。而接下来我们需要把所有的空格部分填充为reduce的标号,这个是依赖于发现当前的item set里有任何一个rule的光标处于它的最右边的情况,这个item set里如果有超过一个rule有这个情况的话,我们发现了reduce/reduce conflict,如果这个item set原本就有transition table里的shift标号,那么我们就遇到了recude/shift conflict
  2. 以上对于理解shift-reduce conflict似乎还是有帮助的吧?那么对于reduce/reduce conflict我还是需要一个实际的例子来理解:
    1. 语法如下:
      
          (1) E → A 1
          (2) E → B 2
          (3) A → 1
          (4) B → 1
      
    2. 添加augmented rule 0如下:
      
         (0) S → E eof
      
    3. 下面是我直接建立transition table
      Item Set detail '1' '2' E A B
      0
      
      S → • E eof
      + E → • A 1
      + E → • B 2
      + A → • 1
      + B → • 1
      
      s1 2 3 4
      1
      
      A → 1  • satisfy r3
      B → 1  • satisfy r4
      
      r3 conflicts with r4 r3 conflicts with r4
      2
      
      S → E • eof  
      
      acc acc
      3
      
      E → A • 1
      
      s5
      4
      
      E → B • 2
      
      s6
      5
      
      E → A 1 • satisfy r1
      
      r1 r1
      6
      
      E → B 2 • satisfy r2
      
      r2 r2
  3. 为什么在第一个R/S conflict无法使用lookahead的LR(1)里解决呢?
    
    E → 1 • E
    E → 1 • satisfy r2
    + E → • 1 E
    + E → • 1
    
    而第二个R/R conflict可以在LR(1)里解决呢?
    
    A → 1  • satisfy r3
    B → 1  • satisfy r4
    
    看来我只有明白了LR(1)的lookahead的计算方法才能体会。但是早上单单计算这两个LR(0)的表就耗尽了我的精力,这个工作真的只适合程序来做,对于这种最最简单的语法已经让人头疼了。
  4. 现在回过头来来看LR(1)才意识到有些算法的细节是之前那篇wiki里写LR(0)写的不好的,不单单是别人已经提过他有些说法令人难以理解,而且是明显的非编程实现的人的通病。比如最明显的处理就是关于最后再把EOF这个加到action table里就是问题。正确的处理是增加一个acc状态也就是说移动光标越过eof之后的状态,甚至还可以增加一个通用的reject状态来接受所有非法输入,这个好处是可以自动把eof添加到所有的action table。这个是我的感觉,我认为我之前有一些错误的认识,现在看这个计算LR(1)的First/Follow才明白:
    1. 原本的针对每一个non-terminal的First/Follow set是不是针对某一个rule的,这个显然在LL里面是粗糙的,在LR(1)里是细分到不仅仅是non-terminal还是rule的函数,比如Follow要两个参数一个item set,一个是rule。
    2. 所以,我认为LR(1)或者说canonical LR(k)算法是充分利用了每一个non-terminal在每一个状态的每一个rule的可能的Follow symbol,那么这个当然是减小了很多的模糊性。如果说LL算法是很粗略的贪心式的DFS搜索,那么LR(1)是预先做了剪枝处理,也就是在每一个搜索的节点不是brute-force的尝试每一个选项,而是根据每一个non-terminal在每一个item set里的每一个rule的可能的Follow set。
    3. 但是不亲自去计算一个LR(1)的parsing table,我觉得很难体会为什么有些在LR(0)语法的R/S conflict没有办法依靠lookahead来消除,而之前那个R/R conflict却消失了。这里面还是很玄奥的。

十一月十日 等待变化等待机会

  1. 实践是最好的试金石,因为你以为你明白了,实际一动手却意识到完全不是那么回事,这里我感觉我对于lookahead的理解完全不对。我之所以放弃wiki的例子是因为那个例子说明让人感觉有什么链条掉了,我根本不能明白是怎么计算的。大侠说他在Stanford教授编译器课程相关的讲义可以详细解读,我感觉他的例子依然不能解决我的疑惑,不如看他的slides。我想说的是,实际上这样的事情比比皆是,比如传统武术练起来虎虎生风威风凛凛,但是一到拳台上和一些注重格斗对抗技战术的技艺对决就立刻原形毕露,为什么?原因中国武术很多不是为了对抗而存在的,而且避免对抗可以长久的隐藏缺点提高了生存度。中美长期的避免对抗也是一种提高生存的方式,毕竟真刀真枪的直接对抗立刻就暴露了优缺点,那么这种消极避战对谁更有利?是弱势一方吧?这个似乎是颠扑不破的真理。保存教授的这篇讲义,看来非常的专业!
  2. 其实,我觉得我领悟了一点就是这个lookahead实际上就是下一步的状态的Follow。
  3. 理解最原始的reduce/shift我觉得很重要,否则我们就是无本之木。
    Reduce:
    If we can find a rule A –> w , and if the contents of the stack are qw for some q ( q may be empty), then we can reduce the stack to qA . We are applying the production for the nonterminal A backwards.
    Handle:
    The w being reduced is referred to as a handle. Formally, a handle of a right sentential form u is a production A –> w , and a position within u where the string w may be found and replaced by A to produce the previous right­ sentential form in a rightmost derivation of u . Recognizing valid handles is the difficult part of shift­-reduce parsing.
    Recognize:
    There is also one special case: reducing the entire contents of the stack to the start symbol with no remaining input means we have recognized the input as a valid sentence. This is the last step in a successful parse.
    Shift:
    If it is impossible to perform a reduction and there are tokens remaining in the undigested input, then we transfer a token from the input onto the stack. This is called a shift.
    Error:
    If neither of the two above cases apply, we have an error. If the sequence on the stack does not match the right­hand side of any production, we cannot reduce. And if shifting the next input token would create a sequence on the stack that cannot eventually be reduced to the start symbol, a shift action would be futile. Thus, we have hit a dead end where the next token conclusively determines the input cannot form a valid sentence.
    只有读了这些才能真正体会到为什么Knuth当初提出的这个算法的流行,因为它正好是栈的逆序,所以,我们不停的压栈而每次都是在栈顶来判断是否有reduce的可能,这个就是rightmost derivation的顺序!从这也可以体会为什么是Push-Down-Automaton,因为它使用的是栈。而且似乎总是偏好最长的match而不是greedy的贪心式?这一点我不确定,可能就是语法的设计要达到这个目的吧?应该是偏好最短的可以match的贪心式。
  4. 重复这个结论性的论断是有意义的:
    LR Parsing
    LR parsers ("L" for left to right scan of input, "R" for rightmost derivation) are efficient, table-­driven shift-­reduce parsers. The class of grammars that can be parsed using LR methods is a proper superset of the class of grammars that can be parsed with predictive LL parsers. In fact, virtually all programming language constructs for which CFGs can be written can be parsed with LR techniques. As an added advantage, there is no need for lots of grammar rearrangement to make it acceptable for LR parsing the way that LL parsing requires.
  5. 这个是提挈领
    We begin by tracing how an LR parser works. Determining the handle to reduce in a sentential form depends on the sequence of tokens on the stack, not only the topmost ones that are to be reduced, but the context at which we are in the parse. Rather than reading and shifting tokens onto a stack, an LR parser pushes "states" onto the stack; these states describe what is on the stack so far. Think of each state as encoding the current left context. The state on top of the stack possibly augmented by peeking at a lookahead token enables us to figure out whether we have a handle to reduce, or whether we need to shift a new state on top of the stack for the next input token.
  6. 这个定义才是真正的定义:
    An LR parser uses two tables:
    1. The action table Action[s,a] tells the parser what to do when the state on top of the stack is s and terminal a is the next input token. The possible actions are to shift a state onto the stack, to reduce the handle on top of the stack, to accept the input, or to report an error.
    2. The goto table Goto[s,X] indicates the new state to place on top of the stack after a reduction of the nonterminal X while state s is on top of the stack.
    The two tables are usually combined, with the action table specifying entries for terminals, and the goto table specifying entries for nonterminals.
    这里也终于解释了为什么我们需要Goto table,之前我认为没有什么用,现在才明白Action table,告诉你怎么去做shift/reduce,但是并不能告诉你然后的状态是什么。这一点如果不实际写程序是体会不到的,因为在很多算法描述中没有提到具体的栈的状态。甚至于没有提到具体的handle是怎样的。不过我对于两个表合在一起还是有些不解,当然从制作表的过程看似乎是这样子的。前者的列就是terminal,后者的列是non-terminal,可是。。。
  7. 仿佛回答我的问题,教授给出了最清晰的算法,这个才是回答疑问的方式:
    LR Parser Tracing
    We start with the initial state s0 on the stack. The next input token is the terminal a and the current state is st . The action of the parser is as follows:
    • If Action[st ,a] is shift, we push the specified state onto the stack. We then call yylex() to get the next token a from the input.
    • If Action[st ,a] is reduce Y –> X1 ...Xk then we pop k states off the stack (one for each symbol in the right side of the production) leaving state su on top. Goto[su ,Y] gives a new state sV to push on the stack. The input token is still a (i.e., the input remains unchanged).
    • If Action[st ,a] is accept then the parse is successful and we are done.
    • If Action[st ,a] is error (the table location is blank) then we have a syntax error. With the current top of stack and next input we can never arrive at a sentential form with a handle to reduce.
  8. 读到这里我也彻底清楚了这个还是LR(0)的表的建立过程,所以,虽然我的问题依然存在,但是总算有了一个更加清晰的概念,很多是算法的细节,尤其是表和栈的关系。表就是函数的输入输出,而栈是记录当前需要处理的和handle相关的symbol部分以及item setitem sets等等。看样子要看到LR(1)还要继续。
  9. 分类很重要,但是这个说法和我之前的理解有些微的差别,看样子我的理解有偏差。
    LR Parser Types
    There are three main types of LR parsers: LR(k), simple LR(k), and lookahead LR(k) (abbreviated to LR(k), SLR(k), LALR(k)). The k identifies the number of tokens of lookahead. We will usually only concern ourselves with 0 or 1 tokens of lookahead, but the techniques do generalize to k > 1. The different classes of parsers all operate the same way (as shown above, being driven by their action and goto tables), but they differ in how their action and goto tables are constructed, and the size of those tables.
    正因为LR(0)根本没有lookahead,我根本不知道这个lookahead是存在哪个数据结构里的,难道是item set里要包含这个token?换言之,LR(0)和LR(1)的item set根本就是不兼容的,差别很大的!所以,不仅仅是表的大小,当然表的大小的直接原因是item set数量来决定的。是的,教授说表立刻增大很多很多。
    The full LR(1) parsing table for a typical programming language has many thousands of states compared to the few hundred needed for LR(0).
    是一个或者几个?数量级的增加!LALR(1) is the method used by the bison parser generator.这个信息是很重要的!
  10. 这里是画龙点睛的经典透视之言,非常的切中要害!
    The essence of LR parsing is identifying a handle on the top of the stack that can be reduced. Recognizing a handle is actually easier than predicting a production was in top­down parsing. The weakness of LL(k) parsing techniques is that they must be able to predict which product to use, having seen only k symbols of the right­hand side. For LL(1), this means just one symbol has to tell all. In contrast, for an LR(k) grammar is able to postpone the decision until it has seen tokens corresponding to the entire right­ hand side (plus k more tokens of lookahead). This doesn’t mean the task is trivial. More than one production may have the same right­hand side and what looks like a right­ hand side may not really be because of its context. But in general, the fact that we see the entire right side before we have to commit to a production is a useful advantage.
    字字珠玑啊!

十一月十一日 等待变化等待机会

  1. 这个是第一次我看到关于closure的解释,本来这是一个数学名词,我也没有真的去理解,现在看来不是数学的意义。
    
    		A –> X•YZ
    		Y –> •u
    		Y –> •w
    
    At the above point in parsing, we have just recognized an X and expect the upcoming input to contain a sequence derivable from YZ . Examining the expansions for Y , we furthermore expect the sequence to be derivable from either u or w . We can put these three items into a set and call it a configurating set of the LR parser. The action of adding equivalent configurations to create a configurating set is called closure. Our parsing tables will have one state corresponding to each configurating set.
    那么这就是所谓的configurating set的概念,而计算它的过程就是closure,说到底,它就是代表了所谓的上下文,而令人感兴趣的是这种语言的本来定义是context-free,也就是说上下文无关的语言里是有上下文相关的解析参数的。所谓的状态就是上下文相关的解析条件的映射。这个是理解item set或者说状态机的状态的关键,当然这里还没有lookahead的角色,但是它只不过是这个的延伸,成为状态的一部分。
  2. 状态机最大的威力在于它穷尽了所有的可能的路径: Each state must contain all the items corresponding to each of the possible paths that are concurrently being explored at that point in the parse.
  3. 之前我不理解Goto table的用途就是没有实际编程的概念,因为我们使用的是栈来记录当前的上下文所以: Each time we perform a shift we are following a transition to a new state.而这里状态是记录在哪里的呢?是记录在栈顶的! 大师的描述是这么的翔实精确我没法不引用
    Recall that we push states onto the stack in a LR parser. These states describe what is on the stack so far. The state on top of the stack (potentially combined with some lookahead) enables us to figure out whether we have a handle to reduce, or whether we need to read the next input token and shift a new state on top of the stack. We shift until we reach a state where the dot is at the end of a production, at which point we reduce. This finite automaton is the basis for a LR parser: each time we perform a shift we are following a transition to a new state.
  4. 这里给出了非常的精确的计算closure的算法,堪称经典,滴水不漏的描述:
    In summary, to create a configurating set for the starting configuration A –> •u , we follow the closure operation:
    1. A –> •u is in the configurating set
    2. If u begins with a terminal, we are done with this production
    3. If u begins with a nonterminal B , add all productions with B on the left side, with the dot at the start of the right side: B –> •v
    4. Repeat steps 2 and 3 for any productions added in step 3. Continue until you reach a fixed point.
  5. 之前我认为Goto table是只适用于terminal的,不知道哪里来的概念,实际上这个transition是适用于所有的语法symbol的C' = successor(C,X),这里的函数successor就是transition table从一个状态C在输入为一个语法symbol为X的时候需要转到的状态C',但是这个公式只有理论意义,为什么实际上没有完全这样子的呢?因为在action table里我们的输入是不同的,对于shift你可以说X就是读入的token,可是对于reduce我们需要先把栈里的symbol做reduce才能得到输入参数X,这里是一个non-terminal,所以,这个通用函数并不是那么的通用,还不如分成reduce/shift来的好。不言自明的是这个函数的计算也就是在创建所有的状态表的过程里得到的。
  6. 我们为什么需要augmented rule呢?没有比这个解释的更清楚的了:
    At the highest level, we want to start with a configuration with a dot before the start symbol and move to a configuration with a dot after the start symbol. This represents shifting and reducing an entire sentence of the grammar. To do this, we need the start symbol to appear on the right side of a production.
    就是因为我们需要把start symbol完全shift/reduce来达到接受状态,所以,必须把它放在语法rule的右边让光标移动到结尾才能做到,于是就有了额外的0规则。
  7. 这里给出的画表格的正确方法。 给定这样的语法:
    1. E' –> E
    2. E –> E + T
    3. E –> T
    4. T –> (E)
    5. T –> id
    那么如何画呢?
    Configurating SetSuccessor
    Item SetRule with Dot
    I0E' –> •ES1
    E –> •E+T S1
    E –> •TS2
    T –> •(E)S3
    T –> •idS4
    I1E' –> E• AcceptAdded by augmented rule 0
    E –> E•+T S5
    I2E –> T•R2
    I3T –> (•E) S6
    E –> •E+T S6
    E –> •T S6
    T –> •(E) S3
    T –> •id S2
    I4T –> id•R4
    I5E –> E+•TS8
    T –> •(E)S3
    T –> •idS4
    I6T –> (E•)S7
    E –> E•+TS5
    I7T –> (E)•R3
    I8E –> E+T•R1
    这个就是LR(0)算法的正确的表格制作方法。大师给出了一个制作精美的图来形象化这个transition table,我不知道这个是不是手工制作的,应该是有工具吧?不过这样子的工具一定很不容易做吧?13.11.2021现在知道这个是使用bison的开关-g制作的.dot文件,但是形象化输出为图像需要xdot之类的工具。总之大师的辛苦制作应该值得保留。
  8. 回忆一下怎么使用这个LR首先要明白怎么制作,这个是制造工具和使用工具的关系。我一直以为我已经有抄录了这个LR(0)表格的制作算法,但是笔记里找不到,只好再抄一遍加深印象
    To construct the LR(0) table, we use the following algorithm. The input is an augmented grammar G' and the output is the action/goto tables:
    1. Construct F = { I0 , I1 , ... In }, the collection of configurating sets for G'.
    2. State i is determined from Ii . The parsing actions for the state are determined as follows:
      1. If A –> u• is in Ii then set Action[i,a] to reduce A –> u for all input. ( A not equal to S' ).
      2. If S' –> S• is in Ii then set Action[i,$] to accept.
      3. If A –> u•av is in Ii and successor( Ii , a) = Ij , then set Action[i,a] to shift j ( a is a terminal).
    3. The goto transitions for state i are constructed for all nonterminals A using the rule: If successor( Ii, A) = Ij, then Goto [i, A] = j .
    4. All entries not defined by rules 2 and 3 are errors.
    5. The initial state is the one constructed from the configurating set containing S' → •S.

    Notice how the shifts in the action table and the goto table are just transitions to new states. The reductions are where we have a handle on the stack that we pop off and replace with the nonterminal for the handle; this occurs in the states where the • is at the end of a production.

  9. 大师的讲义写的非常的翔实,尤其是一步一步的描述parsing过程的栈的情况,这样子的详细准确的记述是少有的,因为大多数明白人都懒得写这些枯燥的步骤,毕竟用嘴来讲课就容易多了。这个讲义如果是出版为书大概没有人愿意买,因为很多人都不屑于听一遍就懂的,而且除非你要写程序作业大多数人估计也不会认真研读,因为你去读是因为你遇到了困难才去回过头来读,所以,可惜了,这样好的文本只能作为学生做作业的指导。
  10. 现在看LR(0)的语法其实很简单:我添加了一个例外就是Accept状态。
    To be precise, a grammar is LR(0) if the following two conditions hold:
    1. For any configurating set containing the item A –> u•xv there is no complete item B –> w• in that set except B is starting symbol. In the tables, this translates to no shift-­reduce conflict on any state. This means the successor function from that set either shifts to a new state or reduces, but not both.
    2. There is at most one complete item A –> u• in each configurating set. This translates to no reduce-­reduce conflict on any state. The successor function has at most one reduction.
    说起来容易,可是如果你不做这个表你能一眼看出来吗?语法有什么特征可以看出来呢?有什么办法防止呢?这些都是超出现实的要求,还是耐心看LR(1)的解决办法吧。
    Very few grammars meet the requirements to be LR(0). For example, any grammar with an ε rule will be problematic. If the grammar contains the production A –> ε, then the item A –> •ε will create a shift-­reduce conflict if there is any other non­-null production for A . ε rules are fairly common programming language grammars, for example, for optional features such as type qualifiers or variable declarations.
    至少我们直到凡是有ε混合的语法规则都是有Shift-Reduce conflict的。这个打死了一船人啊。
  11. 这个才是我需要的因为LR(0)至少我已经是熟悉了的,所以,SLR(1)是出奇的简单,它就是在原来的LR(0)的规则2.a)改了一点点:
    If A –> u• is in Ii then set Action[i,a] to reduce A –> u for all input a in Follow(A). ( A not equal to S' ).
    这个讲义写的深入浅出,娓娓道来,文笔也很流畅,值得收藏。 就是说SLR(1)语法也是有前提条件的,而且看样子很严格的:
    In the SLR(1) parser, it is allowable for there to be both shift and reduce items in the same state as well as multiple reduce items. The SLR(1) parser will be able to determine which action to take as long as the follow sets are disjoint.
    换言之,SLR(1)能够解决一部分的S/R,R/R conflict,如果能够依赖Follow set来区分的话。这个能有多少提高呢?我很怀疑。可能就是依赖于某些语法大师修改冲突的语法来做到。
  12. 遇到一个小小的问题就是AWS的菜单语言设置问题,你单单修改没有用,需要改成其他语言再改回去,我看了这个评论才知道这个方法!我猜想这个和cookie有关。或者更可能的原因是我第一次sign-in页面的设置我忘记修改了。
  13. 要完全理解算法必须先要复习一下First/Follow set的计算,之前这个我认为是理解了,可是实际计算还是出错,主要出在Follow set计算。这个是在Top-Down-Parser的章节,这个定义很不错:
    1. Place EOF in Follow(S) where S is the start symbol and EOF is the input's right endmarker. The endmarker might be end of file, newline, or a special symbol, whatever is the expected end of input indication for this grammar. We will typically use $ as the endmarker.
    2. For every production A –> uBv where u and v are any string of grammar symbols and B is a nonterminal, everything in First(v) except ε is placed in Follow(B).
    3. For every production A –> uB , or a production A –> uBv where First(v) contains ε (i.e. v is nullable), then everything in Follow(A) is added to Follow(B).
    说白了,就是starting symbol要特别加上就结束符$
  14. 当然如果不知道怎么计算First根本就不知道怎么算Follow了:
    To calculate First(u) where u has the form X1 X2 ...Xn , do the following:
    1. If X1 is a terminal, add X1 to First(u) and you're finished.
    2. Else X1 is a nonterminal, so add First(X1) - ε to First(u) .
      1. If X1 is a nullable nonterminal, i.e., X1 =>* ε , add First( X2 ) - ε to First(u) . Furthermore, if X2 can also go to ε , then add First( X3 ) - ε and so on, through all Xn until the first non­nullable symbol is encountered.
      2. If X1 X2 ...Xn =>* ε , add ε to the first set.
    这里唯一需要注意的是只有当所有的X1 X2...Xn都可以是ε才能把这个加入到First set里。
  15. 第一个实践,针对这个语法,我们计算First/Follow:
    
    E' –> E
    E –> E + T | T
    T –> (E) | id | id[E]
    First(E)={(,id}
    First(T)={(,id}
    Follow(E)={$, +,),]}
    Follow(T)={$,+, ),]}
    
    而教授说在LR(0)的parser中会有Shift-Reduce conflict:
    
    E' -> •E
    E -> •E + T
    E -> •T
    T -> •(E)
    T -> •id
    T -> •id[E]
    
    针对id的话
    
    T -> id•       ===>Shift
    T -> id•[E]    ===>Reduce
    
    但是SLR(1)的parser会根据Follow(T)={$,+, ),]}中的token做Reduce,而像{[,(}就会做Shift。
  16. 实践一下吧,如果针对这个语法,我们计算First/Follow set这里我还糊涂了一下是否要去计算augmented rule的symbol E'的Follow,可是记住定义是只有出现在rule的右边才有可能计算的话就知道这个Follow(E')是无意义的
    
    E'–> E
    E–> E + T | T | V = E
    T–> (E) | id
    V–> id
    First(E)={(, id}
    First(T)={(,id}
    First(V)={id}
    Follow(E)={$, +, )}
    Follow(T)={$,+, )}
    Follow(V)={=}
    
    根据大师介绍这个状态的LR(0) parser会遇到Reduce/Reduce conflict因为
    
    E' -> •E
    E -> •E + T
    E -> •T
    E -> •V = E
    T -> •(E)
    T -> •id
    V -> •id
    
    在token是id的时候,
    
    T -> id•=====>Reduce
    V -> id•=====>Reduce
    
    那么根据Follow(T)={$,+, )},我们对于T做Reduce,根据Follow(V)={=}我们对V做Reduce。那么我的疑问是其他的情况呢?比如{(,是错误还是Shift呢?我觉得是错误因为Follow的意思就是说这是所有的可能的情况。或者说也许只能说这个就是SLR(1)的边界,超出部分就是不符合这个SLR(1)的语法了?这个只有自己画表格才能知道,但是太费时间了。
  17. 进入LR(1)感觉突然之间水流湍急了很多倍,我在惊涛骇浪中挣扎,根本来不及辨别方向信息如洪水般汹涌而来,我根本无法仔细看周围的细节就被激流裹挟着到了下一个漩涡之中,只有先快速看一下再重来一遍吧?这个算法实在是太复杂了!

    Repeat the following until no more configurations can be added to state I:

    — For each configuration [A –> u•Bv, a] in I, for each production B –> w in G', and for each terminal b in First(va) such that [B –> •w, b] is not in I: add [B –> •w, b] to I.
    这个解释要读好几遍才能理解尽管大师已经是很直白的解释的很好了
    What does this mean? We have a configuration with the dot before the non­terminal B . In LR(0), we computed the closure by adding all B productions with no indication of what was expected to follow them. In LR(1), we are a little more precise— we add each B production but insist that each have a lookahead of va . The lookahead will be First(va) since this is what follows B in this production. Remember that we can compute first sets not just for a single non­terminal, but also a sequence of terminal and non­terminals. First(va) includes the first set of the first symbol of v and then if that symbol is nullable, we include the first set of the following symbol, and so on. If the entire sequence v is nullable, we add the lookahead a already required by this configuration.
    字字珠玑,需要反复咀嚼。我太累了还是去健身房休息一下吧?

十一月十二日 等待变化等待机会

  1. 《矛盾论》和《实践论》才是指引科学前进和人类认识进步的总指南!每当我觉得我理解了一经实践立刻发现理解的重大偏差,而一个算法之所以诞生绝大部分是它在生产实践中暴露出的不足作为动力推动了它的提高和进步的。所以,很明显的大师的讲义深入浅出就在于每次在退出新算法之前在总结当前算法的局限时候给出一个不能解决的例子。而这个时候却检验了我的认识:原来SLR(1)和LR(0)的表的形态结构是一模一样的,仅仅是创建表的算法中的一步有些微的区别,也就是在考虑reduce一个non-terminal的时候要考虑它的Follow里的terminal作为action table的column。所以,基于这一点你才能理解为什么SLR(1)和LR(0)可以说复杂度是相当的,而LR(1)的表则完全不同因为它的item set里添加了lookahead token,整个计算过程就差别比较大了。甚至于创建表的算法的描述都差别很大。 大师给出的这个语法同样有着Shift-Reduce conflict,而这个在LR(0)就同样存在
    
    S' –> S
    S –> L = R
    S –> R
    L –> *R
    L –> id
    R –> L
    	
    在这个状态里
    
    I2 :
    S –> L• = R
    R –> L•	
    	
    这里根据LR(0)语法的局限:它违反了For any configurating set containing the item A –> u•xv there is no complete item B –> w• in that set except B is starting symbol,所以,它注定引起LR(0) parser的Shift-Reduce conflict。但是它是符合SLR(1)语法的因为Follow(R)={$,=}包含了=,所以,Reduce是合法的,于是SLR(1) parser也会遇到LR(0) parser同样的Shift-Reduce conflict。这个可能是最简短的阐述SLR(1)语法局限的例子其实熟悉c/c++语法的就是l-value/r-value的赋值表达式的部分,这个从侧面应证了以上两种语法对于普通编程语言的不足。
  2. 我想亲自动手实践一下LR(1)表的创建过程来加深理解:
    1. 这个是语法:我做了一点点改动把augmented rule的结尾加了eof符号$没有必要因为更混乱
      1. S' –> S$
      2. S –> XX
      3. X –> aX
      4. X –> b
    2. 先要初始化第一个起始符号的集合F is initialized to the set with the closure of [S' –> S, $].
    3. 创建item set的过程和LR(0)相似的但是也要顺便扩展lookahead的创建,注意 For each configuration [A –> u•Bv, a] in I, for each production B –> w in G', and for each terminal b in First(va) such that [B –> •w, b] is not in I: add [B –> •w, b] to I.这里要计算First(va)
      
      First(S)={a,b}
      First(X)={a,b}
      
    4. 现在开始画表格
      Configurating SetSuccessor
      Item SetsRules with DotLookaheadLookahead Formula
      I0 S' –> • S$ First($)initialized by hand S1
      S –> • XX $ First($$) the first $ is from rule as symbol $ immediately after S in S' –> • S$, the second $ is from propogating S2
      X –> • aX a/b First(X$) the second X in RHS S –> • XX and $ propogated from above S3
      X –> • b a/b First(X$)same as above S4
      I1 S' –> S • $ First(ε$)I consider ε is followed and $ is propogated accept
      I2 S –> X • X a/b$ First(X$)X is from second X in RHSThis exposes my misunderstanding because after second X which is our considering, there is nothing. So, only propogated $ S5
      X –> • aX a/b$ First(a/b$)a/b is propogatedonce previous lookahead is wrong, the propogated is wrong! S6
      X –> • b a/b$ First(a/b$) same as above S7
      I3 X –> a • X a/b First(Xa/b)X is from rule andThis exposes my possible misunderstanding because results is anyway the same a/b from propogated. Here is my understanding: the dot move does NOT calculate lookahead, we just copy it over? Maybe this is why the very first item in set is called kernel? S8
      X –> • aX a/b First(a/b)a/b is propogated S3
      X –> • b a/b First(a/b) same as above S4
      I4 X –> b • a/b First(a/b)simple propogation R3
      I5 S –> XX • $ First($)propogated??? R1
      I6 X –> a • X $ First($)This is the true power of LR(1) here because the lookahead is propogated and the item set was identical as I3 in LR(0)/SLR(1) S9
      X –> • aX $ First($)$ is propogated S6
      X –> • b $ First($) same as above S7
      I7 X –> b • $ First($)propogated R3
      I8 X –> aX • a/b First(a/b)propogated R2
      I9 X –> aX • $ First($) R2
  3. 我用bison的开关-g产生了一幅很精美的图,很显然的bison的算法是不同的,优化了很多减少了不少的状态减少了30%的状态!。这个图实在是顶的上千言万语。 我使用xdot打开bison产生的.dot文件,但是xdot没有输出到文件的功能,因为打印输出的图像总是偏在一边,只好用截图来保存。xdot还有很智能的部分很可惜不能输出svg图形。

十一月十三日 等待变化等待机会

  1. 制造工具的目的是为了使用工具解决具体的问题,如果不明白工具的使用方法也就无从理解工具应当如何改进提高,也就是说工具的使用的优劣性是改进工具的原动力。那么大师讲义里的使用LR(1) parsing table来具体解析的栈和action table的过程我看的不是很清楚,就亲自做一个实践吧:输入字符串为baab 。
    1. 这里一实践就暴露我的理解缺陷,我以为我创建parsing table的item set表能够作为action/Goto table来用,实际上是不行的还需要真正的创建这两个表。 这个是算法:实际上我昨天只是完成了第一步
      1. Construct F = { I0 , I1 , ... In}, the collection of configurating sets for the augmented grammar G' (augmented by adding the special production S' –> S ).
      2. State i is determined from Ii . The parsing actions for the state are determined as follows:
        1. If [A –> u•, a] is in Ii then set Action[i,a] to reduce A –> u ( A is not S' ).
        2. If [S' –> S•, $] is in Ii then set Action[i,$] to accept.
        3. If [A –> u•av, b] is in Ii and succ( Ii , a) = Ij , then set Action[i,a] to shift j ( a must be a terminal).
      3. The goto transitions for state i are constructed for all non­terminals A using the rule: If succ( Ii, A) = Ij, then Goto [i, A] = j .
      4. All entries not defined by rules 2 and 3 are errors.
    2. 惶恐中我突然发现我的笔记里没有函数successor的定义,回去翻教授的讲义才发现原来创建LR(1) item set的算法本身就定义了sucessor的算法!不!两者似乎一样但不是相同的,还是摘录下来这个重要的算法吧!要好好回味一下这个至关重要的算法!
      The successor function for the configurating set I and symbol X is computed as this:

      Let J be the configurating set [A –> uX•v, a] such that [A –> u•Xv, a] is in I. successor(I,X) is the closure of configurating set J.

      应该说之前的这个算法是所谓的closure of configurating set I的计算方法。我之前创建configurating set的时候就把这个successor函数和所谓的Shift混为一谈,两者是截然不同的,因为这里的是语法的任何symbol,而Shift是特别针对terminal大,其中的区别是很大的,是不同范畴的东西,就是说一个是Action table,一个是Goto table。
    3. 那么现在就先创建Goto/Action table吧:
      State on top of StackActionGoto
      ab$SX
      0 S3 S4 1 2
      1 accept
      2 S6 S7 5
      3 S3 S4 8
      4 R3 R3
      5 R1
      6 S6 S7 9
      7 R3
      8 R2 R2
      9 R2
      我这样子反复参照算法配合我之前的configurating set table画了这个Action/Goto table和教授的讲义对照是正确的也就是验证了我对于算法的理解是正确的了。
    4. 14.11.2021我这个parsing的过程无法完成!原因就是对于parsing表的使用不明了,之前以为明白了实际一用就暴露了,这个是LR(的parser tracing
      1. 我们不需要专门使用一个栈来保存当前读取的token,仅仅是一个当前需要处理的token而已,这个是一个巨大的理解的偏差,在我的脑海里我有两个栈,一个是状态栈,一个是已读取还等待处理的token的栈,后者是不存在的,我们只需要一个当前正在处理的token。
      2. 对于Shift我们的操作是直接读取下一个token,并没有什么需要把之前的token保存压栈的动作!这个是巨大的偏差,想想看,如果按照我的想法这个实际的内存消耗是很惊人的,因为现实中一个语法token对应的也许是一个很长的子串,那么要存一个很长的语法规则这还得了?
      3. 对于Reduce我的理解偏差最大!我们的确是需要把状态栈的状态pop,可是是按照语法规则的长度来控制pop次数的!而输入是不变的!这个是理解上的惊人的偏差!
      以上是否正确的理解需要实践来检验!
    5. 教授的表使用的栏目是剩余的remaining input,我觉得我更关心的是当前的输入光标在哪里,所以,我使用一个我认为的input stack。 14.11.2021这个表我尝试了好久都做不出说明对于算法的理解有问题,实践是检验真理及其对于真理的理解掌握程度的唯一标准,因为即便理论正确也不代表每个人都能正确理解,每个人对于同样的文字的真正理解是基于每个人头脑里内部的解析,彷佛一个黑盒子外界无从了解,唯一的检验方式就是黑盒测试一样的实践。是骡子是马拉出来溜溜!
      State Stack Remaining Input Formula(Shift/Reduce/Goto) Parser Action
      S0 baab$ {S0, b} ==>Shift S4 push S4 and read new input a
      S0S4 aab$ {S4, a} ==>Reduce R3 X –> b pop S4 pop 1 time
      S0 aab$ {S0, X} ==>Goto S2 push S2
      S0S2 aab$ {S2, a} ==>Shift S6 push S6 and read new input a
      S0S2S6 ab$ {S6, a} ==>Shift S6 push S6 and read new input b
      S0S2S6S6 b$ {S6, b} ==>Shift S7 push S7 and read new input $
      S0S2S6S6S7 $ {S7, $} ==>Reduce R3X –> b pop S7 pop 1 time
      S0S2S6S6 $ {S6, X} ==>Goto S9 push S9
      S0S2S6S6S9 $ {S9, $} ==>Reduce R2 X –> aX pop S9 pop S6 pop 2 times
      S0S2S6 $ {S6, X} ==>Goto S9 push S9
      S0S2S6S9 $ {S9, $} ==>Reduce R2 X –> aX pop S9 pop S6 pop 2 times
      S0S2 $ {S2, X} ==>Goto S5 push S5
      S0S2S5 $ {S5, $} ==>Reduce R1 S –> XX pop S5 pop S2 pop 2 times
      S0 $ {S0, S} ==>Goto S1 push S1
      S0S1 $ {S1, $} ==>accept
      这个表可以说是让我吐血的,我试图补充一个我认为跳跃的中间过程,结果发现教授的讲义实际上已经非常的简洁了,我修改了几次最后只是增添了一个类似successor函数的中间过程。而且还发现了我之前parsing table里的一个小错误,这里真的是要严丝合缝一步都不能错,自动机的特点就是它所需要的记忆量非常的小,因为push-down-automaton需要的是一个很简单的栈,根本没有搜索或者内存随机访问之类的高级功能。

十一月十四日 等待变化等待机会

  1. 这个表真的是呕心沥血啊,是参照着教授的讲义的表小小的修改反反复复的过筛子才确定是准确的算法。画完这个表我也就差不多了。
  2. 教授的总结其实很重要,虽然这个我们已经基本上明白了,首先是语法的范围大小或者说广泛性吧?
    Every SLR(1) grammar is a canonical LR(1) grammar, but the canonical LR(1) parser may have more states than the SLR(1) parser. An LR(1) grammar is not necessarily SLR(1), the grammar given earlier is an example. Because an LR(1) parser splits states based on differing lookaheads, it may avoid conflicts that would otherwise result if using the full follow set.
    那么SLR(1)相对于LR(0)的改进是可以类比于LR(1)对于SLR(1)的改进的。这里是LR(1)语法的定义:
    A grammar is LR(1) if the following two conditions are satisfied for each configurating set:
    1. For any item in the set [A –> u•xv, a] with x as教授的讲义似乎定义a也要是terminal,可是作为lookahead天生就要求是terminal,所以我以为可能是笔误吧? terminal, there is no item in the set of the form [B –> v•, x]. In the action table, this translates no shift­-reduce conflict for any state. The successor function for x either shifts to a new state or reduces, but not both.
    2. The lookaheads for all complete items within the set must be disjoint, e.g. set cannot have both [A –> u•, a] and [B –> v•, a] This translates to no reduce-­reduce conflict on any state. If more than one non­terminal could be reduced from this set, it must be possible to uniquely determine which is appropriate from the next input token.
  3. 下载了HOMM3的战略家大全
  4. 教授专门花了一整个章节来介绍LALR(1)。结果我看了后面忘了前面,已经开始糊涂LR(1)是怎么回事了。先去健身房回来再看吧?

十一月十五日 等待变化等待机会

  1. LALR(1)是出奇的简单还是我料敌从宽?因为它说穿了是一个缩减版的LR(1)一个加强版的SLR(1),是一个在两者之间的平衡点。这个说出来容易,魔鬼都在细节,一旦看细节我又糊涂了,它要怎么合并LR(1)的状态呢?合并了之后和SLR(1)又有何不同呢?教授讲义解释了我却没有看懂,部分原因我改变了之前一步一步稳扎稳打的战略改为大胆穿插的战术,这个是所有的战役失败的教训之处,孟良崮战役失败就在于此,当初鲁南战役和宿北战役以及莱芜战役失败让国民党军严格遵守稳扎稳打步步为营的战略不动摇才导致初期华野无机可乘,但是经过长时间的拉锯周旋,进攻方耐心耗尽开始大胆突进导致了精锐被歼灭。这个就是耐心耗尽的问题。所以,我也是快速浏览只求一个大概理解抓住大图景的战术。反正细节还是要回过头来仔细推敲还不如囫囵吞枣快速推进?我的理解就是其实很简单:SLR(1)之所以失败在于Follow set计算太宽泛,而LR(1)之所以正确是lookahead分的很细,但这个也是人们诟病的地方,我把它的状态合并并没有增加如前者那些宽泛的部分当然不会错了?但是新的问题就是那么LALR(1)LR(1)的差别在哪里?难道合并正确的状态会引入错误吗?必须要细分那些状态吗?多个独立状态会引入什么错误呢?这些问题才是很深奥的地方,需要透视眼才能理解。现在自己问出的问题才能深刻体会教授的讲解,否则第一次看过去似懂非懂不知所云:
    Can merging states in this way ever introduce new conflicts? A shift-­reduce conflict cannot exist in a merged set unless the conflict was already present in one of the original LR(1) configurating sets. When merging, the two sets must have the same core items. If the merged set has a configuration that shifts on a and another that reduces on a , both configurations must have been present in the original sets, and at least one of those sets had a conflict already.
    这个不但是解释而且直接就给出了一个反证法的证明!教授接下去的解释其实就容易理解了,因为Reduce-Reduce conflict是完全的另一码事,因为合并确实是会引起这个冲突,然而,我们不妨定义这样子的冲突本身就不符合LALR(1)的语法,这个釜底抽薪的解决办法是欧美国家治理的常用手段,如果我们不能消灭腐败,我们就把腐败合法化给他一个名称叫做游说,如果我们不能消灭毒品犯罪我们就把毒品活动合法化把其中我们认为不太严重的大麻部分合法化,如果荷兰不能禁止卖淫嫖娼活动就把卖淫合法化而把嫖娼继续保持,这个细分的做法和编译器的思路何其相似?我们就把我们能够解决的部分语法称之为合法的语法,不能解决的问题称之为非法就从我们的视野里剔除了,多么的干净完整的解决方法啊!
  2. 编译器是人们在对抗探索的进程,当初在简陋的猜测和直觉指引下人们发现了LL的算法因为很直观,可是贪心导致可能最好的时候很快,最差的时候很差,这一点像大膺飞得低的时候比麻雀要低,可是一飞冲天没有什么能拦得住。那么LR(0)是否就是一个人类直觉的产物呢?我觉得不是,反而更像是机械唯物主义的因循守旧,因为执迷于理想的自动机只拉车不看路,不求前瞻期望依靠道路依赖能够预测未来,而SLR(1)更像一个经验主义者依赖过去的First/Follow这样的泛泛的原则来期望能够在复杂状态下一劳永逸的解决层出不穷的新问题。在此之上又退而求其次退回了机械教条主义丧失了人的基本的主观能动性的退回了LR(1)企图依赖大量的堆砌细节来期待用理想主义的完美来解决客观世界的纷繁复杂,导致不切实际的用浩繁的技术细节来磨灭人类直观把握事物本源的创造性,导致只见树木不见森林的忽略生产生活中绝大多数编程语言的语法可以人为规范防止最糟糕的情况发生,于是诞生了一个相当不错的折衷LALR(1),一个在理想与现实的平衡,一个复杂性与可操作性的结合,既剔除了大多数的Shift-Reduce的冲突,而对于更加明显的Reduce-Reduce的冲突留给语法修改者来解决,我的感觉是对于人眼来说后者是比较容易发现的,而且解决这个语法的感觉是比较容易找到的吧?姑妄言之。
  3. LALR(1)的算法其实是相当的对于程序员友好的,基本上会编程的人一点就透,几乎不需要多少的解释,直觉的先计算所有的LR(1)再合并是教授在课堂上讲解给学生才会用的示范方式,而逐渐合并方式是实际编程实现很容易想到因为计算closure之类的本身也有一个比对之后添加的过程,只不过这里我们只是比较core items部分然后合并lookahead部分,这个操作如行云流水一般的舒畅自然,没有什么人想不到的。所以,这个章节的确是很短很简单。
  4. 大师在这个章节的东西都是很多很真知灼见的不传之秘,也许理论不觉得有多么深奥但是实际却非常有用的要诀,仿佛武功秘籍里口诀与心法的关系,前者是基础历代传承是原则性的普遍的原理,但是灵活运用的诀窍法门却是历代高手各自长期实践总结出来的非常实用的方法论。
    There are a few reasons we might want to resolve the conflicts in the parser instead of re­writing the grammar. As originally written, the grammar reads more naturally, for one. In the parser, we can easily tweak the associativity and precedence of the operators without disturbing the productions or adding extra states to the parser. Also, the parser will not need to spend extra time reducing through the single productions ( T–> F, E–> T ) introduced in the grammar re­write.
    总结起来就是一个是自然,一个是快捷。修改语法是非常的繁琐的,因为语法往往是人们对于一个事物的高度概括浓缩的描述,修改后变成机器易于理解而人类不容易理解的是背道而驰。去除这些多余的简单的多次的reduce更是效率的体现。怎么做到呢?对于操作符优先级的打破与维护我们只要倾向于reduce而不是shift。
    How we break the tie when the two operators are the same precedence determines the associativity. By choosing to reduce, we enforce left­-to-­right associativity.
    这些都是非常精炼的绝句! 那么这一句要怎么理解呢?
    Note that just because there are conflicts in LR table does not imply the grammar is ambiguous—it just means the grammar isn't LR(1) or whatever technique you are using. The reverse, though, is true. No ambiguous grammar is LR(1) and will have conflicts in any type of LR table (or LL for that matter).
    LR表有冲突不代表语法有歧义,只代表不是真正符合LR(1)语法这里我还是理解有困难。只能理解没有歧义的语法是另一个衡量语法的维度,不是和LR(!)语法内涵完全重合的概念,也许是所谓的deterministic的概念。要怎么理解呢?反过来似乎比较容易理解,没有歧义的语法一定能够创建出LR(1) parser而且肯定不可能有歧义,也不可能有冲突,是吗? 教授给出了一张震撼人心的图,这张图真的震耳欲聋,我看的心惊肉跳: 教授特别点出这一点:
    Note this diagram refers to grammars, not languages, e.g. there may be an equivalent LR(1) grammar that accepts the same language as another non-­LR(1) grammar.
    什么叫做A picture is worth a thousand words!纵有千言万语,却都在不言中!我看着这幅图感觉就是于无声处听惊雷!这个仿佛就是当年罗辑在坠入冰湖的那一瞬间看到的宇宙的真相的那种顿悟的感觉。
  5. 我不知道对于这个题目看过几次了,我始终半信半疑,原因是我不明白其中的道理:是c++的语法有歧义才导致不能实用如bison之类的工具吗?语法的歧义必须要在语义解析后才能解决,这个是语法的问题吗?这里有不少的信息:
    1. 作者说c/c++很难使用LALR(1) parser,需要额外的处理。
    2. 作者说c/c++可以比较容易使用GLR parser
      A GLR parser (GLR standing for "Generalized LR", where L stands for "left-to-right" and R stands for "rightmost (derivation)") is an extension of an LR parser algorithm to handle non-deterministic and ambiguous grammars.
      。这里关于GLR的介绍
      Briefly, the GLR algorithm works in a manner similar to the LR parser algorithm, except that, given a particular grammar, a GLR parser will process all possible interpretations of a given input in a breadth-first search.
    3. GLR可以使用generator来产生:Elkhound,而它产生的Elsa据说可以相当不错的解决c/c++的歧义?
    4. 此作者更多的是推销他们自己的产品就是使用GLR制作的,这一点让我很感兴趣。作者很骄傲的不断更新他们的对c++标准的支持,最新的是c++17。这个让人感到相当的有趣。我想起了科技猿人关于中国研制原子弹的难度的著名的评述,大意是最大的难度在于知道某条路径的可行性,中国的后发优势主要在于知道这个路径可以造出原子弹。
    5. 我直接编译Elsa但是看到说有更新版在oink,于是下载源码,可是简单的configure始终报错,最后还是google找到了这个补丁。而编译过程的错误似乎需要这个补丁,我要实验一下。
    6. 遇到古老的直接把stream强制转为void*的编译错误,我只能在编译开关上添加古老的标准。但是这又引出了其他的错误,我对于这个oink的信心在不断的流逝,这是一个成熟的项目吗?最后看样子还是改源代码强制转换吧?最后似乎还是应该按照注解里提示的改成实现stream的operator bool()方法,这个纯粹就是作者把运行库实现了一遍,当然是很小的一部分,是string和IO部分。也许为了方便或者移植,总之我是很恼火的。
    7. 遇到另一个比较少见的错误就是这个开关是过时的obsolete option -I- used, please use -iquote instead19.11.2021我的记忆力实在是太差了,这个是几个月前的笔记,我完全没有印象了。这个会导致编译错误找不到stringfwd.h,我查看了库文件里包含它的头文件使用的是引号而不是尖挂号<> 这个我觉得是比较tricky的部分:
      If you need separate control over the search paths for the quote and angle-bracket forms of the ‘#include’ directive, you can use the -iquote and/or -isystem options instead of -I.
      而问题的关键是这个过时的选项:
      -I-

      Split the include path. This option has been deprecated. Please use -iquote instead for -I directories before the -I- and remove the -I- option.

      Any directories specified with -I options before -I- are searched only for headers requested with #include "file"; they are not searched for #include <file>. If additional directories are specified with -I options after the -I-, those directories are searched for all ‘#include’ directives.

      In addition, -I- inhibits the use of the directory of the current file directory as the first search directory for #include "file". There is no way to override this effect of -I-.

      从这里我不知道作者到底是想使用带引号的文件还是说不想搜索本地的呢?我吃不透,所以,我的修改是很危险的,似乎问题很严重。
    8. 编译都出这么多的问题,我感觉这个要么是成熟度的问题,要么是人为的,据我所知这个也许是一个商业产品的基础,作者专注于开发商业化的产品了,这个开源的东西是他大学时代的实验品吧?我是小人之心。只是我不想浪费无谓的时间在这上面。这个比较也许可以告诉我们有其他选择。这里说的很清楚bison是一个很好的平台工具。作为评估是足够的。

十一月十六日 等待变化等待机会

  1. 这里看到了Elkhound的作者的更多的视频论文,我大部分看不懂,也不想深入因为没有那个能力,只是摘抄一个结论:
    Case Study: A C++ Parser
    To verify its real-world applicability, we put Elkhound to the test and wrote a C++ parser. This effort took one of the authors about three weeks. The final parser specification is about 3500 non-blank, non-comment lines, includ- ing the grammar, abstract syntax description and post-parse disambiguator (a limited type checker), but not including support libraries. The grammar has 37 shift/reduce conflicts, 47 reduce/reduce conflicts and 8 ambiguous nonterminals. This parser can parse and fully disambiguate most 6 of the C++ language, in- cluding templates. We used our implementation to parse Mozilla, a large (about 2 million lines) open-source web browser.
    论文随后列举c++语法中的困难部分,这个是我所感兴趣的:
    Type Names versus Variable Names
    The single most difficult task for a C or C++ parser is distinguishing type names (introduced via a typedef) from variable names. For example, the syntax “(a)&(b)” is the bitwise-and of a and b if a is the name of a variable, or a type- cast of the expression &b to type a if a is the name of a type.
    这个的确是非常难以解决的问题,我印象中在论坛里看到相关的帖子来说明语法的歧义性,但是怎么解决只是概念性的就是靠语义分析,但是我没有意识到这个实际上在编译器里有更加有效率的所谓的lexter hack
    The traditional solution for C, sometimes called the “lexer hack,” is to add type names to the symbol table during parsing, and feed this information back into the lexical analyzer. Then, when the lexer yields a token to the parser, the lexer categorizes the token as either a type name or a variable name.
    这个说起来简单实际上没有实践经验的我还是不太明白。但是明白这是一种骇客行为,是很有效率的因为能够在lexer阶段就解决当然是很高效率的,我猜想这个也许是parser要识别typedef之类的再反向注入这个到lexer的token table里?总不能parsing开始了然后再来一遍预处理吧?这个理解也许是对的因为作者说的这个typedef在后面就困难了,因为注入动作总是要发生在应用之前才行:
    In C++, the hack is considerably more difficult to implement, since a might be a type name whose first declaration occurs later in the file: type declarations inside a class body are visible in all method definitions of that class, even those which appear textually before the declaration.
    这个类的定义体里声明可以在后面出现的概念我头脑里不深,这个是很令人惭愧的,这个例子非常的棒:
    
    int *a;  // variable name (hidden)
    class C {
    	int f(int b) { return (a)&(b); } // cast!
    	// GCC complains cast to int loses precision. So, change to long int
    	//typedef int a;  // type name (visible)
    	typedef long int a;  // type name (visible)
    };
    
    这个例子主流编译器都能够识别a是作为type name出现的,但是没有作者提醒我却没有想到其中有这么困难的工作在发生在编译器里。
    To make lexer hack work for C++, the parser must defer parsing of class method bodies until the entire class declaration has been analyzed, but this entails somehow saving the unparsed method token sequences and restarting the parser to parse them later. Since the rules for name lookup and introduction are quite complex, a great deal of semantic infrastructure is entangled in the lexer feedback loop.
    为了验证作者的陈述我做了一个简单的实验:就是这段代码在GCC里用gdb跟踪看看今天的编译器是否还是如同作者在十几年前所宣称的那样处理它。看代码太困难,就读一读GCC的注释的例子就够了吧?在函数cp_parser_class_specifier_1里非常的复杂的过程,很多应该都是这个相关的原因吧?我很快就放弃了这个不切实际的幻想,因为这部分的代码实在是太复杂了,任何一段GCC的代码都需要很长时间的理解。我虽然不能肯定作者的陈述,但是有一点是确定的就是对于类的定义体里很多情况非常的复杂是需要至少两次以上的解析,比如注释里给出的例子就需要解析函数两次:
    
    struct S {
    	void f() { g(); }
    	void g(int i = 3);
    };
    
    这个在c语言里是不可能遇到的,因为在c++的类定义里,你不能指望g会前置声明,何况还有默认参数的问题,这个根本不可能一次解决的。 我在想GCC的开发团队要在c++标准的哪一年才会说我们放弃了,这个是不可持续的。也许。。。clang给我们一个前端吧? 作者说这个在GLR里很容易解决:
    However, with a GLR parser that can tolerate ambiguity, a much simpler and more elegant approach is possible: simply parse every name as both a type name and a variable name, and store both interpretations in the AST. During type checking, when the full AST and symbol table are available, one of the interpretations will fail because it has the wrong classification for a name. The type checker simply discards the failing interpretation, and the ambiguity is resolved.
    随后大侠解释这个细节我却看不懂,因为我还没有这方面的经验和知识储备:
    The scoping rules for classes are easily handled at this stage, since the (possibly ambiguous) AST is available: make two passes over the class AST, where the first builds the class symbol table, skipping method bodies, and the second pass checks the method bodies.
    为什么要两次?如果传统编译器解析两次是否也能解决呢?这个似乎在否定GLR的容错能力了吧?难道在第二次里就不能在类型检查里发现错误吗?那么何必要复杂的GLR呢?在我看来GLR就是分叉建立两个或者更多的AST,那么有什么必要来两次呢?两条路径都要两次吗?那不是四次?
  2. 另一个歧义的地方是这里
    Declarations versus Statements
    Even when type names are identified, some syntax is ambiguous. For example, if t is the name of a type, the syntax “t(a);” could be either a declaration of a variable called a of type t, or an expression that constructs an instance of type t by calling t’s constructor and passing a as an argument to it. The language definition specifies ([12], Section 6.8) that if some syntax can be a declaration, then it is a declaration.
    这个大概又是一个典型的例子,而作者引用的委员会的解释让人感觉彷佛是我们小时候的儿歌,远看像萝卜,近看像萝卜,它就只能是萝卜。这个比喻是粗俗的,就是说解决冲突就选择一个。就像我们对待dangling-else一样选择Shift而不是reduce让我们把最近的if联系起来。 作者解释的解决方法我看不懂
    Using traditional tools that require LALR(1) grammars, and grammars for language fragments A and B, we would need to write a grammar for the language A\B. This is at best difficult, and at worst impossible: the context-free languages (let alone LALR(1) languages) are not closed under subtraction.
    这里是说要定义某些例外情况吗?这个在语法里怎么表达呢?我不明白。 这里我做了实验才明白这个例子是纯粹的parser的问题,就是说这个是纯粹的语法解析才有的困扰,在应用先声明再使用的原则下,这个是不可能发生的。因为a没有之前被声明过,它不可能作为参数出现,所以只能是变量声明。
    
    struct t{ 
        t(int i=0){}
    };
    t(a); // what is a? 
    
    即便我想混过去使用一个函数参数来shadow,但是这个似乎不是warning而是error,我以前印象中这个不能算error的?
    
    void test(int a){
        t(a);  //error: declaration of 't a' shadows a parameter
    }
    
    也许这个shadow在c语言才算是warning?如果我强制使用c编译器-x c,得到的是警告:warning: implicit declaration of function 't' [-Wimplicit-function-declaration]所以,c语言问题也是不少,直接可以解析为函数声明了。我怎么看都不像,为什么是函数调用变成声明了呢?就是说函数没有声明就可以直接使用,这个违反了先声明再使用的原则吗?,当然c++的成员函数/constructor是不可能有的了,这根本也不可能。所以,这个纯粹是语法上的歧义,在语义阶段是可以解决的,当然我们讨论的就是语法的歧义。先声明后使用的原则本来就是语义阶段的,是吗?没有经过lookup怎么知道有没有声明过?
  3. 我想模板一定是任何编译器的实现者的噩梦:
    Angle Brackets
    Templates (also known as polymorphic classes, or generics) are allowed to have integer arguments. Template arguments are delimited by the angle brackets < and >, but these symbols also appear as operators in the expression language:
    作者说的这个规定我好像没有什么印象啊
    The language definition specifies that there cannot be any unparenthesized greater-than operators in a template argument ([12], Section 14.2, par. 3).
    我搜索标准似乎这个是古老的页码了,找到这个典型的案例
    这里编译器会把std::enable_if<sizeof(T) > 1, void>::type其中的大于号作为模板参数结束符号,所以必须要要加括号才行。这个是我遇到过的问题了。
  4. 在查找作者的关于标准的引述的时候我意外遇到一个问题:就是怎么使用字符串作为模板参数因为这个是被禁止的。但是我随后发现这个是有限制的必须是constexpr导致它几乎没有用,因为如果你要存储constexpr的ctor的参数的话只能存在static的成员里,导致这没有什么用了。
  5. 大侠说的这段话是什么意思呢?
    It is interesting to note that, rather than endure such violence to the grammar, the authors of gcc-2.95.3 chose to use a precedence specification that works most of the time but is wrong in obscure cases: for example, gcc cannot parse the type “C< 3&&4 >”. This is the dilemma all too often faced by the LALR(1) developer: sacrifice the grammar, or sacrifice correctness.
    难道说GCC定义了操作符的优先级吗?
  6. 大师设问的这个问题非常的让人震撼:
    Building the C++ parser gave us a chance to explore one of the potential draw- backs of using the GLR algorithm, namely the risk that ambiguities would end up being more difficult to debug than conflicts. After all, at least conflicts are decidable. Would the lure of quick prototyping lead us into a thorn bush without a decidable boundary?
    就是说歧义性很难发现,对吗?难道发现两条路径都成立不是证明吗?大师说这个并不像想像的那么困难
    ambiguities and parse trees are concepts directly related to the grammar, whereas conflicts conceptually depend on the parsing algorithm.
    这句话太深奥了,以后看看有没有机会接触这些高深的概念。为什么conflict是由于算法引起的呢?

十一月十七日 等待变化等待机会

  1. 大侠的论文读了能读懂的部分,回过头来看教授对于LL和LR算法的特点总结与比较
    PerspectiveLL(1)LALR(1)Remarks
    Implementation LL(1) parsers may be implemented via hand­coded recursive-­descent or via LL(1) table­driven predictive parser generators like LLgen . Because the underlying algorithms are more complicated, most LALR(1) parsers are built using parser generators such as yacc and bison . There are those who like managing details and writing all the code themselves, no errors result from misunderstanding how the tools work今天我们能够相信bison是成熟可靠的吗?, and so on. But as projects get bigger, the automated tools can be a help, and yacc / bison can find ambiguities and conflicts that you might have missed doing the work by hand.就是这一点也是相当的有意义,考虑到委员会成员在不断的疯狂的把其他语言的优秀特性引入c++的话,你能够避免语言/语法的歧义性吗?前者真的不可能发生吗?哪怕避免后者也是极端必要的吧?看看lambda的不断的演化就知道维持一个语法的困难程度。 The implementation chosen also has an effect on maintenance. Which would you rather do: add new productions into a grammar specification being fed to a generator, add new entries into a table, or write new functions for a recursive­-descent parser? 关于GCC的灵魂之问,如果在二十年前预见到当前c++语法是一个每隔三年就有一个相当大甚至巨大的演化,当初的设计者会怎么决策?
    Simplicity The algorithm underlying LL(1) is simpler, so it’s easier to visualize and debug. The details of the LALR(1) configurations can be messy and when trying to debug can be a bit overwhelming. Both techniques have fairly simple drivers.
    Generality All LL(1) grammars are LR(1) and virtually all are also LALR(1), although there are some fringes grammars that can be handled by one technique or the other exclusively. This isn't much of an obstacle in practice since simple grammar transformation and/or parser tricks can usually resolve the problem. As a rule of thumb, LL(1) and LALR(1) grammars can be constructed for any reasonable programming language.
    Grammar conditioning An LL(1) parser has strict rules on the structure of productions, so you will need to massage the grammar into the proper form first. If extensive grammar conditioning is required, you may not even recognize the grammar you started out with. The most troublesome area for programming language grammars is usually the handling of arithmetic expressions. If you can stomach what is needed to transform those to LL(1), you're through the hard part—the rest of the grammar is smoother sailing. LALR(1) parsers are much less restrictive on grammar forms, and thus allow you to express the grammar more naturally and clearly. The LALR(1) grammar will also be smaller than its LL(1) equivalent because LL(1) requires extra nonterminals and productions to factor the common prefixes, rearrange left recursion, and so on.
    Error repair LL(1) parse stacks contain symbols that are predicted but not yet matched. This information can be valuable in determining proper repairs. 瞻前 LALR(1) parse stacks contain information about what has already been seen, but do not have the same information about the right context that is expected. This means deciding possible continuations is somewhat easier in an LL(1) parser. 顾后 Both LL(1) and LALR(1) parsers possess the valid prefix property. What is on the stack will always be a valid prefix of a sentential form. Errors in both types of parsers can be detected at the earliest possible point without pushing the next input symbol onto the stack.
    Table sizes For LL(1) parsers, the uncompressed table has one row for each non­terminal and one column for each terminal, so the total table size is |T| x |N|. An LALR table has a row for each state and a column for each terminal and each non­terminal, so the total table size is |S| x (|N| + |T|). The number of states can be exponential in the worst case. (i.e., states form the power set of all productions). So for a pathologically designed grammar, the LALR(1) could be much, much larger. However, for average­case inputs, the LALR(1) table is usually about twice as big as the LL(1). For a language like Pascal, the LL(1) table might have 1500 non­error entries, the LALR(1) table has around 3000. This sort of thing used to be important, but with the capabilities of today’s machines, a factor of 2 is not likely to be a significant issue.
    Efficiency Both require a stack of some sort to manage the input. That stack can grow to a maximum depth of n, where n is the number of symbols in the input. If you are using the runtime stack (i.e. function calls) rather than pushing and popping on a data stack, you will probably pay some significant overhead for that convenience (i.e. a recursive-­descent parser takes that hit). If both parsers are using the same sort of stack, LL(1) and LALR(1) each examine every non­terminal and terminal when building the parse tree, and so parsing speeds tend to be comparable between the two.
    这个总结是相当重要的,因为它给了你一个大图景,尤其一些结论性的东西对我的认知有很大的冲击,我一直以为LL(1)要比LALR(1)少很多,看来那个仅仅是worst case,一般的编程语言未必,这里的一般编程语言包括c++吗?讲义里有大段的编程语言的发展简史,读来娓娓动人,我回想起在大学里中国的讲师能够把这么动人的故事全部跳过真的是误人子弟。那时候的国内大学基本上讲课就是一帮子助教,可能连讲师都算不上,完全是欧美大学里高年级学生作为辅导员的角色,中国的教育水平的断层来自于文革的摧残,需要很多代的修复。每次我都想起来刘慈欣在《乡村教师》里从宇宙文明角度来审视人类文明的本质,这是一个缺乏记忆知识继承的物种,只能依靠口腔器官振动空气发出声波信号以速率仅仅不到几十个bps的传播速度来彼此通信,而每个接受者的理解程度识别率除了最原始的黑盒测试没有任何其他手段的原始与落后的方式。在这样与硅基生命的自动知识,记忆几乎可以全部遗传给后代而且个体间彼此通信依靠光电效应通讯速度达到兆甚至千兆甚至更高的文明面前完全没有存在的必要,而这个物种的文明的传承手段竟然主要依赖于最原始的一群自称为教师的个体的一代又一代反反复复重复的所谓的口口相传以至于保持他们最古老祖先刚刚发现并利用一个他们自称为化学反应的一种依赖于专门个体不断添柴的所谓薪火相传的方式来传承他们文明中积累下来的知识文化技术。在宇宙大神面前这种落后的物种居然对于未来的代表先进进步的硅基智慧物种产生出于物种生存竞争本能的抵制与阻挠,是可忍孰不可忍?消灭人类暴政,这个世界属于三体!在这个蓝色星球上目前生活的这个种群在道路依赖式的思维观性下,主观的认为智慧体天生就是由碳基化合物组合而成,直到他们开始利用他们掌握的工艺技术造出他们称之为计算机的具有原始智慧特征的机器才开始意识到,生命固然最容易起源于碳基化合物因为其活跃性相比于硅基化合物的稳定更加容易在原始自然而然产生,但是其固有的不稳定性既是其容易在自然环境中不断变异演化,也是其天生的难以稳定保持的特点导致它在最终和硅基智慧体竞争中必然被淘汰。如果要改变这个命运的一种可能手段是创造出记忆遗传以及更高速率的通信手段和接收可靠性评估机制,或者就直接放弃个体个性差异化的优点做完全思想思考机制的克隆。退而求其次的另一种可能性是只在少数精英个体上实现上述机制,而让其他绝大多数个体转为自然界蚂蚁社群里的工蚁兵蚁角色。
  2. it was realized that BNF and context­free grammars were equivalent
  3. 眼前这场世纪大瘟疫的发生也许是物种进化的周期性发生的消减种群数量的自然过程,从目前看欧美社会充斥的各种各样的稀奇古怪i的所谓的政治正确论调,说到底是反映了当前发达国家社会的现状,也就是种群个体在缺乏残酷生存竞争压力下的自然退化现象,物种在长期进化或者说演化中总是时时刻刻尝试向各种可能的方向尝试性的变异,这个是随机发生的自然过程,而客观环境的能够容纳的种群数量限度成为抵消这种变异取向的裁判员,当种群个体变异有利于更高效率获取生存繁衍的资源时候就自然而然的向这种变异开绿灯以倾斜资源的方式让这种变异的后代优先得到发展。而相反的如果某种变异导致获取资源的成本增高以至于可能容纳的个体总数减少的话,自然资源总量基本保持恒定的客观限制就像裁判员一样自动干预让当前种群内部为抢夺有限资源而内耗,当前流行的说法称之为内卷。目前处在北美的这个国家的个体平均占有资源数量远远超过其他各个大洲的平均数,如果它的个体不能有效演化形成高于其他地区个体获得资源的效率则必然导致自然资源总量相对恒定的法则的惩罚,惩罚的方式并不是简单的消灭获取资源最高的个体,而是导致各个种群个体都在感到资源获取困难后产生激烈斗争拼抢有限资源的零和博弈,其最终形式是所谓的在两个甚至更多个的军事联盟之间的世界大战。应当很容易证明多个军事集团最后会在很简单的投入产出比较后自动缩减为最终两个阵营,而战争的终点在一方的彻底放弃获得自然资源的前提下而结束。目前地球文明就处在这样一个爆发的前夜,而当前的新冠病毒是它的一个直接的导火索或者是催化酶。

    蔓延在欧美社会当前的有关性别取向的所有运动都是物种自然演化过程从一开始就不断发生的异化现象的放大,这种放大一方面是过去在残酷生存竞争两性自然分工随着现代文明技术进步带来的模糊性造成 ,雄性肌肉耐力在机械的代替下不再是必然的优势选择,即便是在国家对抗的战争环境下也由于武器技术的进步不再主要依赖力量和耐力的面对面的肉搏,这个导致自然演化不再强制强化性征两极化发展。而另一方面,随着所谓文明进步,在古代受制于自然资源获取量有限而必须不断进行的种群数量控制的机制被取消,导致大量本来在严酷自然淘汰机制下不可能存活下来的个体的自由繁衍导致主体种群数量渐渐边缘化发展,导致社会内部异化现象日趋严重,作为所谓的民主机制自动反映种群数量分布的方式造就出了各种各样在传统社会无法想像的新主张。但是这一切都有归零的一天,因为大自然的法则是:人类可以自由进化,大自然只管无情收割。究竟哪种变异更能长久,活到最后就是选择,这个就是最简单直观的物竞天择适者生存的自然法则。

  4. 教授新的一章的讲义似乎很高深,慢慢看吧?感觉怎么这么像是bison/yacc的输入文件形式?的确是的。

十一月十八日 等待变化等待机会

  1. 俗事缠身。教授的讲义关于bison和bison自己的手册,我感兴趣的是关于冲突和解决的办法以及算法部分。这里手册也说到了操作符优先级的问题,应该在c++语法里是不体现的,因为我印象中语法本身隐含了优先级,否则就有歧义性。这里是关于正负号的优先级的说明,我看不懂,头疼。以后再看吧。
  2. 这里才看到bison默认并不是创建GLR parser的需要在声明部分声明:%glr-parser

十一月二十日 等待变化等待机会

  1. quoted并不是返回字符串,而是一个结构,它只是重载了操作符<<,所以只能使用stringstream来收集它的结果。

十一月二十一日 等待变化等待机会

  1. 我不知道是什么时候被教育不要使用assert坏习惯,导致我头脑里一直在抵制它以至于见而生畏,这个看来是一种偏见,既然是debug手段,自然不应该是有什么限制,只不过把带有debug代码在正式版里保留是一种期望用户替你完成未完成的任务的偷懒的行为。总而言之,我看来是有偏见的导致我对于一些稀松平常的东西有莫名的抵制,这个是仁者见仁智者见智的手段,没有什么好与不好的,所以,我也可以培养这种偏好。
  2. 我一直不知道gedit有这么多好用的插件,当然这个需要安装。

十一月二十二日 等待变化等待机会

  1. 有时候对于这么简单的一个功能你却觉得标准库里没有趁手的兵刃。比如我要trim字符串结尾的空白符,一开始想使用string_view里的一些功能期望比较有效率方便,否则为什么要创造它呢?肯定是提供了一些新的功能才对啊?可是找不到合适的方法,最后居然只好这样子:
    
    while (!str.empty() && str.ends_with('\t')){
    	str.resize(str.size()-1);
    }
    
    因为我感觉sting_view其实并不好用,尤其你要防止从空字符串创建的错误。不过这个也许并不是它的问题,因为对于string来说从nullptr创建也是一样的违法。
  2. 另一个问题就是我在使用这些语法的时候才发现了其中的问题,首先是不够完整,问题就是说我即便忽略cpp的部分,对于module依然是一个很大的头疼的部分,因为module是一个横跨cpp和parser之间的甚至还牵扯到linker的超级复杂的部分,单单语法部分就是很复杂的。而我也想不清楚究竟要达到什么目的。但是凡事开了头就是好事,因为我立刻就看到bison里爆出了几百个conflict,你也就明白c++的语法是几乎完全不适合bison的,也许可以尝试一下GLR,但是你总算可以明白为什么这个没有人尝试的原因,不是没有,而是大家都尝试过然后放弃的。

十一月二十三日 等待变化等待机会

  1. 花了两天时间才做了这么一个实验,还是很有意义的。
    1. 首先是从标准里拷贝来的语法,我有意识的忽略了cpp部分的语法,因为我压根儿不想去关心和实现它。毫无疑问的,有些部分必须手动修改,比如语法里简单的说是什么one of ...我只能手动把它们改成bison接受的BNF形式,其他的就是创造几个token,因为我还没有想好要怎么实现或者必须要在flex里实现的部分。存在这里做输入文件
    2. 然后的就是我的小程序去整理这个成为规范的BNF输入,主要是区分terminal对其加引号,和处理c++语法中的众多的opt规则,其中,又要区分是terminal还是nonterminal,总之都要创建一个新的opt的规则使用bison的%empty来表面它是optional的,这里特别啰嗦的是对一些操作符的optional,我只能自己给它们起名字,比如...opt就叫做eclipseopt
    3. 一个简单的Makefile来运行这个小工程。
    4. 很快的暴露了c++语法中相当多的冲突:
      
        First example: • "using" identifier attribute-specifier-seqopt '=' defining-type-id ';' $end		
        Shift derivation
          $accept
          ↳ translation-unit                                                                     $end
            ↳ declaration-seqopt
              ↳ declaration-seq
                ↳ declaration
                  ↳ block-declaration
                    ↳ alias-declaration
                      ↳ • "using" identifier attribute-specifier-seqopt '=' defining-type-id ';'
        Second example: • "using" "namespace" nested-name-specifieropt namespace-name ';' $end
        Reduce derivation
          $accept
          ↳ translation-unit                                                                                       $end
            ↳ declaration-seqopt
              ↳ declaration-seq
                ↳ declaration
                  ↳ block-declaration
                    ↳ using-directive
                      ↳ attribute-specifier-seqopt "using" "namespace" nested-name-specifieropt namespace-name ';'
                        ↳ •	
      	
      这里我还是看不大懂这个冲突的意思,难道是说"using"后面无法分辨identifier"namespace"的区别吗?或者是说attribute-specifier-seqopt 需要reduce?但是这个不是众多的optional的语法的问题吗?
    5. 这样子的天真的语法产生了上千的冲突:
      
      cplusplus.y: warning: 14639 shift/reduce conflicts [-Wconflicts-sr]
      cplusplus.y: warning: 6307 reduce/reduce conflicts [-Wconflicts-rr]
      
      26.11.2021结果我更新了一下补充了遗漏的语法导致更多的冲突了:
      
      cplusplus.y: warning: 14813 shift/reduce conflicts [-Wconflicts-sr]
      cplusplus.y: warning: 7650 reduce/reduce conflicts [-Wconflicts-rr]
      
      我随后想实验看看GLR算法能否解决一些?根本不能解决冲突,难道这个是语言本身的模糊问题?
    6. 打算下载编译bison最新的版本看看。看上去没有多少代码为什么编译这么复杂呢?还非要自己的库?我没有遵照指示先更新子目录git submodule update --init结果还配置失败了。而且居然要求高版本的autoconf,我只能下载编译2.71版的。
    7. 这里还有一个关于eclipse的小插曲耗费了我好多精力,就是我import existing Makefile project总是失败害得我还重新启动电脑,最后才想到查看bison的目录下居然有一个.project文件,看来这个是冲突的原因,因为eclipse需要创建.project和.cproject这两个文件。真的是浪费时间啊。我之所以要看代码是因为最新版的bison爆出错误,我后来才发现我不能不安装bison,似乎bison配置相当的复杂我看到有rpath之类的,看样子这个是版本不同造成的。
  2. 系统监视器居然有这么一个问题!我居然要卸载snap的版本?

十一月二十四日 等待变化等待机会

  1. 我以为我对于linking已经很熟悉了,但是遇到linking script就又是眼前一黑晕倒了,起因是我使用gcc链接libm不成功,因为代码里定义的pow没有定义,所以,查看/usr/lib/x86_64-linux-gnu/libm.so发现它是脚本,而这里的语法GROUP是什么意思呢?
    GROUP(file, file, …)
    GROUP(file file …)
    The GROUP command is like INPUT, except that the named files should all be archives, and they are searched repeatedly until no new undefined references are created.
    这里的archives我猜想就是ldconfig的杰作。 这个并没有解决我的疑惑,直到我看到/lib/x86_64-linux-gnu/libm.so.6的函数并没有pow才意识到这个其实挺复杂的,因为g++可以正确的识别使用哪一个pow,它们是一系列的函数,比如cpowf64,powf, cpowl等等实际上应该是一系列的参数类型重载了,而这个肯定需要一个小小的翻译代码指向真正的实现函数吧?这个是谁在做呢?c++并没有特别指明参数类型。肯定有人要做这个工作,这个难道不应该是libm.so.6应该做的吗?c版本的原型也没有多少帮助。实际上这个奇怪的问题来自于bison的例子,在c语言的例子里有使用头文件math.h而且不论我是否链接-lm都不成功,更让我惊讶的是普通的时候我根本不需要链接-lm,后来我看到错误信息说built-in pow和这个帖子才相信这个也许是编译器优化把普通的数值计算放在了编译期进行。比如如果代码是这样trivial的是测不出这个问题的:int foo=pow(1,4);我在汇编里看了半天都看不到pow的调用,编译器即便在我强制不准优化的指令下依旧直接计算了pow的结果这个直接就是编译期的计算结果,当然只有在我使用变量作参数的时候这个链接不到的问题才暴露,而进一步的测试我惊讶的发现这个在c语言下链接-lm失败的问题是我的ubuntu18/gcc-7.5的问题,当我使用我自己编译的gcc-10.2.0的时候我发现原来libm被包含在了glibc库里,pow@GLIBC_2.2.5,这个说明了它使用的是带版本的库函数,而只有在ldd下才暴露它的库的依赖,换言之我编译的时候GCC只是在10.2.0的版本下正确的认定pow的动态库依赖,留给运行期,当然这个前提是。。。
  2. 一个简单的链接-lm引发了这么一场腥风血雨?
    1. 首先,这个pow在编译器里有自带的库函数只要有可能编译期就直接计算了事了,所以,你使用常量计算数值根本不需要运行期的动态库来解决,在代码里直接就编译成了数值,而且即便我要求不准优化-O0也不行。
    2. 链接是一个异常复杂的过程,我使用LD_DEBUG=symbols试图追踪为什么pow无法在我的GCC-7.5下成功链接并没有得到什么头绪,结果无意中发现我随手把-lm放在了编译开关的最靠前,居然动态库也像静态库一样有链接顺序?而这个仅仅是GCC-7.5有这个问题,至少GCC-10.2.0不论你把链接库放在开始还是最后都没有问题,这个问题看来是比较深奥的。
    3. 联想这个linking script要求,我怀疑这里也许也和环境配置有关系吧?因为在我看来找不到pow的一个很可能的原因就是没有反复的搜索,因为链接顺序造成的影响就是和静态库位置相关类似的,照例说动态库压根不应该有这个问题啊?难道说动态库链接机制是依赖于这个脚本的反复搜寻才实现的?我对于这个解释是比较信服的,因为直觉上动态库链接凭什么就没有顺序的影响呢?最简单的解释就是链接脚本的功劳吧?
    4. 数学库是这么常见的一个问题,我依稀记得在哪里看到过这个问题的讨论,就是说编译器在编译期对于整数可以模拟数学库的计算直接了事,只有说浮点运算才会把这个任务交给动态库libm.so来运行期解决,因为这个开销还是挺大的,我才会在libm.so.6里看到除了pow之外的大量的pow的姐妹函数适应各种类型的参数,也就是说它们才是真正复杂需要动态运行计算的版本,那么编译器的一个任务就是在自己不能解决除了整数类型参数意外的情况习负责指明链接的正确的重载类型,这个工作显然在c和c++里是不同的吧?相当于在c函数里支持重载的意思。总之肯定有一系列的stub对应来调用。
    总而言之,为了一个小小的和bison完全无关的折腾了一个早上,到头来发现是我的GCC-7.5的微妙的问题。

十一月二十五日 等待变化等待机会

  1. 看bison的手册总的感觉就是一个字:难。非常的费解,因为没有概念,经常不知道是怎么回事,看来没有一个星期很难理解基本的例子。也许结合代码更好理解吧?

十一月二十六日 等待变化等待机会

  1. 我以前应该反反复复看到过这个关于import-keyword and export-keyword的解释,但是我始终不理解,我一直认为它们就是简单的importexport这两个terminal的简单的包装的non-terminal,可是现在在修改我的程序的过程中我开始意识到应该是另一个意思:
    In all three forms of pp-import, the import and export (if it exists) preprocessing tokens are replaced by the import-keyword and export-keyword preprocessing tokens respectively.
    就是说预处理器实际上把它们替换成了一个人造的新的terminal:import-keyword and export-keyword。我相信大多数英语母语的读者应该没有理解的困难,可是我始终没有理解,当然其中的一个可能的理由是这个语法是有被修正过的,最早的并没有这么做,应该是实现上遇到困难要降低难度才这么做的,这样子预处理的改变是比较小的,或者说根本就不可能让预处理器来完成module的工作,所以,这个是传统的手法在预处理器添加了这个特别token。当然这里我现在才看明白实际上import头文件和include是等价的:
    Additionally, in the second form of pp-import, a header-name token is formed as if the header-name-tokens were the pp-tokens of a #include directive. The header-name-tokens are replaced by the header-name token.
    至于说为什么我就不太理解了。
  2. 这个语法差点害死我,因为我当时完全想不明白为什么我拷贝的语法有问题。没想到是这么的让人哭笑不得,你能想象BNF里nonterminal和terminal使用完全相同的名字吗?但是仔细阅读我有些糊涂了,面对几十年的司空见惯我就茫然了,这个是准确的语法吗?
  3. 还是先更新我之前的错误吧?这个是目前产生的bison的输入文件。这个是用bison产生的html文件它非常的巨大有15M,而产生的向量图更加的大完全无法使用dot来转化。我感觉似乎比这个制作要更好一些吧,至少对我目前更好用。
  4. 基本上没有人指望真的有BNF语法能够作为实现,所以,对于这些约定成俗的部分有点语焉不详的意思,比如这个alternative representation我一开始误以为是需要preprocessor把它们替换为对应的操作符,似乎在哪里也看到了类似的说法,但是我似乎没有在GCC的libcpp里看到这种操作,那么为什么c++标准里不提它的语法呢?现在回过头来看这位大侠的工作,我才意识到他做的很好。因为比较标准的写法让我被误导了。我怀疑这个是笔误或者至少可以改进。大侠给它们改名为overloadable-operator我决定也遵照他的做法。而且我现在感到欣慰的这个的确是早就知道的问题,我没有错。而且我注意到这个提案是2005年就提出来了,标准委员会的老爷们死不改悔,压根没有人关心,因为比这个重要的问题一大堆。我随后发现这个根本就是c语言委员会的范畴,难怪c++标准组不愿意动手。我找到这个大部头的文档,它是属于wg14小组的,不是wg21小组的,就是说这个是c委员会的玩意,它里面规定了这些: 在GCC里有一个头文件iso646.h
    这里让我彻底糊涂了,难道说这个是c++所不支持的吗?不可能啊?那么这个宏__cplusplus表明这些字母形式的操作符是c++天然支持的,那么在c语言里也可以支持,只不过编译器内部已经使用宏处理了,所以,我猜想这个就是所谓的phase7的工作吧?但是为什么语法里不包含这些keyword呢?我的解释就是标准委员会懒得管,大家都忙死了,重大的攸关生死的事情都管不过来了,就好像美国目前的情况一样的,连杀人放火都没有精力来管,谁还在乎偷窃?所以,零元购在美国进行的如火如荼。
  5. 但是接下来我发现大侠的这个做法让我很费解,这个并不是标准的语法,难道是老版本的吗?

十一月二十七日 等待变化等待机会

  1. 针对大侠的语法我提了两个issue。一个是关于请教如何改进and, xor, ...这部分遗漏的语法的问题。另一个是关于literal-operator-id没有更新到最新的语法的问题。
  2. 只有真正开始去做才明白,因为我看到大侠的identifier的定义和标准不同,我尝试的提了一个issue。这个也促使我认识到我的众多的冲突很多来自于预处理器的问题,或许我可以把这些语法隐藏起来放在lexer里,比如identifier就定义一个token好了,所以,我把它改成了IDENTIFIER,果然冲突下降到了一千多个,当然这个并没有改变问题,只是原来二十六个字母排列组合的冲突数字下降了而已。但是这个也让人看的舒服多了。
  3. 然后我再来看c++的语法就能够比较真切的体会到了其中冲突和模糊的关系了:
    
    type-nameclass-name
    		| enum-name
    		| typedef-name
    class-nameIDENTIFIER
    		| simple-template-id		
    enum-nameIDENTIFIER
    typedef-nameIDENTIFIER
    		| simple-template-id		
    
    产生的R/R冲突:
    
    class-nameIDENTIFIER   ["::"]
    enum-nameIDENTIFIER   ["::"] 
    typedef-nameIDENTIFIER   ["::"]
    
    现在我的问题就是这个是语法本身的模糊性,只有在语义解析之后才能解决,对吗?就是说如果对于一个自动机完全没有除了栈以外的记忆力要在看到有限的输入比如一个token如::要判断在IDENTIFIER之前的是以上三个的哪一个是不可能的,只有依靠先声明再使用的原则在之前定义过的变量表里找出来这个IDENTIFIER才能判断?这个本来是每个程序员从第一天开始的常识,但是我却要重新理解,这个是语法本身的问题还是解析算法的问题。
  4. 是不是所有的冲突都可以借助语义分析来解决呢?答案似乎是明显的,那么我们要怎么告诉GLR parser呢?bison似乎没有这个机制。。。
  5. 	

    剽窃自《洛神赋》

    翩若惊鸿婉游龙, 荣曜秋菊华春松。 髣髴隐隐蔽云月, 飘飖渺渺回雪风。 远望皎如霞蒸蔚, 迫察灼若芙蕖波。 秾纤得衷修合度, 肩若削成腰如酥。 延颈秀项皓质露, 芳泽无加铅华弗。 云髻峨峨修眉联, 丹唇朗朗皓齿鲜。 明眸善睐靥承权, 瑰姿艳逸静体闲。 柔情绰态媚于语, 奇服旷世骨应图。 披衣璀粲珥瑶碧, 翠首明珠耀妙躯。 文履轻裾微曳雾, 幽兰芳蔼步踟蹰。

    这里是曹植的《洛神赋》

    翩若惊鸿,婉若游龙。 荣曜秋菊,华茂春松。 髣髴兮若轻云之蔽月,飘飖兮若流风之回雪。 远而望之,皎若太阳升朝霞; 迫而察之,灼若芙蕖出渌波。 秾纤得衷,修短合度。 肩若削成,腰如约素。 延颈秀项,皓质呈露。 芳泽无加,铅华弗御。 云髻峨峨,修眉联娟。 丹唇外朗,皓齿内鲜,明眸善睐,靥辅承权。 瑰姿艳逸,仪静体闲。 柔情绰态,媚于语言。 奇服旷世,骨像应图。 披罗衣之璀粲兮,珥瑶碧之华琚。 戴金翠之首饰,缀明珠以耀躯。 践远游之文履,曳雾绡之轻裾。 微幽兰之芳蔼兮,步踟蹰于山隅。

十一月二十八日 等待变化等待机会

  1. 	

    改编自《明月几时有》

    明月把酒问青天, 天上今夕是何年。 琼楼玉宇乘风去, 清影高处不胜寒。 人间朱阁低绮户, 何事有恨别时圆。 阴晴圆缺有悲欢, 自古人愿共婵娟。

    附《明月几时有》原文

    明月几时有,把酒问青天 不知天上宫阙,今夕是何年 我欲乘风归去,唯恐琼楼玉宇 高处不胜寒,起舞弄清影 何似在人间,转朱阁 低绮户,照无眠 不应有恨,何事长向别时圆 人有悲欢离合,月有阴晴圆缺 此事古难全,但愿人长久 千里共婵娟,我欲乘风归去 唯恐琼楼玉宇,高处不胜寒 起舞弄清影,何似在人间 转朱阁,低绮户 照无眠,不应有恨 何事长向别时圆,人有悲欢离合 月有阴晴圆缺,此事古难全 但愿人长久,千里共婵娟
  2. bison有一个好处就是它的成熟度很高,有大量的输出与debug输出,这个是很难得的。比如文档里没有提到的trace的选项-T,它的可选参数是
    
      - ‘none’
      - ‘locations’
      - ‘scan’
      - ‘parse’
      - ‘automaton’
      - ‘bitsets’
      - ‘closure’
      - ‘grammar’
      - ‘resource’
      - ‘sets’
      - ‘muscles’
      - ‘tools’
      - ‘m4-early’
      - ‘m4’
      - ‘skeleton’
      - ‘time’
      - ‘ielr’
      - ‘cex’
      - ‘all’
    
  3. 真的是见了鬼了!你用肉眼能看出char32t里的underscore后面实际上是两个看不见的字符吗?我在mc查看hex才看到多了两个看不见字符C2AD 我不知道这个鬼字符是怎么来的也许是编码垃圾?最后只能使用grep来发现:grep -P -n "[\xAD]" bnf.txt,而奇怪的是我找不到另外那个字符0xC2我怀疑是UTF-8编码的前缀吧? 最后,我还是决定再过滤一遍,因为我的输入文件只允许所有的printable的ASCII,唯二不可见的字符被允许的是horizontal tab(0x09)form feed(0xA) 所以,这个是我的测试方法: grep -P -n "[\00-\x08\x0B-\x1F\x7F-\xFF]" bnf.txt 输入文件是多么的重要啊!一个早上就这样子糊里糊涂的浪费了,我还以为是bison的问题跟踪了半天,原来是我自己的文件的问题!
  4. 让我们来看看到底冲突是怎么样的,我决定看看第一个状态冲突
    State 0 conflicts: 41 shift/reduce, 3 reduce/reduce
    相对于41 shift/reduce来说3 reduce/reduce是比较清楚的,就是三个关于IDENTIFIER的问题:
    
    IDENTIFIER        [reduce using rule 57 (attribute-specifier-seq-opt)]
    IDENTIFIER        [reduce using rule 301 (explicit-specifier-opt)]
    IDENTIFIER        [reduce using rule 581 (nested-name-specifier-opt)]
    
    而它的本质原因是这三个nonterminal都是nullable的原因,这个即便是LL也需要做语法修改或者说即便手工写的编译器也是要去解决这个问题,换言之这个根本不是问题,因为它们本来就是一个可有可无的,我的理解是nullable的rule如果正好是empty它对于后面的解析是没有贡献的,我不妨把这些-opt的规则重新写一下来去除这些不必要的冲突:
    
    A: B-opt C-opt;
    可以修改为:
    A: B C;
    A: B;
    A: C;
    A:%empty; //这个有必要吗?
    
    经过修改我不再使用那种nullable的production,等于是把所有的optional的rule都展开,结果大部分的冲突转变为Reduce/Recude conflicts。
    
    cplusplus.y: warning: 327 shift/reduce conflicts [-Wconflicts-sr]
    cplusplus.y: warning: 2647 reduce/reduce conflicts [-Wconflicts-rr]
    
    这个也许是一个好消息?虽然总数不减少甚至增加,但是作为似乎更加的容易理解,至少我们现在可以看出来这个是需要sematics的解析才能消除冲突。
    
    IDENTIFIER      reduce using rule 145 (class-name)
    IDENTIFIER      [reduce using rule 297 (enum-name)]
    IDENTIFIER      [reduce using rule 1066 (template-name)]
    IDENTIFIER      [reduce using rule 1124 (typedef-name)]
    
    比如如果我们不知道IDENTIFIER代表的是什么我们是无法判断代表的是这四个的哪一个,听上去像是废话但这就是语义分析唯一能够解决的方式。
  5. 我虽然不能肯定,但是我感觉现在这些所谓的冲突是非常的自然的就是说即便你使用手写的Recursive Descent方式依然会遇到这些个需要抉择的地方,就是说使用bison产生的冲突很可能就是语言本身或者说语法本身的问题,并不是用手写的递归方程就看的更清楚,反而人类容易遗漏,而更主要的是随着c++委员会不断的修改语法添加新特点这个自动产生的机制更加的清新不容易遗漏。这个是我目前最最希望证实的!

十一月二十九日 等待变化等待机会

  1. 花了好长时间才理解bison怎么统计冲突的个数的,这个数字在不同地方有不同的解读
    
        "::"      reduce using rule 146 (class-name)
        "::"      [reduce using rule 271 (elaborated-type-specifier)]
        "::"      [reduce using rule 1125 (typedef-name)]
        ':'       reduce using rule 146 (class-name)
        ':'       [reduce using rule 271 (elaborated-type-specifier)]
        '{'       reduce using rule 146 (class-name)
        '{'       [reduce using rule 271 (elaborated-type-specifier)]
        "final"   reduce using rule 146 (class-name)
        "final"   [reduce using rule 271 (elaborated-type-specifier)]
        $default  reduce using rule 271 (elaborated-type-specifier)
    
    1. 一种是以一个状态内冲突的个数,也就是重复的规则次数就是5个Reduce/Reduce conflicts(注意加了[]的规则):
    2. 但是另一种视角是从lookahead的token的种类来看,实际上只有这四个token: "::", ':' ,'{' ,"final",所以,从这个角度看是4个token的REduce/Reduce conflicts
  2. 准备再分析一个冲突
    1. 首先这是一个Shift/Reduce conflicts,是关于terminal '(' 的冲突。
      
        325 explicit-specifier → "explicit" • '(' constant-expression ')'
        326                    | "explicit" •  [IDENTIFIER, '=', ';', "alignas", '(', ')', "...", '&', ',', "::", '[', ':', '{', "virtual", "class", "struct", "union", "noexcept", "&&", "operator", "const", "volatile", "friend", "typedef", "constexpr", "consteval", "constinit", "inline", "decltype", "->", "enum", "extern", '>', "explicit", '*', "requires", '~', "auto", "char", "char8_t", "char16_t", "char32_t", "wchar_t", "bool", "short", "int", "long", "signed", "unsigned", "float", "double", "void", "static", "thread_local", "mutable", "typename"]
      
          '('  shift, and go to state 228
      
          '('       [reduce using rule 326 (explicit-specifier)]
          $default  reduce using rule 326 (explicit-specifier)
      
    2. 这个是很好理解的因为explicit-specifier的语法原来就可以是只有一个"explicit"
      
        325 explicit-specifier → "explicit" '(' constant-expression ')'
        326                    | "explicit"
      
    3. 我现在想要明确为什么在这些lookahead里会有'('explicit-specifier出现在RHS的语法有:
      
      234 deduction-guide → explicit-specifier template-name '(' parameter-declaration-clause ')' "->" simple-template-id ';'
      235                 | template-name '(' parameter-declaration-clause ')' "->" simple-template-id ';'
      410 function-specifier → "virtual"
      411                    | explicit-specifier
      201 decl-specifier → storage-class-specifier
      202                | defining-type-specifier
      203                | function-specifier
      204                | "friend"
      205                | "typedef"
      206                | "constexpr"
      207                | "consteval"
      208                | "constinit"
      209                | "inline"
      
      我不想在找了,我相信bison计算的First/Follow set应该是正确的,总之在decl-specifier的Follow set应该是有'(',这个是无法简单的解决的,即便是Recursive Descent函数也是要做类似的lookahead token的判断。但是好处是在这么多的lookahead token里只有一个是需要解决的。所以,和GCC现在的意大利面条的代码相比来看也不会更糟糕。
  3. 改编自李清照的《寻梦令》

    寻寻觅觅冷清清, 凄凄惨惨复戚戚。 乍暖还寒难将息, 三杯淡酒饮不及。 晚来雁过伤心甚, 旧时相识再相逢。 独守到黑望窗台, 黄花憔悴谁堪摘? 梧桐细雨点点滴, 一个愁字哪及第?

    附李清照的《寻梦令》

    寻寻觅觅,冷冷清清,凄凄惨惨戚戚。 乍暖还寒时候,最难将息。 三杯两盏淡酒,怎敌他、晚来风急? 雁过也,正伤心,却是旧时相识。 满地黄花堆积,憔悴损,如今有谁堪摘? 守着窗儿,独自怎生得黑? 梧桐更兼细雨,到黄昏、点点滴滴。 这次第,怎一个愁字了得!
  4. 分析这些冲突是很宝贵的经验,因为我脑子突然就开窍了,明白了为什么LALR的lookahead的用意,就是因为即便我们使用LL算法也是以下一个nonterminal的First来判断,这个是非常的主观的猜测的投机,而LR(1)就是考虑了当前这个nonterminal的前一个symbol的连带影响因为人类朴实的区分歧义的办法也无非是看它之前的那个symbol是否允许当前这个nonterminal可能出现,怎么决定呢?当然是把之前的symbol的影响考虑进去,它的Follow必然是影响后继的主要因素。这个应该是手写的Recursive Descent函数的最普通的做法,而GCC里充斥了很多的尝试失败回滚样式的代码你可以说是LL算法的通病,但是为什么大家放到GLR里就不能接受尝试呢?大概早期GCC古老版本时候bison还不够稳定,debug的手段也不够多,面对成百上千的冲突的解决程序员宁愿自己手写毕竟因为bison自己也有可能有bug,这个是额外的开销和风险。但是今天如果我们可以对bison的质量有信心,GCC的大神们会改絃更张吗?也许还需要再多几年等到c++26引入更多的新feature才能吧?
  5. 看到视频提到一个有趣的应用,我就尝试一下。引出一大堆的问题。这是一个利用user-defined-literals的经典的案例,就是大家经常面对的分解ip地址的问题。但是我又一次被带偏了,因为我想不出有什么优雅的方法来分解字符串,我对于strtok是深恶痛绝的。大家能够想到的就是这些方法,但是首先我不想依赖第三方库哪怕是boost,那么除了自己手写find/find_first_of之类的就是使用非常炫目的split,结果我被耍了一通,因为这个例子居然只有GCC-12才能行!我一开始还以为是个别问题发现clang更加的不支持。后来我想利用演讲者提到的编译期检查的好处结果发现使用stringstream转化数字是不能支持constexpr的。
  6. 最后我还是决定动手写一下,也是学习的机会。这个时候才发现如果你要在编译期就抛出异常的话必须要使用consteval,单单声明为constexpr并不足以抛出异常,这个当然是常识只是我头脑中还没有这个概念,总算对于consteval有了切身的体会。其次对于user-defined-literal-operator再次加深印象,因为它的函数重载的参数应该是只能使用const char*和length,而我一步跨越想要使用string就完全失去了设计这个feature的意义。

十一月三十日 等待变化等待机会

  1. 有一个简单的事实是我才意识到的,bison的一部分代码是依靠bison自己产生的,有一点点像是GCC的bootstrap的意味,就是最原始的编译器编译出一个简陋的编译器功能很有限仅仅用来把真正的编译器的代码编译以便再进一步的编译出真正的编译器,这个和启动程序一样的境遇因此都叫做相似的名字bootstrap,原本就是如何拽着自己的鞋带把自己提升到空中的办法。这里的部分是所谓的Sematics Parser的部分,就是bison输入文件部分的解析的代码是另一个bison自己产生自己部分的代码的范例,那些action部分的解析本身就是一个bison应用的最好的场景,可是这么一来对于读码的人是场噩梦了,首先我不知道那个类似的输入的包含这些action语法的文件在哪里,比如这个文件parse-gram.c似乎明显的是一个自动产生的代码文件,可是它的输入是作为开发文件的一部分并没有提供在源码里面,就是说bison的开发过程还是有一部分是没有提供的。
  2. 现在再回过头来看这位前辈的说法才理解c语言的语法是相当的简单并且几乎没有任何的歧义的。因为的确的c语言唯一的冲突就是那个著名的dangling else
      Example: IF '(' expression ')' IF '(' expression ')' statement  ELSE statement
      Shift derivation
        selection_statement
        ↳ 195: IF '(' expression ')' statement
                                     ↳ 179: selection_statement
                                            ↳ 196: IF '(' expression ')' statement ELSE statement
      Example: IF '(' expression ')' IF '(' expression ')' statement  ELSE statement
      Reduce derivation
        selection_statement
        ↳ 196: IF '(' expression ')' statement                                       ELSE statement
                                     ↳ 179: selection_statement
                                            ↳ 195: IF '(' expression ')' statement
    
    这个是教科书里反复提及的语法本身定义的问题,而且无所谓对错,两种解释都是合法的,也就是歧义,那么人为选择一种的做法是提高两个rule的一个的优先级。是否所有的冲突都可以使用优先级来解决呢?也就是说一对或者更多的规则在一个场景里的优先级是否会在另一个场景产生矛盾?我觉得似乎把这些规则优先级排序就不可能产生环状的冲突,但是这个并非是语法制定者希望的,归根到底语法是为语义服务的,制定语法的人是基于语义来定义的,当然如果语义矛盾了那就是语言本身的问题是没救了,不过这个都不可能是问题。唯一的问题是冲突解决者来看怎么办的问题,无法认定这个优先级是否是正确的做法,或者是否必要,也许从上下文自然能够判断出来的根本不是问题,而人为规定的优先级也许就错了。总之,面对成百上千的冲突究竟有那些是可以自动解决的?很明显的GLR就是自动解决的办法,它的方式和手写的罗辑也很像,无非是试错,可是问题是这个过程的代码深深的埋藏在自动产生的代码里,我根本看不明白其中的逻辑,这个是令所有普通程序员望而却步的。当然普通程序员压根不需要关心这个机制因为它是自动的,问题是也没有人能够轻易的修改它了。
  3. 我有一点点意外是看起来bsion的更新很活跃,我无意中发现即便3.81和3.82版本的文档都有不少的改动,尤其是关于GLR部分,我应该集中精力认真做一遍例子来理解。看上去bison的大部分更新都是和autoconf相关的,这位大神Akim Demaille是autoconf的专家,那么能不能说主要要用这个工具去产生GLR的代码呢?看样子是吧?那么这个就很难了,因为data/skeletons/glr2.cc这样子的代码是用这些m4的宏来写那实在是天书一样。不过我也是糊里糊涂,我自己产生的代码里就包含了GLR算法的逻辑,我需要这么关心它是怎么产生的吗?如果我能看懂哪一部分不就够了吗? 头脑昏昏沉沉的。
  4. 我想暂时解除预处理器的烦恼,这个开关老是忘记,就是使用GCC的-E -P,其中-P禁止输出讨厌的行号

十二月一日 等待变化等待机会

  1. 偶然发现手机上的浏览器打开纯粹的text文件的时候实际上无法确定它的编码,这个是自然的因为一般在UTF-8编码文件都有开头的代表endian的开头字符,但是我的这个文件没有,这个是我当初整理的所谓的《中华字经》。网上看到的版本都不怎么全。
  2. 看bison的例子注意到一个问题,就是产生的.c文件作为make的中间文件最后被删除了,这个是Makefile的很机巧的写法,对于工程是很好的防止一个简单的例子的过多的中间文件分散了读者的视听,但是我是要研究它产生的代码的那么要怎么防止make删除中间文件呢?这里有好几个不同的答案,我要尝试才知道哪一个更加的适合我:
    1. 这个方法似乎是最简单扼要的,就是添加一行.SECONDARY: 作者解释了原因
      .SECONDARY with no prerequisites causes all targets to be treated as secondary (i.e., no target is removed because it is considered intermediate).
      我一开始感到有点太宽泛因为所有的中间文件都被保留未必是最好的选择,后来看到还有这句话:
      The targets which .SECONDARY depends on are treated as intermediate files, except that they are never automatically deleted.
      但是当我使用wild来表达我想保留所有的.c文件的时候是没有用的,只能使用具体的文件名。所以这个实际上和第二个做法本质上是一样的。就是中间文件名一定要出现在规则里就不被当作中间文件了,当然这样子的特殊规则.SECONDARY意义也不大了。但是这个确实是正解。 而作者的最后解释才让我大开眼界:
      Why is this better than .PRECIOUS? That causes files to be retained even if their recipe fails when using .DELETE_ON_ERROR. The latter is important to avoid failing recipes leaving behind bad outputs that are then treated as current by subsequent make invocations. IMO, you always want .DELETE_ON_ERROR, but .PRECIOUS breaks it.
      我以后要主动使用 .DELETE_ON_ERROR,隐隐约约以前也被这个问题困扰过,记不清是什么也许是我添加了脚本或者复杂的链接过程,失败有些残存文集没有删除导致下一次链接就能通过之类的吧?总之,出错就清空是很好的手段!
    2. 这个就是之前说的彻底改变中间文件的属性把加在某个规则里:
      a file cannot be intermediate if it is mentioned in the makefile as a target or prerequisite.
      我要承认作者的方法不是很好,因为我没有成功,原因并非他的办法不对而是我要怎么样子才能获得一个数组的字符串呢?因为他的意思是要利用bash的字符串前后缀的替换: all_pps: $(ALL_OBJECTS:.o=.pp) 但是这个方法不行,因为我也不知道要怎么定义 $(ALL_OBJECTS因为它作为一个长字符串显然不行,而作为数组我不知道要怎么做到?我找到了make的手册 的确我可以使用objects = foo.o bar.o baz.o来定义一个数组,然后通过$(objects:.o=.c)得到了所有的.c文件,但是作为一个目标却不成立,不知道为什么这个定义的无用的规则只能保证第一个.c文件不被删除。所以,不好。 它引用的make的手册里详细阐述了它的根本原因非常的棒,因为它直接就是以yacc/bison作为例子的。下面这个我却理解错了,这个恰恰是怎么样把一个在规则里提到的文件作为中间文件来删除的!
      Ordinarily, a file cannot be intermediate if it is mentioned in the makefile as a target or prerequisite. However, you can explicitly mark a file as intermediate by listing it as a prerequisite of the special target .INTERMEDIATE. This takes effect even if the file is mentioned explicitly in some other way.
      我做了一个实验把.o文件放在了.INTERMEDIATE的依赖的目标,当然是完整的文件名字,就能够起到最后删除的效果!这个有时候也是有用的一个好东西。
    3. 这里还有一个使用.PRECIOUS的做法,但是我感觉就是第一个作者强调的这个是误用或者说是overkill,有很多的副作用。还是.SECONDARY最好。
  3. 这个是一个非常好的shell programming的语法网站,可惜我没有时间。
  4. 我无耻的从这里剽窃了这些表格,因为这些bash-string-operation很好但是我记不住,每次都要查找手册。
  5. 整个早上我原本是要看看怎么处理flex的输入文件的产生的问题,结果严重偏离了十万八千里,都是因为一个中间文件被自动删除的问题,引出了这么多的风波。累死了。

十二月二日 等待变化等待机会

  1. 一个如此简单的道理我却花了这么旧才体会出来:在bison的输入文件里的那些terminal token,你虽然可以在其中使用string literal来直接表达BNF,但是归根结底你还是要为此付出代价,就是说根本的一点bison/yacc只能够通过yylex返回的一个数字来工作,那么至于你是自己在.y文件里自己实现一个flex的功能定义一个yylex还是交给flex来产生那是你自己的选择。总而言之,不要被bison输入文件里的字符串给欺骗了因为你的工作一点都省不了,你照样要定义这个字符串的%token数值,而这个形式定义的字符串作为alias仅仅是在报错的时候的多语言支持里的显示功能,最终你要在flex的输入文件里定义相同的token的对应的字符串。换言之,所谓的bison产生的编译器的最终代码就是一些状态机的描述表格,表格里只有数字,那么你期望能够使用这些状态数字打印之前的规则字符串是不可能的,因为不同的编译器其实就是不同的状态机,如果抛开语言你根本不知道不同编译器究竟有什么区别。
  2. 改编自李清照《行香子·七夕》

    草际鸣蛩落梧桐, 人间天上皆愁浓。 云阶月地关千重, 浮沉去来不相逢。 星桥鹊驾经年见, 离情别恨怎能穷? 牵牛织女叹离中, 儿女哭泣晴雨风。

    李清照《行香子·七夕》

    草际鸣蛩,惊落梧桐,正人间、天上愁浓。 云阶月地,关锁千重。 纵浮槎来,浮槎去,不相逢。 星桥鹊驾,经年才见,想离情、别恨难穷。 牵牛织女,莫是离中。 甚霎儿晴,霎儿雨,霎儿风。
  3. 改编自赵明诚(李清照丈夫)《凤凰台上忆吹箫》

    香冷金猊翻红浪, 起来独自梳头忙。 病酒悲秋新来瘦, 离怀别苦说还休, 阳关万遍去难留, 武陵人远锁秦楼。 楼前流水应念我, 终日凝眸添新愁。

    赵明诚(李清照丈夫)《凤凰台上忆吹箫》

    香冷金猊,被翻红浪,起来慵自梳头。 任宝奁尘满,日上帘钩。 生怕离怀别苦,多少事、欲说还休。 新来瘦,非干病酒,不是悲秋。 休休,这回去也,千万遍《阳关》,也则难留。 念武陵人远,烟锁秦楼。 惟有楼前流水,应念我、终日凝眸。 凝眸处,从今又添,一段新愁。

十二月三日 等待变化等待机会

  1. bison里对于单个字符的token就是它自己的ASCII的数值,所以,当然可以直接就使用,那么对于多个字符还是要映射成一个token数值,这个最好是flex去做,但是保持彼此间的通信就需要在bison里定义为enum来在头文件里分享给flex,这个是最最基本的也是最凡人的部分,比如这么多的操作符要取一个别名真的不容易,尤其很多的英文名字我都念不上来,于是找到libcpp里的定义,这个是我总是忘记的地方:cpplib.h,我之前已经反反复复的做笔记了还是忘记! 我在libcpp里看到了ISO-646里定义的那些操作符的字符版本的代码,虽然不是很确定但是我感觉这个定义在c语言委员会标准里的东西应该是在预处理器里实现的,所以,我决定它不应该出现在parser的token里。
  2. 我又再一次遇到这个module实现上的烦人的东西,就是import-keyword, module-keyword,export-keyword 它们在预处理上是属于所谓的keyword,也就是说有着不同于一般的identifier的规则,可是这个实际上因为它和一般传统意义上的c语言的keyword截然不同,等于是要预处理器为之单独开放一个规则,否则一般的c语言的identifier也是不能这样识别它的。这个真的是很头疼。

十二月四日 等待变化等待机会

  1. 为什么内存越界?
    
    std::transform(str.begin(), str.end(), strTarget.begin(), [](auto c){
    	return std::tolower(c, std::locale("C"));
    });
    
    假如你想把str里的字符转为大写并写进目的地字符串strTarget,这样子是会内存越界的,很容易在gdb里看到strTarget的内部缓存里的确有已经转为大写的来自于str的数据可是目的地字符串的长度依然没有变化,很显然的原来的例子是目的地就是源头字串让人有了误解其实例子解说的很明确是要使用back_inserter的,我其实脑子里一直认定必须要要back_inserter,但是被误导了以为transform有做什么神奇的操作能够让操作符++神奇的实现目的地的递增。真的是幼稚。
  2. flex自己也有regex的escape character的规则:
    *,[,],(,)," ,\,{,}
    我一开始遇到 warning, rule cannot be matched的错误还以为是以上的escape的问题,后来才理解这个是规则的问题。也就是说有的规则之间是互相冲突的,其实很容易理解flex是一个类似RMP之类的算法,那么对于否定的集合是有问题的是无法区分的。

十二月五日 等待变化等待机会

  1. 看来我对于这个简单的flex是有误解的,它绝非是那么简单的就是一个字符串比较和regex表达式,否则谁不能随便手写一个脚本呢?它是有类似规则检查的,这个原来课堂上都讲过的,字符串也是讲究最长的匹配原则,这个算是解决歧义的一个默认原则吧?那么这个就导致凡是否定的规则不好用了,我在哪里看到过说表达式是否定的很麻烦似乎不可行,因为否定并不是简单的逻辑。。。总之,对于像string-literal里的raw-string这样子的超级语法只是用来说明的,你是无法把简单的转为规则的。换言之,我决定把
    
    encoding-prefixopt R raw-string
    
    raw-string作为scanner/lexer的一个逻辑代码来实现返回一个完整的terminal token:RAW_STRING,或者这个就是一个典型的例子是parser回到lexer里动态修改表创建一个delimiter-string的token之类的,总之这个是实现的细节。我完全没有什么意愿与兴趣。
  2. 总算是编译了一个框架,这个是目前可行的一个原始语法以及处理文件:
    1. bnf.txt 原始语法文件
    2. bnf.cpp: 语法处理并产生bison/flex的输入文件
    3. Makefile: make文件
    以下则是产生的中间文件,都是以上产生的,不过还是很有用的可以作为分析改造的基础:
    1. cplusplus.y: bison输入文件
    2. scanner.l: flex输入文件
    3. cpluscplus.c: bison产生的parser程序
    4. scanner.c: flex产生的lexer程序
    5. cplusplus.html: bison产生的语法分析文件(非常巨大:14M!非常有用!)
  3. 本来想把本地的git上传,但是要额外修改以前的程序所以还是简单的对于以上的文件做一个备份吧?
  4. 在寒风中我再次揣摩这个冲突的意义
    
    if_stmt:
    	  "if" expr "then" stmt
    	| "if" expr "then" stmt "else" stmt
    ;
    
    在我看来bison的所谓的正确选择根本是唯一的选择,因为选择shift就是选择更长的可能,为什么我们要选择更长的可能呢?因为语法的作者的倾向性已经告诉我们这个是语法的本意,可以这么改写
    
    else_stmt: 
    	%empty
    	| "else" stmt
    	;
    if_stmt:
    	"if" expr "then" stmt else_stmt
    	;	
    
    这里的else_stmt就相当于在c++语法里众多的XXXopt,所以,不妨写成else_stmtopt,这样子就很清楚了,所谓的dangling else的经典例子实际上是一个optional的问题,结果我现在才肯定就是通过改写这些optional成为两个并列的规则根本对于解决甚至改变冲突毫无帮助,甚至冲突的类型也完全没有变化,就是说上述改写产生的冲突是一样的,我之前的印象一定是被误导了。 但是想一下这个"if" expr "then" "if" expr "then" stmt • "else" stmt冲突只是其中的一个特殊的情况,更加普通的情况根本没有冲突,比如: "if" expr "then" stmt • "else" stmt只要这个stmt不是一个if-stmt我们完全没有可能reduce因为后面的"else"就不符合语法了,换言之,在我们能够reduce的时候一定要保证后面的"else" stmt成立才行,什么时候"else"reduce后的Follow应该就是答案。我觉得我在公园里已经想的很清楚了,结果现在又糊涂了。 06.12.2021我想说的是reduce并不总是能够得到合法的结果仅仅是一些边缘的例子,而shift总是能够得到一个合法的,因为语法的作者之所以要加入这个optional的元素就是要表达他希望这些optional要尽可能的被使用,否则为什么已经可以reduce了还要给你optional能够做shift呢?作者的倾向性不是很明显吗?最核心的问题是是否我们做shift的时候不至于错误的把shift得到的最终结果把原本可以reduce得到的结果弄丢了?感觉我完全误解了,这里的歧义根本和else_stmt的Follow无关。
  5. 对于bison爆出的错误error: start symbol XXX does not derive any sentence的原因是可能有无限递归造成没有结果,很可能是起始的XXX没有一个terminal来结束递归。

十二月六日 等待变化等待机会

  1. 其实人的理解是非常复杂的过程,往往一些似是而非的概念掩盖了误解,对于BNF语法中的string literal的理解我就暴露了这个问题,从本质上说bison创建的状态机就是一堆的数字表格,string literal是没有其位置的,除非你把它们定义为flex能够理解的token,或者你自己实现一个yylex函数来返回这些token的数字代号,这里我的理解还是有问题,本质上说你不能把这些string literal想像成什么nterm,因为它们一定是terminal,因为bison似乎不允许给nterm起alias,这个看样子是合理的,本身non-terminal/nterm就是一个代号,名字起得不好就改掉好了不会影响到其他人,为什么要再多此一举给它起代号呢?这个是彻底的误解。只有terminal才需要代号。换言之你在BNF语法中使用了IF作为一个terminal,那么为了显示的好看,你可以定义它的alias像这样子:%token IF "if"于是在真实的语法打印过程中这个alias就出现了,但是这个完全不代表bison会帮你去实现本该是flex的工作,这个是我一开始的误解,我以为bison可以天然的使用string literal作为一个terminal然后并直接实现flex的tokenize的工作,这个根本不可能而且没有必要,这个不是它的工作,它的工作已经够复杂了,而flex也决不仅仅是比较字符串那么简单的实现,是一个优化的搜索过程,仿佛我以前课堂上学的最长字符串比对的表。
  2. 总而言之,这个检查-Wdangling-alias很有用我决定把它加入我的程序中。
  3. 我现在发现我遇到的很多问题都是flex的问题,本来想要避免的次要方面全都成了主要问题,看样子只能快速的学习一下子。还是很有意思的。搜索了好半天发现GNU对于这个老掉牙的项目基本上不再host了,我只好自己保存一个pdf的flex手册

十二月七日 等待变化等待机会

  1. lex是不同的语法,这个是不能混作一谈的。9个步骤复杂到以至于需要单独的程序,问题是后面有不少的语法有可能需要反过来干预。我为什么要关心这个呢?不是说好了让libcpp来处理的吗?所以,我的脑子是混乱的只有这个是lex和parser的分界线,在parser眼中只有这么四个:
    
    token:
    	identifier
    	keyword
    	literal
    	operator-or-punctuator 
    
    其他都是分隔符。其中literal的处理是比较复杂的,如果能够处理好的话,那么除去后面这三种就是identifier。所以,关键的是literal尤其是raw-string是非常的困难。 这里的conditional escape sequence让我非常的费解,究竟是什么意思?显然不是用户定义的,难道是编译器自定义的?
  2. 这个帖子当然是很好的,回答了我很多的从来没有用到过的问题,可是依然没有回答什么是conditional-escape-sequence

    Control characters:

    (Hex codes assume an ASCII-compatible character encoding.)

    • \a = \x07 = alert (bell)
    • \b = \x08 = backspace
    • \t = \x09 = horizonal tab
    • \n = \x0A = newline (or line feed)
    • \v = \x0B = vertical tab
    • \f = \x0C = form feed
    • \r = \x0D = carriage return
    • \e = \x1B = escape (non-standard GCC extension)

    Punctuation characters:

    • \" = quotation mark (backslash not required for '"')
    • \' = apostrophe (backslash not required for "'")
    • \? = question mark (used to avoid trigraphs)
    • \\ = backslash

    Numeric character references:

    • \ + up to 3 octal digits
    • \x + any number of hex digits
    • \u + 4 hex digits (Unicode BMP, new in C++11)
    • \U + 8 hex digits (Unicode astral planes, new in C++11)

    \0 = \00 = \000 = octal ecape for null character

    If you do want an actual digit character after a \0, then yes, I recommend string concatenation. Note that the whitespace between the parts of the literal is optional, so you can write "\0""0".

    我现在想这个所谓的conditional的意思就是不一定有支持,是否可以忽略呢?比如说我随便写一个\j那么GCC就会爆出警告说是warning: unknown escape sequence: '\j'这个不算是错误但是应该是可以忽略的吧?这个忽略是很容易的,我是这么做的: \\[^0-7'"\?\\Uuxabfnrtv] { unput(yytext[1]);}
  3. 很多时候语法的问题很难解决就把这个推给了lexer来解决,比如这个balanced-token这个难道不是应该语法来解决的吗?可是如果lexer能够神奇的tokenize那么它就不是一个复杂的问题了。因为粗粗一看这个语法是无可挑剔的,但是你实际要想它怎么实现的就明白这个是偷懒的做法基本上告诉你一个大原则,具体实现是一个很头疼的问题,因为这个是需要上下文才能解决的问题,这个所谓的any token other than a parenthesis, a bracket, or a brace完全是一言以蔽之的操作,这个包含了多少可能性呢?所以,你才能理解为什么委员会说语法是描述性的,只有指导意义。只有这个时候我才想念以前看到的大侠Fog的语法修改,我这个时候已经有了一点点的实践体会了现在回头看Fog大神的语法我下载的是他工程里的代码和网络上公开的版本可能有些不同吧?这个是bison产生的html的版本。比较学习吧!最难能可贵的是大神已经消除了冲突不管他是否是基于比较早期的c++语法,这都是了不起的成就。我以前看不明白,现在应该能够看懂更多了。
  4. 那么这个前端是否是用手写的函数呢?我想是吧?
    The front end translates source programs into a high-level, tree-structured, in-memory intermediate language. The intermediate language preserves a great deal of source information (e.g., line numbers, column numbers, original types, original names), which is helpful in generating symbolic debugging information as well as in source analysis and transformation applications. Implicit and overloaded operations in the source program are made explicit in the intermediate language, but constructs are not otherwise added, removed, or reordered. The intermediate language is not machine dependent (e.g., it does not specify registers or dictate the layout of stack frames). The front end can optionally generate raw cross-reference information, which can be used as a basis for building source browsing tools.
  5. 每次都要查这个Makefile的宏,摘录这个吧:
    • $@: The filename representing the target.

    • $%: The filename element of an archive member specification.

    • $<: The filename of the first prerequisite.

    • $?: The names of all prerequisites that are newer than the target, separated by spaces.

    • $^: The filenames of all the prerequisites, separated by spaces. This list has duplicate filenames removed since for most uses, such as compiling, copying, etc., duplicates are not wanted.

    • $+: Similar to $^, this is the names of all the prerequisites separated by spaces, except that $+ includes duplicates. This variable was created for specific situations such as arguments to linkers where duplicate values have meaning.

    • $*: The stem of the target filename. A stem is typically a filename without its suffix. Its use outside of pattern rules is discouraged.

    In addition, each of the above variables has two variants for compatibility with other makes. One variant returns only the directory portion of the value. This is indicated by appending a “D” to the symbol, $(@D), $(<D), etc. The other variant returns only the file portion of the value. This is indicated by appending an “F” to the symbol, $(@F), $(<F), etc. Note that these variant names are more than one character long and so must be enclosed in parentheses. GNU make provides a more readable alternative with the dir and notdir functions.

  6. 大神的注解里说明了如何解决冲突的,这个是我最需要的,但是也是最难的,我首先要补课:
    1. 大师首先定义了一个虚拟的操作符%nonassoc SHIFT_THERE,而这里的%nonassoc
      Directive: %nonassoc
      Declare a terminal symbol (token kind name) that is nonassociative. Using it in a way that would be associative is a syntax error.
      就是说这个没有结合律
    2. 所谓的associative是Operator Precedence的一种
      Use the %left, %right, %nonassoc, or %precedence declaration to declare a token and specify its precedence and associativity, all at once. These are called precedence declarations.
    3. 就是说%nonassoc declares that it is a syntax error to find the same operator twice “in a row”.
      
      selection_statement:	
      	IF '(' condition ')' looping_statement    %prec SHIFT_THERE
      |	IF '(' condition ')' looping_statement ELSE looping_statement
      	
      这里我看不懂就是说bison的标准做法是这样子
      
      if e1 then if  e2 then s1 • else s2
      
      The conflict involves the reduction of the rule ‘IF expr THEN stmt’, which precedence is by default that of its last token (THEN), and the shifting of the token ELSE. The usual disambiguation (attach the else to the closest if), shifting must be preferred, i.e., the precedence of ELSE must be higher than that of THEN. But neither is expected to be involved in an associativity related conflict, which can be specified as follows.
      
      %precedence THEN
      %precedence ELSE
      
      可是问题是以上是Pascal风格的if-then-else,在c语言里没有then,那么是否大师就创造了一个虚拟的操作符SHIFT_THERE呢?
    4. 所以,大师使用的是另一个玄奥的东西:Context-Dependent Precedence
      The %prec modifier declares the precedence of a particular rule by specifying a terminal symbol whose precedence should be used for that rule. It’s not necessary for that symbol to appear otherwise in the rule.
      这说明什么呢?我的理解就是这个虚拟的token根本不一定需要出现在那里,为什么这么说?在大师的语法了这个虚拟的SHIFT_THERE根本就是不定义的,最后bison把它翻译成了terminal,但是并没有任何定义,也就是说lexer根本不会返回的terminal token,行同虚设。在bison产生的冲突解决是这么说的:
      
        207 selection_statement → IF '(' condition ')' looping_statement •  [error, '+', '-', '*', '&', '~', '!', '[', '{', '}', '(', ';', DEC, INC, SCOPE, BOOL, CHAR, DOUBLE, FLOAT, INT, LONG, SHORT, SIGNED, UNSIGNED, VOID, WCHAR_T, CLASS, ENUM, NAMESPACE, STRUCT, TYPENAME, UNION, CONST, VOLATILE, AUTO, EXPLICIT, EXTERN, FRIEND, INLINE, MUTABLE, REGISTER, STATIC, TEMPLATE, TYPEDEF, USING, VIRTUAL, ASM, BREAK, CASE, CONST_CAST, CONTINUE, DEFAULT, DELETE, DO, DYNAMIC_CAST, FALSE, FOR, GOTO, IF, NEW, OPERATOR, REINTERPRET_CAST, RETURN, SIZEOF, STATIC_CAST, SWITCH, THIS, THROW, TRUE, TRY, TYPEID, WHILE, CharacterLiteral, FloatingLiteral, Identifier, IntegerLiteral, StringLiteral, '#']
        208                     | IF '(' condition ')' looping_statement • ELSE looping_statement
      
          ELSE  shift, and go to state 894
      
          $default  reduce using rule 207 (selection_statement)
      
          Conflict between rule 207 and token ELSE resolved as shift (SHIFT_THERE < ELSE).
      
    5. 这个实在是太难理解了,我感觉看不懂这个就无法理解:
      The associativity of an operator op determines how repeated uses of the operator nest: whether ‘x op y op z’ is parsed by grouping x with y first or by grouping y with z first. %left specifies left-associativity (grouping x with y first) and %right specifies right-associativity (grouping y with z first). %nonassoc specifies no associativity, which means that ‘x op y op z’ is considered a syntax error.

      %precedence gives only precedence to the symbols, and defines no associativity at all. Use this to define precedence only, and leave any potential conflict due to associativity enabled.

      很难懂,我丝毫看不出来大师的定义是什么意思,%nonassoc对于这个虚拟的SHIFT_THERE有什么用呢?它怎么会出现的重复呢?
    看的是我一头雾水,睡觉吧?

十二月八日 等待变化等待机会

  1. 因为之前意识到我表面上消除nullalble symbol并不能帮助减少冲突,我决定回复语法原有的-opt,结果显示冲突变成:
    
    cplusplus.y: warning: 1976 shift/reduce conflicts [-Wconflicts-sr]
    cplusplus.y: warning: 1449 reduce/reduce conflicts [-Wconflicts-rr]
    
    我认为原本就是Shift/Reduce的冲突之前被转化为Reduce/Reduce的冲突,总的来说并没有变化,它们总数的差异应该是统计的角度不同造成的吧?总之,我现在要学习大师使用虚拟的优先级来首先消除dangling-else的冲突,看看效果如何?完全不起作用,仔细再看大师使用的是%term和%type我使用的是%token这中间的差别是什么?需要再学习。
  2. 我觉得我的脑子混乱了,我当初根本就没打算解决这几千个冲突准备让GLR去对付它,为什么现在又糊涂了呢?我唯一需要借鉴的是如何做好基本的lexer和parser之间的衔接部分。我还是再想想吧。
  3. 通过大师的工程来学习,首先是编译,我对于什么时候要定义yywrap依然不太懂,只知道这个是多个文件输入的逻辑,目前就直接用链接-ll来混过去。重点是看大师如何实现一部分在lexer与paser边缘游移的语法,比如到底escape-sequence是一个lexer的token还是语法来定义它?我看到大师是用lexer但是配套的代码是核心关键,我需要打起精神才能理解。先到这里吧。

十二月九日 等待变化等待机会

  1. 大师的程序需要debug,因为有很多的基本原理我还不清楚,在开始高级的flex/bison的功能以前需要学习基本的常识:
    1. flex产生的核心就是yylex所以在我看到链接-fl说找不到yylex的定义的时候我就彻底糊涂了,我产生的代码里就是定义的yylex,为什么我要去链接flex库呢?仅仅是为了我不想定义yywrap其实,它通常就是返回1代表我不打算使用多个输入文件,于是,我自己在debug的程序里定义一个不要链接flex!
    2. 另一个解决办法是在.l里定义%option noyywrap
    3. 我为了确保大师的.l的正确性,就先debug一个简单的lexer程序: 当然其他还有一些配套的大师定义在parser部分的变量。这里我对于yylex的理解出了偏差,它本身是一个成功与失败的函数,你只能判断什么时候文件结束,必须要在成功的时候通过yytext来获取输入。
    4. 编译flex的时候使用-d不过我也不确定这个的意义,我自己添加了ECHO;但是不知道是否有用因为大多数的flex的action只能是一个,除了那个吃掉所有空白字符的无action规则我才可以添加,可是有什么意义呢?
    5. 又一次遇到基本的regex的问题: ^.*\n { LEX_SAVE_LINE(yytext, yyleng); REJECT; } 大师的第一个规则就让我摸不着头脑,只好再次温习regex101:
      
      The single character '.' when used outside of a character set will match any single character except:
      • The NULL character when the flag match_not_dot_null is passed to the matching algorithms.
      • The newline character when the flag match_not_dot_newline is passed to the matching algorithms.	
      	
      所以,最通常的perl版本的regex是不认换行符的,所以,大师是说如果我先读一行,那么我就用REJECT;把它再放回去,因为
      REJECT
      directs the scanner to proceed on to the “second best” rule which matched the input (or a prefix of the input).
      这个是什么意思呢?就是说大师把它放在第一位让我们先做一个准备因为它会把yytext and yyleng set up appropriately,然后逼迫flex去寻找一个更的规则,这个就是一个很高级的机巧,这也给我之前面对R-string感到不可能的情况有了一线曙光。先勇规则得到全部,比如对付escape-string的话,我们先找到它的起始和结束再去对付它。这些flex的高级奇技淫巧我并不想花精力也不可能,但是它是绕不过去的坎,因为没有lexer的parser是无法进行基本的检验的。
    也就是说flex部分是正常的,那么问题就是处在bison部分的规则了,这个是一个巨大的挑战。我需要饱餐战饭再说。

十二月十日 等待变化等待机会

  1. 实验大师的程序,我一直困惑于为什么陷入死循环,搞了半天才明白大师使用了所谓的error-recovery,就是bison的error
    You can define how to recover from a syntax error by writing rules to recognize the special token error. This is a terminal symbol that is always defined (you need not declare it) and reserved for error handling. The Bison parser generates an error token whenever a syntax error happens; if you have provided a rule to recognize this token in the current context, the parse can continue.
    而这个例子是比较难懂的:
    
    stmts:
    	%empty
    	| stmts  '\n'
    	| stmts exp '\n'
    	| stmts error '\n'
    The fourth rule in this example says that an error followed by a newline makes a valid addition to any stmts.
    
    每个人都应该问这个问题:
    What happens if a syntax error occurs in the middle of an exp? The error recovery rule, interpreted strictly, applies to the precise sequence of a stmts, an error and a newline. If an error occurs in the middle of an exp, there will probably be some additional tokens and subexpressions on the stack after the last stmts, and there will be tokens to read before the next newline. So the rule is not applicable in the ordinary way.
    后面还有技巧去做相反的事情,我的脑子接受不了了。总而言之,我最后只好在大师的一个backtracking的函数里使用YYABORT强行终止才能禁止这个无限循环: advance_search: error { YYABORT; } 这期间我被我的Makefile里遗漏的规则折磨的头疼,因为一直没有完全编译搞得我发疯。头疼。
  2. 此外我感觉设定bison的debug代码并不能自动启动它,因为你要在你的程序代码里设定yydebug才行,否则你仅仅能够看到代码里有事先生成的debug语句,还是要开启才行。而我对于使用宏-Dparse.trace还是声明%define parse.trace感到困惑,而-t到底是什么意思?为什么POSIX-compatible和bison-extension应该是矛盾的,总之我看的手册解释让我更糊涂。
  3. 大师的例子第一次走通了备份一下
  4. 很多时候我都会把flex和bison的东西弄混了,因为两者之间是紧密勾连的,在flex返回一个token的时候,往往要决定语义的动作,于是YYSTYPE就是应用的场景了,因为默认的所有元素可以是integer,那么对于语法分析肯定是不够的,bison倾向于使用%define api.value.type {your_type}或者另一个比较古老的做法是在prologue里使用宏#define YYSTYPE your_type,但是对于多种类型我以为使用type tag可能是最方便的。不过这些是以后的事情,我其实最感兴趣的是大师的flex部分,而且我觉得尽管c++已经发展了几十年,但是这个二十几年前的这个部分几乎是可以照搬的,因为这个部分几乎没有变化,可能最大的变化就是r-string了。
  5. 所以,我还是要把注意力集中到flex部分。大师的测试无法对于一个简单的main函数识别完全是语义分析中怎么分辨函数名和类型名的问题,大师使用的search思路是完全正确的,问题仅仅是怎么实现,而这个是语义解析的巨大的问题,根本不是我要考虑的问题,当下的重点是怎么把tokenize的部分解决以便跑起来。感觉头脑越来越混乱。
  6. 如果有一条路从没有看到有人走过,这是否说明它是死路一条呢?当然是的,大路旁的李子树上的李子没有人摘来吃当然是因为它是酸的。这个是白痴都能够明白的道理。这里的区别仅仅是improbable和impossible的差别
  7. flex和bison的分界线在哪里?比如
    
    string-literal:
    	encoding-prefixopt " s-char-sequenceopt "
    	encoding-prefixopt R raw-string
    
    对于parser来说它需要知道这两个string-literal的区别吗?它们究竟对于语义分析有任何区别吗?所以,从token的角度来看它们就应该是一个STRING_LITERAL这么一个token,至于说raw-string是一个多么复杂的机制完全是flex部分如何实现的细节。同样的
    
    literal:
    	integer-literal
    	|	character-literal
    	|	floating-point-literal
    	|	string-literal
    	|	boolean-literal
    	|	pointer-literal
    	|	user-defined-literal
    	;
    
    这个语法根本是lexer的部分,根本不应该出现在c++的parser的语法部分!
  8. 我以前没有意识到这个基本的问题,所以看到The BNF non-terminal for a C++ preprocessing file就以为这些之外就都应该是The BNF non-terminal for a C++ translation unit,这里不是非此即彼的关系,究竟哪些要放在flex来处理更容易是一个需要实践才明白的道理。
  9. 这个integer-literal其实是没有什么意义存在的,只是方便说明,不论是lexer还是parser都不需要这个。而新版的语法user-defined-integer-literal增加了binary-literal
  10. 这一部分工作量是挺大的。之前我其实一直不明白其中的道理,直到今天才有些清楚,并不是说语法不在预处理部分就应该都放在bison的输入里,它们有这些是属于flex的。我是结合看大师的flex的输入文件才理解的,看来我是非常的愚笨的之前完全想不清楚只是机械的把语法拷贝,我根本没有超过实际操作以外的预见能力。看来我只配做一个foot soldier。

十二月十一日 等待变化等待机会

  1. 我内心深处其实对于预处理和lexer的过程总是很模糊,这两个是完全不同的范畴,这个就解释了我为什么当初对于写的清清楚楚的lex语法视而不见?诚然这里包含了所谓的预处理的token,但是两者是完全不同的逻辑,另一个反映我模糊认识的实例九是对于module的转化为module-keywords的问题,这里不是在预处理做的,我一开始还是不明白一定要使用cpp去看import/export/module是否被转为xxx-keyword才相信,这个是对于用户透明的过程发生在lexer里,我又一次回忆起在不知道哪里看到的过渡语法是在预处理实现module的模糊概念,这个可能是早期对于简单理解module的功能类似include才可能吧?对于链接这个是无能为力的,而且微软还对于import/export有扩展为了避免这些啰嗦事才改为keyword的吧?总之,我现在才意识到应该把translation-unit和lex部分的语法分开处理,两者是相互依存引用的,但是应该要分开处理,因为前者产生bison的输入,后者产生flex的输入,逻辑完全不一样,这么一个简单的道理我花了一个月才明白。
  2. 在所谓的preprocessing-operator部分里我感到有些迷茫,这些所谓的Digraph究竟有什么存在的价值呢?仅仅是兼容,那么它们算作是操作符吗?GCC的说明似乎更清楚一些:

    Punctuators are all the usual bits of punctuation which are meaningful to C and C++. All but three of the punctuation characters in ASCII are C punctuators. The exceptions are ‘@’, ‘$’, and ‘`’. In addition, all the two- and three-character operators are punctuators. There are also six digraphs, which the C++ standard calls alternative tokens, which are merely alternate ways to spell other punctuators. This is a second attempt to work around missing punctuation in obsolete systems. It has no negative side effects, unlike trigraphs, but does not cover as much ground. The digraphs and their corresponding normal punctuators are:

    Digraph:        <%  %>  <:  :>  %:  %:%:
    Punctuator:      {   }   [   ]   #    ##
    
    这里提到的三个c语言的操作符标点符号我似乎还是从来也没有用过,怎么使用呢?我觉得我可以省略这些,它们是独立的功能。
  3. 我现在体会到我把lex和parser的语法分开的好处,就是之前总觉得有些语法很费解明明用不到还要定义,比如literal里的很多部分,那么现在参考大师的lexer的输入我可以直接使用regex来描述lexer的语法,BNF和regex在某种程度上是等价的吧?我删除了lex部分的语法大部分都比较容易了,唯一需要一些机巧的是balanced-token不过flex有很多铉酷的功能应该可以解决这个问题。或者这个直接就是parser的功能?不应该在tokenizer里实现?
  4. 这个几乎是对于之前的做法的一个否定,我决定在github上保存一下。此外大师的例子我也保存在这里。

十二月十二日 等待变化等待机会

  1. 如果你要测试bison那么没有输入的tokenizer是不行的,所以,一定要有某种lexer来配合,而我很快就打消了自己使用regex库写一个简单的lexer的念头,因为regex是一个非常难以debug的东西,而flex的强大在于它让这一切变得容易。手册里有大量的例子和经验之谈,我很有信心对于哪怕是看上去很困难的RAW-string也是轻松应对吧?希望如此。这里的一个special directives
    BEGIN followed by the name of a start condition places the scanner in the corresponding start condition
    但是这里的start condition是一个让我感到难以理解的概念。有大量的例子但是我总是抓不住重点。
    BEGIN(0) returns to the original state where only the rules with no start conditions are active. This state can also be referred to as the start-condition INITIAL, so BEGIN(INITIAL) is equivalent to BEGIN(0). (The parentheses around the start condition name are not required but are considered good style.)
    所以BEGIN(INITIAL)仅仅是清零到无起始状态?这个INITIAL变量找不到定义。
  2. 我查了半天也不明所以,还是google解决大问题,天下聪明才智之士比比皆是,这位大侠就是使用start condition来实现raw-string的,我来实验一下!结果不错,不过有一点点的不足,就是说我返回的token应该是string-literal本身而不是delimiter等等,这里有一点很有意义就是在flex里的定义的临时变量就是定义的变量没有indented是flex内部的不会被用户代码看见,而我在返回的时候改变yytext和yyleng以便返回整个的string-literal。这就是我的完整的程序。
  3. 十二月十二日忆加州兄弟

    登高南望觅归途, 狼烟四起有也无。 微信借我千里眼, 关山万重传音图。
  4. 这个是string-literal实际上比raw-string容易多了,根本不需要用到高级的start condition这样的玩意,但是我以为不同编解码是有大区别的,所以必须返回不同的token。当然普通未编码的字符串必须放在其他的后面这一点很重要。

十二月十三日 等待变化等待机会

  1. 我开始一步步的推进flex部分,基本上是以unit test的形式来实现,这帮助我更好的理解flex的defintion和pattern,这里和regex一个最大的不同是{}被赋予了变量名的作用而不是字符串的作用。而我对于BNF的敏感度太低了,比如对于这个语法我竟然看不出它的regex表达式:
    
    binary-literal:
    	0b binary-digit
    	0B binary-digit
    	binary-literal 'opt binary-digit 
    
    居然要琢磨半天才意识到flex的definition是不能支持递归定义的,这个是早就预料到的,问题是它其实很简单,就是
    binary-literal is the character sequence 0b or the character sequence 0B followed by one or more binary digits (0, 1). Optional single quotes (') may be inserted between the digits as a separator. They are ignored by the compiler.
    明白了这一点根本就不需要递归定义,就是(0[bB][01][01']*),这个BNF的左循环确实是讨厌,LL(1)一定要去除它是有道理的。
  2. 有一刻我一直担心我的tokenizer不去区分这个token和下一个token是否有whitespace来隔离,后来我明白了这个是在语法里来限制的,也就是说为什么语句之间需要分隔符;因为tokenizer是不负责区分是否两个token是紧密连接的因为这个语法是由parser的语法依靠分隔符来实现的。
  3. 有些概念需要反复的理解才能真的体会,比如再次遇到conditional-escape-sequence现在我更加清楚的明白这个是实现者的选择,如果你要实现的编译器压根不想增加任何额外的escape-sequence那么你在你的lexer/parser里根本就可以定义它是非法的或者直接忽略掉。这个就是最简单的,或者就是像GCC那么样子专门爆出警告。因为从逻辑上看它的定义应该是通常的escape-sequence的补集,我不知道这个在regex里会不会有问题。

十二月十四日 等待变化等待机会

  1. 这就是传说中的内部保留字吗?我还是第一次明确看到这个说法:
    Each identifier that contains a double underscore __ or begins with an underscore followed by an uppercase letter is reserved to the implementation for any use.
    真的有这回事吗?这一条更加的离谱设么叫做underscore开头的变量名是保留给全局变量的,我从来就没有看到程序员有遵守过。
  2. 我在不经意间把大侠的代码的关于 dchar_seq [^()\\[:space:]]{0,16}搜索长度限制去掉了结果导致一系列的问题,具体原因我现在还是想不透彻,也许是flex的限制吧?总而言之我以为在实现中对于这种限制是可以接受的,尽管标准没有说raw-string的delimiter的长度有什么限制,可是谁会用一个超过一般长度的delimiter呢?找茬?还有一个困惑就是关于user-defined-string-literal这个理论上是应该也可以支持RAW-string,可是真有人要这么做吗?对于我目前的这个做法变成要拷贝粘贴很多的代码,这个和各种各样的encoding的string-literal一样,在语法层面来看u8/u16/u32/L的string可以一言以蔽之为string-literal,可是对于实现者来看当然要区分因为应该要生成不同的数组类型,不过这个都是生成目标代码的问题,我在想我做一个原型仅仅验证语法需要这么复杂吗?我甚至应该可以全部都返回literal,不管是什么类型!就像最新版的语法对于identifier的规定也是让我头疼不已,什么是ISO/IEC 10646?我下载了标准依旧没概念,这里的各种编码太令人头疼了,我不应该在这些细枝末节上浪费精力。
  3. 我无意中看到这位大侠的工程应该是很有参考意义的,因为我也在想实际上使用flex制作一个preprocessor应该是可能的,至少是比lexer的工作量要小一些吧?不过我对于宏替换感到有些怀疑,似乎这个要产生动态的宏的名字作为start-condition,这个要怎么可能呢?因为在我看来如果我能够把宏作为我的start-condition那是可行的,但是这样子就意味着动态产生flex的代码?不可能吧?
  4. 我因为也要面对实现所谓的header-name所以,就参考一下大侠的实现,当然我是不满意的,因为颠覆我原本的认识就是不论是system-header还是user-header实际上隐含是支持文件名包含空白字符的,这个在windows里尤其突出,很多人命名文件是习惯于夹带空格的,所以,这里的语法是很明确,而且这里是不允许去除""或者<>以外的任何字符的因为开头的空格也是会被作为文件名的一部分的!当然了没有规定#include必须是这一行的起始。
  5. 这是我的简单的关于header-name的测试程序。
  6. 保存unittest
  7. 对于raw-string作为user-defined-string-literal来说是很有用的,比如我们可以把这个raw-string做escape的处理,这个会是很有用的做法:

十二月十六日 等待变化等待机会

  1. 我昨天才意识到很有必要明确c++的这9个phase的含义,因为它明确了cpp的界限,究竟flex能够做哪一个phase。这位大侠和我有类似的想法,可是看样子没有人有解决方案,说明不大可能或者说不值得?这个作业倒是要求学生要实现一个macro expander,可是这个对于object-like-macro是容易的,但是对于function-like-macro是几乎不可能的。这个应该就是不可能的地方吧?不过这里的notes还是对于阅读flex的manual有些帮助。 这里回到了我昨天的一个疑问,因为我把官方的regex文档的规则当作是flex的保留字的规则,这里似乎有些细微的差别吧?
    Reserved symbols include:. $ ^ [ ] - ? * + | ( ) / { } < > Reserved symbols can be matched by enclosing them in double quotes or prefixing them with a backslash.
    这里的核心关键就是在flex里凡是被double quote的这些特殊保留字符就相当于escape了,我昨天因为没有意识到这一点感到很困惑,因为在标准的regex里字符串的double quote是没有影响的。总之,这个是我在手册里没有读到的。这个是我对于flex手册的第一个最大的报怨,因为在作者看来天经地义的事情根本没有提及regex的保留字是如何处理的,似乎根本没有意识到regex是一个多么复杂的世界分了好几个流派POSIX的basic和extended,至于说extended和perl有多大区别我懒得看了。,至少我在boost的regex文档里是得到这个印象,最普通的默认的是perl规则。昨天我实验制作一个comment就遇到这个问题因为"/*"在没有escape的情况下是代表若干个/的,当然这个double quote的规则对于我来说还是第一次听说的。
  2. 关于这一点the memory pointed to by yytext is destroyed upon completion of the action.我猜想应该不包含start condition里建立的吧?
  3. parser做的就是第7phase。而flex做的tokenizer工作是第3phase可是宏替换是在这个之后,所以,我的难题就是flex是做不到宏替换的,或者说很困难。那么其他人是怎么解决的呢?Fog大侠也没有提过这个问题。也许这里的顺序只是逻辑上的?换言之,我之前没有把preprocessor-token包含在内就是错误的认为tokenizer是在preprocessing之后,这个难道是我最大的错误之一吗?从语法的分类来看的确是我错了,因为pp-token也是lex的语法的一部分,看来我必须把这部分加入!我还是理解错误,因为我没有能力去实现preprocessor的功能,那么我只是利用它的结果,那么tokenizer的工作就是在大概phase7之前的部分,甚至phase6能不能做都是一个问题。
    A preprocessing token is the minimal lexical element of the language in translation phases 3 through 6.
  4. 这页的notes是非常深刻的,是每一个实现者都要反复阅读的。比如我从来没有想过这个问题:
    The program fragment x+++++y is parsed as x ++ ++ + y, which, if x and y have integral types, violates a constraint on increment operators, even though the parse x ++ + ++ y can yield a correct expression.
    这里的constraint是什么?在GCC里的错误是error: lvalue required as increment operand这个是在语义分析之后才得到的吧?我的实验是这个在预处理里没有办法识别的,这个也许是委员会成员的理想吧?我觉得这个根本的法则
    Prefix versions of the built-in operators return references and postfix versions return values, and typical user-defined overloads follow the pattern so that the user-defined operators can be used in the same manner as the built-ins. However, in a user-defined operator overload, any type can be used as return type (including void).
    就是说这里唯一的限制只不过是语义分析之后的lvalue/rvalue的问题,如果我们硬要自己重载操作符的话是没有问题的: 注意这里的postfix version的increment的操作符重载形式,必须使用一个形式参数,实际上这个参数可以是任何类型必须是int 所以,这个结果不但是合法的而且是合理的
    
    	My x,y(10);
    	cout<<x+++++y<<endl;
    
    答案是个12。
  5. 这个是刚刚收到的很棒的关于emplace的博文,我还来不及仔细看。
  6. 这个操作符的优先级应该是要能够在bison里表达的吧?这个是以后的问题。
  7. 我不想假装我能够读懂这些复杂繁琐的顺序,这个太复杂了。这些都是语义分析的问题吧?GCC是怎么解决这些问题的呢?这些应该不一定是parser要解决的吧?如果在产生目标代码的时候去做,当然如果能够在语法中自动产生这种顺序那么目标代码的产生自然也是一样,可是不一定非要这么做吧?
  8. 这里是c++的所谓的common misunderstanding我还来不及仔细看。总之我现在很没头绪不知道要怎么应对#pragma这个到底是谁去完成呢?GCC的预处理的结果里依然包含这个。
  9. 我一直发愁的是因为我看到GCC的预处理出现了#pragma GCC visibility这个颠覆了我对于cpp的phase的概念,因为在我看来这个directive应该已经被处理了为什么在预处理的结果里出现呢?这里的解释让我这样认为这个是GCC的特定的做法就是这个是关于linkage的信息需要传递给最后的代码生成,它不是parser需要关心的问题,当然我的担心不是没有原因的因为根据语法这个是预处理才出现的,在预处理结束不应该是结果的一部分。
  10. 很久以来我对于size_t就感到有困惑,在工作中不止一次的遇到麻烦,今天我终于第一次意识到它不是一个keyword,它是在哪里定义的呢?

十二月十七日 等待变化等待机会

  1. 经过了不少的debug总算初步完成了一个简单的tokenizer,基本上能够用的原因是它通过了几乎所有的stl库的预处理结果,这个应该是相当的复杂的结果,我猜想也许能够覆盖c++的各式各样的语法现象里的全部吧?至少目前我能够处理所有的token:literal/keyword/operator/identifier,当然identifier是简化版因为我实在不明白要怎么表达所谓的一些encoding的现象。另外对于GCC的特别#pragma我予以忽略毕竟那个是后续的工作。其中的debug还是比较辛苦的,首先我吃亏在定义rule的时候在结尾部分加上注释结果很多稀奇古怪的现象出现,其次,就是计算raw-string以及要在这个基础上实现支持user-defined-raw-string就遇到一点小错误,就是说哪怕是一个否定的规则你实际上也要把这个字符退回去,比如你定义一个否定的规则看是否是identifier的开头[^_a-zA-Z] {yyleng--;BEGIN(INITIAL); return STRING_LITERAL;} 这里我第一次意识到很多的编辑器会自动在文件结尾加上一个回车字符,这个我在vi[m]上也没办法去除,也许是因为我使用.cpp的文件后缀名的关系吧?总之,这个导致了很多的痛苦,另外一个心得就是如果我担心yytext/yyleng在规则过后就失效的话,那么使用yymore就是最稳妥的。此外就是debug输出的时候要使用cerr因为coutyyerror或者是flex已经占用了吧?最后就是我始终吃不准flex里特殊字符escape的规则,我感觉有些不太对头,也许是我的问题,总之,我本来以为在[]里面是如regex一样失去特殊意义,但是至少double quote似乎不是如此。总之,总算完成了一个里程碑吧?
  2. 我第一次尝试就遇到了这个显而易见的歧义,这个语法我自始至终也没有理解,我现在意识到这个实际上是相当复杂的部分因为关于类型是c++最核心的部分之一,因为强类型是它的优势也是它的难点。 typedef long int somebloodyid;这个真的有很多种解释吗?但是我还是很高兴的因为我终于实际的跨出了第一步,走了这么大一个圈子就是为了一个简单的答案:GLR-parser如何能够破解歧义?需要真正的离开flex进入到bison的领域来学习了。

十二月十八日 等待变化等待机会

  1. 我打算做一个简单的测试,就是选取起始点为translation-unit的一个子节点,可是基本上并无帮助,因为语法树是一个大树,我从decl-specifier-seq开始几乎就是覆盖了整个的语法树,这个说明了冲突发生的很早,我对于bison如何探测处理冲突的机制还不明白,需要再次阅读文档手册。
  2. 其实我对于诗词韵律一窍不通,这个工具也没有看懂。

    看到网上一曲伤感的歌词改了改

    离恨梅花冷成伤, 半生惆怅诗两行。 缘尽何妨歌一曲, 雁飞无奈恋故乡。 长琴素手不相见, 断桥凭问天地长。 玉碎光沉心如水, 红笺似血旧时光。 云边瀚海天欲老, 风泣云愁任桑沧。 半生梅花何相仿, 惆怅不过泪成双。 白发明月醉相望, 千古迷惘词几章。 夜深彷徨梦一场, 花舞为谁留余香。 回首不堪云遮月, 来生相认又何妨?
  3. 这里看到一大群的文艺青年
  4. 结合最新版的bison-3.8.2的文档和例子才明白了一点,就是GLR是如何解决歧义的,这个相当的复杂,但是非常的非常的有用!这个是典型的c/c++里有歧义的语法,假定大写字母为类型,小写字母为变量,那么T(id)=a+b;既可以解释为变量id是被type-cast为类型T也可以解释为声明一个类型为T的变量。这个本身在c/c++语法里是典型的,因为虽然大多数人不需要额外的括号来声明但是是合法的语法。GLR并不能解决这个歧义因为无解,但是能够帮助你探测这个情况的发生,所以,特别的可以定义所谓的%merge指示让你定义一个函数类似回调一样在发生这个无法解决的歧义的时候额外的做些什么。注意到int(i+j)=x+y;是没有歧义的,我进一步在编译加上了开关--report=solved --report-file=output --html=glr-example.html这样子能够看到GLR是如何解决冲突的,但是我看的如同天书:
    
    State 23
    
        8 expr: expr • '+' expr
        9     | expr • '=' expr
        9     | expr '=' expr •
    
        '='  shift, and go to state 13
        '+'  shift, and go to state 14
    
        $default  reduce using rule 9 (expr)
    
        Conflict between rule 9 and token '=' resolved as shift (%right '=').
        Conflict between rule 9 and token '+' resolved as shift ('=' < '+').
    
    回过头来看定义的操作符优先级
    
    %right '='
    %left '+'
    
    这个说明了什么呢?这个很难懂,我先保留它的html语法输出再慢慢体会。
  5. 实际上是两个问题,首先不要看依靠优先级如何解决冲突的方法,这个似乎更困难。还是先看看冲突是如何产生的,这里说的就很明白了:就是在这个状态下它们的lookahead都包含了),换言之每次出现 Type (id);就会遇到歧义,而这个时候定义一个%merge打印一个<OR>是给我们一个提示:
    
    T  ( a );
    1.0-7: <OR>((T, a), <cast>(a, T))
    

十二月十九日 等待变化等待机会

  1. bison文档里提到了不少的技术,其中一个是很普遍的也是我唯一听说过的,就是把parser的问题改为lexer的问题,比如设置一个标志量让lexer的代码做特殊的处理以便返回不同的token kind,这个显然是下下策是万不得已才为之的,我之前忘记在哪里看到类似的做法。好像是GCC让lexer动态生成一个特殊的token kind。
  2. 另一个机巧是很直接的,就是语法里一言一概之说除了关键字都是identifier,可是在typedef的和声明的能一样吗?前者应该是TYPEDEFNAME,这个就是我之前在大师看到它有一个函数来区分这个,当时我完全不能理解,现在明白大师也是在实践这个方法这个不是很准确,这个是在ISO-C/C++的那个大师里看到的但是他要求程序员额外处理typedef问题让其返回的token的开头字母能够区分是identifier还是typename,这个就是更不可行了。。不过我现在开始真正的有点开窍了,就是真正开始接触到核心问题,c语法可以解决成仅有一个dangling-else的冲突,但是c++有几千个,那么靠通常的手段能够解决多少,是否能够都能解决,答案显然是否定的。关键的问题是如果不这么做,这些冲突是不是也是同样会在手写的编译器处理函数同样出现,所以,这里的问题就是哪一个更好更容易,能否自动化处理一部分?
  3. bison手册里举了一个惊人的例子说明我对于c语法的不可理喻缺乏了解:
    
    typedef int foo, bar;
    int baz (void)
    {
    	static bar (bar); 	/* redeclare bar as static variable */
    	extern foo foo (foo); /* redeclare foo as function */
    	foo(bar);		/* I add this line myself */
    	return foo (bar);
    }
    
    这里的foo和bar被怎样的滥用!这个对于编译器希望通过typedef的上下文来返回一个细分的lexer token可能是不可能的吧?因为同样的foo在不同语境里既是typedefname,也是一个新声明的变量而且还可以再次被声明为函数,这个要感谢c语法的类型的不够严格,c++可以杜绝这个,这也许是好消息虽然也许帮助不大?因为c++编译器作为兼容c语法即便你强制使用-x c++这个语法依然是合法的,完全不需要任何的强制转换之类的连警告都没有,因为这三个foo各有不同的含义: extern foo foo (foo);
  4. 在bison使用-Dtrace.parse仅仅是打开了debug输出的可能性,不要忘记在代码里真正的打开:yydebug=1;,用这个方法来配合输出的html语法文件是最有效的学习方式!
  5. 我发现我对于这个literal operators感到有些意外,因为这个语法并没有准确的反映吧?因为既然不能有encoding为什么不定义一个专门的nterm呢?还有就是我不理解什么叫做只有\0的string literal。这个我要怎么定义呢? 比如如果你硬要求字符串必须是一个空串那么何必要求也支持raw-string呢?比如以下这两个是等价的不可能同时定义的,而且字符串只能是一个\0,那么何必要这么麻烦呢?我终于明白为什么了:因为lexer没有办法表达""或者说quote(")是不在token范围内的!所以,在c语言里single quote(')为什么被称作apostrophe,问题是operator ""_escape在parser眼里是什么呢?难道是一个空字符串?所以我自己发明了一个token:LITERAL_OPERATOR_STRING就是一个没有任何encoding的空字符串。不!这个应该留给语义分析做判断,一个字符串或者说string-literal就是一个覆盖了所有可能的string-literal,至于是否允许encoding,是否是一个仅有null字符的空字符串这个是语法分析最后的语义分析才能决定的。
    
    string operator R"delim()delim"_escape(const char* ptr, size_t length);
    string operator ""_escape(const char* ptr, size_t length);
    
    不过虽然目前看literal-operator-id
    
    operator string-literal identifier
    operator user-defined-string-literal
    
    的两个形式似乎本质是一样的多此一举,谁能保证以后不变呢?
  6. 我现在知道我为什么头疼了因为literal包含了string-literal,可是语法里又单独要求需要string-literal被单独使用,那么我的这个primary-expression语法的语法就麻烦了因为我需要单独把string-literal加在literal之下,这个也是让我头疼的地方,lexer定义了各种各样的literal比如character-literal,numeric-literal等等,这些除非你做语义分析根本不需要区分,而这个显然是不合理的,因为语法就是服务于语义分析的,怎么可以事后再。。。以后再说吧?

十二月二十日 等待变化等待机会

  1. 我对于这个语法有疑问,因为最简单的一个语法int i;居然都有歧义! 我看了半天也不是很确定大概就是说
    
    simple-declaration:
    	decl-specifier-seq init-declarator-listopt ;
    
    init-declarator-list如果是可选的导致i作为IDENTIFIER会被当作class-name。这个实在是太可笑了。那么究竟为什么init-declarator-list是可选的呢?这里就看出标准的不好的地方因为没有多少的解说,而这里就有解说。我的感觉就是这里的specifier并不是所有的都需要init-declarator-list,比如typdef,但是对于其他的则是一定的。这个在早期c++语法里是不存在这个问题的,那个时候语法很简单就是简单的init之类的。所以,只有修改语法吗?
  2. 语法断断不可改!因为它是指引仅仅是告诉了什么是可能的,就是给出了可能性,而标准的解读大多给出了什么是不可以的范围限定或者特例,所以,只能在现有语法下去解决例外或者大破歧义。也许要深入理解bison是怎么认定歧义开始吧?现在的任务是bison的算法和代码!
  3. 我只能说学习bison不容易,哪怕一个所谓self-explained的simple例子也要耗费好多时间。时间和实践在中文拼音里是同音字这个不是没有理由的。

十二月二十一日 等待变化等待机会

  1. bison的难度还是不小的尤其是和flex结合。比如一个简单的YYLTYPE就折腾了我好久,这个我读手册始终不得要领因为总是报错说未定义,可是我压根没有想自己定义特殊化的location_t类型,为什么我不能使用系统默认的呢?最后才看到一个选项%location,这个类似的东西还有很多,比如我对于头文件的理解就被带偏了,因为文档里说了很多的复杂的东西反而让我对于简单的事物产生误解,比如bison产生头文件完全是给其他程序用的,bison产生的parser本身是不需要引用头文件的,结果例子里面的代码写%define api.header.include {"word.hh"}就让我产生了错觉以为产生的头文件本身包含了某些用户的定义成分必须要引用。这里的直接原因是一个关于更加基本的yyerror的定义的问题,因为这个是必须用户来定义的一个东西,可是我自己定义了使用位置参数YYLTYPE并且使用系统的yyloc的版本结果bison却始终使用默认简单参数的类型并报错,这个折腾了好久才看到%define api.pure这个在所谓的A Pure (Reentrant) Parser的情况下这些全局变量是不可用的,于是在pure parser的情况下是不可能使用yyloc这个的,所以只能自己去实现location的功能。而更多的是一点一滴的琐碎的细节,比如你自己实现一个简单的yylex,那么假如返回值是yytoken_kind_t的类型,使用c++编译器implicit类型转换往往是被禁止的,也就是说返回数字0不行,那么只能使用特定的变量YYEOF或者YYUNDEF,这个总比强制转换yytoken_kind_t(0)来的好看一些。
  2. 使用c++编译器来编译c语言版本的bison产生的parser和使用c++语言生成的parser是两个完全不同的概念,我对于两者的区别一开始感到很迷茫,因为cc/cxx的编译肯定是很多不同的,因为前者使用g++编译c代码仅仅是混合编译的问题,可是后者是纯粹的c++11/14风格的代码,我担心这个会有额外的问题还是使用纯粹的c语言版本来的可靠一些吧?
  3. 再一次体会bison的复杂度,这个应该是GCC社区当初不愿意使用的一个非常重要的原因,因为在十几年前bison/yacc还不是非常的成熟的时候额外的复杂度和可能的bug让这个失去了很大的吸引力。当然这个都是人尽皆知的陈年旧事。看来没有个把月的实践很难入门。

十二月二十二日 等待变化等待机会

  1. 现在回过头来看这两个关键函数yylex/yyerror是不太可能任意定义的,因为这个相当于回调,不可能你任意定义参数类型的,这个有机会结合代码看看它的原理。
  2. 太多的琐碎,比如折腾了好久才发现这个%define api.header.include {"header.h"}是只有c语言编译器才有的,而我对于yacc的一无所知导致我始终没有意识到-d/-t是和yacc兼容,而很多东西都是bison的扩展,比如很多的宏directive的定义,就包括这个include宏。
  3. 我简直要被逼疯了,bison有太多的灵活性,而种种高级功能让人抓狂,看样子我只能从一个简单的例子来入手。从最最基础的c语言的简单例子入手不要使用c++!另外学习官方的做法不要在bison的开关上定义众多选项而是把选项写在bison的输入文件的directive里这样子能够最大限度简化make文件。
  4. 以c/calc来为例:
    1. bison命令行开关--header和指示%header是等效的,差别也许只是yacc兼容的问题。
    2. 首先,%define api.header.include {"calc.h"}并没有什么实质的用途,这个通常是用来引用其他用户定义的头文件,引用自己产生的头文件我看不出有什么用意,除非就是自己的.y文件里代码部分就包含了include自定义头文件部分。这个是不成立的因为在.y文件里的所有include是当作verbatim直接拷贝到产生的.c文件里的,而产生的.h文件包含的内容都是bison想要输出的部分比如token的enum即yytoken_kind_t,显然这个是给flex用的以及extern int yydebug;还有就是如果不重新定义YYSTYPE这个宏的话就会输出它。
    3. YYSTYPE是怎么定义的呢?首先我们定义它是union比如%define api.value.type union但是不用具体定义它的成员而是一边用一边定义,比如 对于terminal或者说token是%token <double> NUM "number"这里注意最终的union里的成员是NUM后面的"number"实际上是打印输出好看的alias,这个是一箭双雕的两件事。而对于nterm或者说type则是%type <double> expr term fact注意nterm是没有alias的,根本不需要啊,你不满意名字就改一个没有人能把限制呀!所以在union里我们就看到了
      
      union YYSTYPE
      {
        double NUM;                              /* "number"  */
        double expr;                             /* expr  */
        double term;                             /* term  */
        double fact;                             /* fact  */
      
      #line 72 "calc.h"
      
      };
      typedef union YYSTYPE YYSTYPE;
      
    4. 我之所以说引用自己产生的头文件毫无意义是因为,假如你没有%header或者--header那么即便不产生头文件给第三方用,bison产生的头文件里的哪些定义部分也是要出现在.c文件里的,否则根本就编译不了。那么即便产生了头文件而你没有定义%define api.header.include {"basename.h"}而产生的.c文件照样会自动帮你加上这个include头文件部分,所以,这里的例子纯粹在误导用户以为.y定义的部分必须要通过用户主动引用自己产生的头文件才能定义,这个是导致我很多错觉的根本原因。
    5. 我之所以遇到YYSTYPE没有定义的错误就在于我必须要在.y文件里至少给出一个定义,在c语言里或者你明确定义YYSTYPE或者使用 %define api.value.type来定义,而可能选项则只有none,union,union-directive,variant,yystype如果定义了union之后可以不用一次定义所有的union的类型,而是采用tag的方式来定义每个的类型。
    6. 原则上可以使用纯粹的gcc编译纯粹的c语法,但是并没有人能够限制你使用g++编译混合c/c++语法,我脑子里还是对于%language "c++"有一个不正确的认识,因为这个产生c++接口是一个非常大的差别,也许是不值得,因为有很多所谓的和yacc不兼容的问题吧?还是坚持使用c语言接口就好了。这里的潜在的意思就是说yyoFILE*所以并不能当作cout来使用。所以, %printer { fprintf (yyo, "%i", $$); } <int>;这里的类型tag我认为和定义的时候是类似的方法,它应该是在parse.trace开关打开的时候的默认输出吧?
    7. %verbose是产生所谓的bison的output文件,也就是类似于html的文本输出文件,后缀名默认就是.output,这个是一个简化bison开关的好对策。
    8. 类似的%define parse.trace也是产生debug代码的可能性,真正运行期也依靠yydebug=1;在代码里打开。
    9. 在标准的c接口下,这两个函数的定义是容易的:
      
      int yylex (void);
      void yyerror (char const *);
      
      没有那么多的类型转换或者检查的问题,应该就是最古老的yacc的形式吧?YYEOF/YYUNDEF这些宏总是可以使用的。
  5. 我感觉还是要一步一步的从最基本的开始,但是很多时候最基本的例子我往往take for granted,就是司空见惯不以为然认为是天经地义,结果到了高级的例子立刻暴露了所有的缺失,也许学习就是一个反复遭遇挫折回滚的过程吧?看来在两个星期内掌握bison是不太切合现实的吧?

十二月二十三日 等待变化等待机会

  1. bison的设计肯定不是那么的完善,或者说我的期望值是不切合实际的。比如定义的所谓的%printer实际上并无神奇之处,它仅仅是被插入到debug输出的部分,因此不要指望这个所谓的yyo是一个全局变量,它仅仅是一个所谓的
    
    static void  yy_symbol_value_print (FILE *yyo,  yysymbol_kind_t yykind, YYSTYPE const * const yyvaluep)
    
    里的函数参数而已。另一个类似的低级文帝就是所谓的YYLOCATION_PRINT (stdout, &@1);这个也是纯粹的为%debug输出的,如果不定义这个宏的话编译就会出错,其实似乎没有必要用户自己主动在代码里调用它,因为你要debug就每一行都输出才对啊?没有必要专门去debug某几行的代码,不是吗?不过这个确实是一个被我误解的。
  2. c++的接口复杂度高了不少,我也隐隐担忧它的稳定性,当然首先是我自己的问题都不知道有多少。
    1. 这个排在第一位的头文件的部分也许反而是最不重要的部分,bison有一个扩展top要保证这部分在最开始
    2. 首先,一个基本的问题就是%locations这个宏不要随便使用,因为配合%debug它应该是在各个调用函数里添加这个location的参数,这个是让我错手不及。
    3. 对于c++接口最好是使用高版本所以这个宏是需要的%require "3.2"当然表明语言是绝对必须的%language "c++"
    4. 对于所谓的debug facility的成为可能最好是使用bison的扩展%define parse.trace在c++接口下,全局变量yydebug是纯c语言的接口办法就不适用了,需要使用所谓的parser.set_debug_level(1);
    5. 对于c语言也许也是可以使用union的吧?但是对于c++来说这个是必须的%define api.value.type variant
    6. 这个宏是什么意思呢?%define api.token.constructor比如我定义了一个token是%token <std::string> WORD;那么我们可以放心的让bison产生一个ctor函数叫做parser::make_WORD它只需要一个参数类型是string
    7. 更好的做法是我直接使用std::variant来定义一个关于这样语法的
    8. 现在很明显了对于token我们使用的是真正的variant的赋值方式给maybeword,那么对于其他的nterm呢? 这里我们利用%empty来创建default ctor,然后在sequence里当作vector来把variant装入。
    9. 那么我们要怎么显示得到的结果呢?
      
      result:
      	sequence  {std::cout<<$1<<std::endl;}
      	;
      
      而实现这个vector的操作符重载和bison就无关了,这里值得关注的是对于variant使用visit的访问方式。
    10. 另一个非常重要的方面是c++接口的lexer接口也是不同的,首先它的函数签名是不同了:auto yylex () -> parser::symbol_type这个symbol_type是一个相当复杂的部分这就是为什么使用make_WORD这样子的ctor来处理了。我仿照例子做了一个简单的,注意参照作者建议使用namespace yy来避免代码冲突
    11. 最后是简单的main部分,注意默认我们使用类parser来创建,而调用它的parse()方法
  3. 学习这个c++的接口确实不是很容易,有点累了。
  4. 很多时候,我感觉bison/flex这些做法就像武侠小说里的一种传说中的武林秘籍,很难,很偏。

十二月二十四日 等待变化等待机会

  1. 这个问题导致我不应该使用c++接口,因为%glr-parser不支持这个%define api.value.type variant但是例子里是可以的不过要使用这个%skeleton "glr2.cc"
  2. 我总算是想起来了当初为什么要阅读bison的手册:是因为我遇到了GLR也不能解决的歧义,那么这个有什么解决办法呢?看样子文档虽然关于GLR的介绍很简单但是我要再读三遍。这里是一个相当不错的帖子。大部分都是我所能够预料的,不过这个还是值得思考的:
    In C, all names are lexically scoped, so it is certainly possible to resolve x at compile time. But the resolution is not context-free. Since GLR is also a technique for parsing context-free grammars, using a GLR parser won't directly help you. It might be useful in the sense that GLR parsers can theoretically produce "parse forests" rather than parse trees; that is, the output of a GLR parser might effectively contain all possible correct parses, leaving the possibility to resolve the ambiguity by building symbol tables for each scope and then choosing between alternative parses by examining the name binding in effect at each site. (This works because type alias declarations -- "typedefs" -- are not ambiguous, so all the potential parses will have the same alias declarations.)
    这个 lexically scoped是什么概念?那么这个做法resolve the ambiguity by building symbol tables for each scope难道不是最根本的办法吗?难道还有别的可能吗?这里提到的choosing between alternative parses by examining the name binding in effect at each site似乎说明了bison-glr的算法?这方面的资料很有限,看代码其实不太容易,因为似乎是m4的形式,我要找一下它产生的代码在哪里。总之,这个帖子是相当的有水平的。大侠也提到了lexical feedback或者说the lexer hack,但是我是很抵制这个也许是最后没有办法的办法才行吧?可是据说模板只能这么做???

十二月二十五日 等待变化等待机会

  1. bison的很多代码本身就是一个依赖bison/flex产生的代码,这个仿佛bootstrap一样的,GCC里也是有类似的场景,compiler-compiler本身的代码也需要一个可靠的compiler-compiler来编译,那么第一个这样的东西在哪里?这样子的鸡生蛋蛋生鸡的问题是难以回答的,但是也很容易理解只不过不是我需要关心的。同样类似的是关于skeleton部分的代码都是使用m4产生的,而读取它似乎又有一个语法也是产生的代码,所以我才印象中找不到glr的代码因为看不懂m4,而如何产生真的是一个超乎想像的复杂过程。bison的编译过程的复杂是给我一开始留下深刻印象的,因为这个过程看样子是要有很大的所谓的自持不可过分的依赖于外界我现在看到了官方的说Bison is self-hosted它有如下的好处:
    1. dogfooding: let Bison be its first user
    2. monitoring: seeing the diff on the generated parsers with git is very helpful, as it allows to see easily the impact of the changes on a real case parser.
    。总之,很不简单。绝非一般的开源项目那么司空见惯。
  2. 而且另一个让我吃惊的是关于skeleton的文档里说glr2.cc似乎并不是官方支持的接口
    glr.cc A Generalized LR C++ parser. Actually a C++ wrapper around glr.c.
    看样子这个是非常复杂的部分,也许还没有稳定下来?这个过程有多复杂呢?首先,skeleton部分的代码是m4写的,不知道要怎么转化应该就是m4,但是有一个scan-skel.l来扫描,这个应该是在这之前还是之后呢?或者这个难道只是为了防止flex的一个bug的重新实现?这里的复杂度超出了我的认知。
  3. 长久以来我对于m4一无所知到了连名字都不明白的地步,今天我终于知道它就是一个宏处理器,并不是autoconf本身而是说它是很多项目的所依赖的因为大家不论是什么语言都需要宏,所以,autoconf作为配置的一部分就和m4手拉手了。说不定autoconf本身也需要宏,所以,m4是所有的鼻祖,据说是最早汇编的最爱,而这里的4是宏编译器版本号了。事实上m4已经成为宏处理器支持的语言的代名词了知道这个就足够了。
  4. 现在先阅读bison的readme:
    Bison is written using Bison grammars, so there are bootstrapping issues. The bootstrap script attempts to discover when the C code generated from the grammars is out of date, and to bootstrap with an out-of-date version of the C code, but the process is not foolproof. Also, you may run into similar problems yourself if you modify Bison.
    这里的学问很深我还无法想像。
  5. bison大师推荐这个入门的文章,我要好好读一下。文章里说如果读者对于LR还不够自信的话就补习一下这个,但愿我不需要它。大师说
    The parsing algorithm is same (at least in theory) for all LR parsers. They differ only in the tables generated by the parser generator. In our case, Bison generates LALR(1) parsing tables in the form of C/C++ arrays. The generated yyparse() function implements a parsing routine that uses these tables and a stack to guide the parse.
    这个要怎么理解呢?难道说的是dragon书里的理论做法也是这样吗?大师提到的入门书我一本都没有看过。改正了大师语法中漏掉的引号。这里在比较手写的表和bison产生的表,我看不出两个有什么压缩的区别。这里是我不知道的
    The empty slots in the action part of the table are errors; The empty slots in GOTO part is NOT errors, but simply unreachable; It is impossible to detect an error on a non-terminal symbol because of the correctness of the LR algorithm.
    怎么理解GOTO的空白是不可能的呢?是说如果按照正确的算法根本不可能看到不可能看到的token?这个要怎么理解呢?这个是要flex的tokenizer要配合才行的还是本身算法就决定了?理解错了因为GOTO部分都是nonterminal,那么这个不是取决于flex的返回的实际的token,因为nonterminal是由语法规则决定的,除非算法有错否则空白就是不可能的。这个应该是很容易理解的我却没有意识到。所以这部分表格是可以省略压缩的。 这里是大师的预告:
    Instead of generating a big sparse matrix like the one shown above, Bison generates several smaller one dimensional tables as C arrays. Not all tables are used directly by the parsing routine - some are used to print debugging information and error recovery.
  6. 为什么
    There is no notion of "final state" in theoretical LALR parsing (although it exists in an LR(0) characteristic FSM),
  7. 这个压缩的算法非常的复杂,我只能看懂一点就是使用所谓的default来描述最大多数的情况,然后用其他来描述例外,这样子把一个很多空格的矩阵缩减,但是这个算法非常的复杂,我似乎不应该把精力放在理解这么高深的算法上吧?而且依赖于现代的数据结构和现代的计算机硬件资源似乎存储不是绝对的头条大问题。

十二月二十六日 等待变化等待机会

  1. bison采用的所谓的double displacement的压缩方法我还没有仔细的去理解但是还是有很大帮助的
    1. 它让我更加的容易理解bison的数据结构,这个压缩是有意义的,否则这样上千的状态的巨型sparse matrix的确是耗费很大的存储。
    2. 它让我理解了这里的default action因为我当初看到这个完全不能理解为什么有default,这里是对于所有拥有reduce action的状态选取一个出现频率最多的reduce作为它的default,以便使用一个一维数组来表达这个稀疏矩阵的最大公约,也就是概率最大的行动,我相信这个对于CPU芯片设计肯定是有意义的算法。当然对于只存在shift的状态是没有default的。
    3. terminal的标号理论上是容纳255个ASCII字符的:0保留给了虚拟的$end,而256分配给了error然后基本上是顺序排列下去,字符串似乎不是按照字典的顺序排序,我怀疑有某种hash算法来指导这个顺序。总共数量有一两百个。
    4. nonterminal的顺序和terminal的标号是有重叠的,这一点让我感到意外,这么做的原因也许是允许用户动态增加terminal吗?nonterminal的起始是虚拟的$accept标号是140不知道这个是怎么选取的,然后是所有的nonterminal依靠字典顺序排列。大约有三百多个。
    5. 语法目前有大约七百多条。这个是我不再去除%empty语法保持原本委员会的语法的情况下。
    6. 状态(itemsets)有大约一千三百多个状态,这个标号当然是自然扩展的书序。
    7. double displacement是一个相当巧妙的算法。首先我们要理解我们的目的是什么,我们在使用所谓的default去除了那些只有reduce的状态之后就要对付那些剩下的shift的状态行,我把大师的表增加了行号和列号并且去除了状态号以便帮助查询,也就是说问题转化为如何有效率的用两个一维数组来表达这个稀疏的矩阵!这里有一个隐含条件就是我们只关心有数值的格子,如果你要计算那些空白的部分我们就无能为力了毕竟把二维数组压缩为两个一维数组是不可能表达所有信息的。
      列号/行号012345
      0     1 2    
      1     1 2    
      2 9         8
      3   10        
      3 9          
      4         11  
      5     1 2    
      6     1 2 11  
      7   10        
      大师使用了这样两个数组
      
      D = {4,6,0,0,12,9,8,0,13} and
      T = {9,10,1,2,11,8,1,2,1,2,1,2,9,11,10}
      
      这两个数组的生成我不知道是什么方法,但是它能够依据这个公式来计算我们关心的那些有数值的格子:T[D[row]+col]比如我们知道矩阵row=2,col=0的数值是9,那么公式就是T[D[2]+0]=9
    8. 后面具体的bison的数据结构就很复杂或者说我看不下去,原理上并不一定复杂,但是除非你要去实现否则没有什么关注的价值。
  2. 顺便说一下本地编译的bison如果还没有安装要本地运行的话要使用test/bison因为src/bison会使用系统的skeleton之类的,我之前就遇到这个问题无奈之下只好安装了最新版的bison到我的ubuntu,这个是非常不可取的行为。
  3. 我在文档NEWS里看到很多的信息,这些是最新版3.8的相关信息:
    1. %printer是目前推荐的方式,这个宏YYPRINT已经被放弃了。
      
      The %printer directive defines code that is called when a symbol is reported.		
      	%printer { code } symbols                                                       
      Invoke the braced code whenever the parser reports one of the symbols. Within
      code, $$ (or $<tag>$) designates the semantic value associated with the reported
      symbol, and @$ designates its location.
      
    2. glr2.cc被提到最终将取代glr.cc,大师也自己说还需要用户反馈还不成熟。
    3. 大师鼓励使用yytoken_kind_t取代enum yytokentype
      This type now also includes tokens that were previously hidden: YYEOF (end of input), YYUNDEF (undefined token), and YYerror (error token). They now have string aliases, internationalized when internationalization is enabled. Therefore, by default, error messages now refer to "end of file" (internationalized) rather than the cryptic "$end", or to "invalid token" rather than "$undefined".
    4. 我觉得大师推荐%header来输出头文件。命令行-H.--header虽然有最高优先级,但是我喜欢命令行单一的做法。
    5. 这个命令行--html是推荐的做法?The xsltproc program must be available.
    6. User actions may now use `YYNOMEM` (similar to `YYACCEPT` and `YYABORT`) to abort the current parse with memory exhaustion.
    7. The `YYLOCATION_PRINT(File, Loc)` macro prints a location. It is defined when (i) locations are enabled, (ii) the default type for locations is used, (iii) debug traces are enabled, and (iv) `YYLOCATION_PRINT` is not already defined.
      我要是早读到这个也就不会疑惑了,但是说老实话我之前读这个一定不知所云。
    8. 这个关于GLR traces要怎么理解? There were no debug traces for deferred calls to user actions. They are logged now. log在哪里?
    我注意到了一个现象就是bison3.x是最近两年才活跃的,之前的版本有差不多十几年没有大的改动,应该是很多人看到了它的新的应用?

十二月二十七日 等待变化等待机会

  1. 我昨天理了一下思路感觉glr始终是一个黑箱子一定要想办法看看编译过程把中间文件拿到否则永远不明其所以然,因为文档太少而且这个东西相对比较新,很多细节不足为外人道也。看bison的Makefile的确是不简单,因为很明显的,这个是大行家的风范不是一般的做法,感觉也许GNU最早的一些规矩都和这批前辈高人有关,不少的automake的做法是专门适应bison/yacc的。比如我看到这个AM_V_GEN查手册才知道是内部的函数:
    You can use the predefined variable AM_V_GEN as a prefix to commands that should output a status line in silent mode, and AM_V_at as a prefix to commands that should not output anything in silent mode. When output is to be verbose, both of these variables will expand to the empty string.
    所以,现在看这个命令就明白是怎么回事了:
    
    # Create $@~ which is the previous contents.  Don't use 'mv' here so
    # that even if we are interrupted, the file is still available for
    # diff in the next run.  Note that $@ might not exist yet.
    	$(AM_V_GEN){ test ! -f $@ || cat $@; } >$@~
    
    其实很多事情真的是不说还真的看不透,大师说的很清楚不用mv大师对于文件拷贝cp有什么成见吗?我不太明白其中的奥妙也许cp可以被打断而shell命令cat $@>$@~是一个不可被打断的atomic?而且我一开始不明所以以为$@~是什么特殊变量,现在才知道仅仅是unix的备份文件的惯例,比如vim总是这么做的,而我的老朋友$@也不是automake的专属,而是unix/shell的惯例:

    The difference between the two syntaxes shows up when you have an argument with spaces in it (e.g.) and put $@ in double quotes:

    wrappedProgram "$@"
    # ^^^ this is correct and will hand over all arguments in the way
    #     we received them, i. e. as several arguments, each of them
    #     containing all the spaces and other uglinesses they have.
    wrappedProgram "$*"
    # ^^^ this will hand over exactly one argument, containing all
    #     original arguments, separated by single spaces.
    wrappedProgram $*
    # ^^^ this will join all arguments by single spaces as well and
    #     will then split the string as the shell does on the command
    #     line, thus it will split an argument containing spaces into
    #     several arguments.
    

    Example: Calling

    wrapper "one two    three" four five "six seven"
    

    will result in:

    "$@": wrappedProgram "one two    three" four five "six seven"
    "$*": wrappedProgram "one two    three four five six seven"
                                 ^^^^ These spaces are part of the first
                                      argument and are not changed.
    $*:   wrappedProgram one two three four five six seven
    
    就是说对于多个参数尤其是参数含有空白字符最正确的做法是"$@"
  2. 对于Makefile的哑口不言我已经报怨不止一次了。
    1. 最早是修改文件添加
      • set -x : Display commands and their arguments as they are executed.
      • set -v : Display shell input lines as they are read.
      毕竟不是很干净的做法。 那么这样子make SHELL='sh -x'做是不是overkill呢?这个实际上不是Makefile专属的方式而是所有的shellscripting的方式,噪音肯定是多得多了。 输出大小138K
    2. 那么make -p如何呢?
      
      -p, --print-data-base
                  Print  the  data base (rules and variable values) that results from reading the make‐
                  files; then execute as usual or as otherwise specified.  This also prints the version
                  information  given by the -v switch (see below).  To print the data base without try‐
                  ing to remake any files, use make -p -f/dev/null.
      
      从产生的文件大小来看它比前者还要大!输出大小7.6M
    3. 以前使用
      make --debug=j

      https://linux.die.net/man/1/make

      --debug[=FLAGS]

      Print debugging information in addition to normal processing. If the FLAGS are omitted, then the behavior is the same as if -d was specified. FLAGS may be a for all debugging output (same as using -d), b for basic debugging, v for more verbose basic debugging, i for showing implicit rules, j for details on invocation of commands, and m for debugging while remaking makefiles.

      但是这个毕竟是debug的做法我又不是make的开发者,感觉这个也是有一点点的不好。比如用最基本的-b输出看看大小。这个输出是最少的,但是是否是最清楚的呢?输出大小52K
    4. 那么这个是不是最符合Makefile作者希望你看到的部分呢? make AM_DEFAULT_VERBOSITY=1 这个其实和make V=1或者make VERBOSE=1是等价的,我一开始以为这个开关是环境变量实际上不行。 输出大小132K
    5. 那么make -n这个dryrun如何呢?
      
      -n, --just-print, --dry-run, --recon
      	Print the commands that would be executed, but do not execute them (except in certain
      	circumstances).
      
      虽然它的输出不是最少的,我却觉得相当的干净。输出大小209K
    6. 关于make --trace我感觉还是在debug的味道,输出噪音也不少。输出大小664K
    总结一下就是我喜欢make -n因为它基本上就是输出我关心的命令部分。
  3. 我看了一早上才突然意识到skeleton不是静态编译的而是bison在动态执行过程中产生的,这个在编译过程是看不到的!我的脑子太糊涂了。这个又回到了之前对于scan-skel.l的理解上了,可是在哪里做m4的呢?我找到代码output.c里看样子可以模拟一下运行了!找到一个没有公开的trace方法,就是-T
    
    static const argmatch_trace_arg argmatch_trace_args[] =
    {
      { "none",      trace_none },
      { "locations", trace_locations },
      { "scan",      trace_scan },
      { "parse",     trace_parse },
      { "automaton", trace_automaton },
      { "bitsets",   trace_bitsets },
      { "closure",   trace_closure },
      { "grammar",   trace_grammar },
      { "resource",  trace_resource },
      { "sets",      trace_sets },
      { "muscles",   trace_muscles },
      { "tools",     trace_tools },
      { "m4-early",  trace_m4_early },
      { "m4",        trace_m4 },
      { "skeleton",  trace_skeleton },
      { "time",      trace_time },
      { "ielr",      trace_ielr },
      { "cex",       trace_cex },
      { "all",       trace_all },
      { NULL,        trace_none},
    };
    
    1. 这里为了不使用系统的安装的版本我使用本地编译的bison: ~/Downloads/bison-git/tests/bison -Ttools word.y -o word.cc2仅仅输出这么一行命令行参数running: /usr/bin/m4 --gnu -I /home/nick/Downloads/bison-git/data /home/nick/Downloads/bison-git/data/m4sugar/m4sugar.m4 - /home/nick/Downloads/bison-git/data/skeletons/bison.m4 /home/nick/Downloads/bison-git/data/skeletons/glr2.cc
    2. 比如bison -Tskeleton就是在输出scan-skel.l的trace情况。这个纯粹是flex的情况,似乎我不应该太关心。
    3. m4-early,m4基本就是m4的宏替换部分,我还到不了那个关心程度。
    4. time大致上就是运行time m4的样子。
    5. scan是扫描用户.l的trace
    6. locations,resource,cex应该是还没有实现。看代码应该也是。
    7. grammar,automaton, losure,sets对于分析.y语法是非常的有用的,当然大部分都是我以前看到过的。parse也是最广泛应用的,不过相当的复杂。
    8. bitsets顾名思义除非你专门感兴趣的话?
    9. muscles可能是最接近我想要看到的吧?实际上我也不知道要看什么关键是可能也看不懂。
    我隐约记得文档里提到glr-trace是在log文件里,但是怎么做呢?估计只能看代码了。
  4. 什么是muscle呢?
    The bits of information passing from bison to the backend is named "muscles". Muscles are passed to M4 via its standard input: it's a set of m4 definitions. To see them, use --trace=muscles.
    我现在已经知道命令行就是这个,而且这个就是通过stdin的,那么内容是什么呢?我如果把muscle的输出传入如何呢? 确实是这样子的,我把-Tmuscles的输出存储正文件去掉不相干的部分,然后用这个命令行就得到了正宗的glr2.cc的源码!现在我有一点点明白为什么是叫做muscle了,因为我们写的.y文件就是这些muscle,而bison产生的代码就是骨架所以是skeleton,而这些总是相对于我们的肉不同而产生的不同的骨架,这里有大量的宏替换所以才用到m4。要看懂这些代码对于bison的数据结构是一定要清楚的,所以,我估计还是要回到之前的那个教授的关于bison压缩表的讲解。不过我今天还是有收获的,至少有了一个方向!我今天可以好好睡一觉了吗?应该是吧?
  5. 雄关漫道真如铁,而今迈步从头越!我竟然忘了昨天是毛主席他老人家诞辰日。

十二月二十八日 等待变化等待机会

  1. 我昨天以为我发现了什么实际上几乎什么都没有。整个过程目前我看到的是这样子的,首先,用户定义的.y文件被制作成所谓的muscle实际上就是一些以一定格式定义好的作为m4的操作对象,这个包含了用户定义的语法经过转换后的各种元素,我猜想包含了各种表等等数据结构,但是他们是要被m4填补到所谓的预先设计好的所谓的skeleton比如glr2.cc里面。我觉得我可能是理解力的问题,这么浅显的道理从名字都能猜出来,为什么我费了这么多力气才明白?这就是我昨天看到的,也就是说使用-Ttools看到了m4的执行的参数,这个是使用所谓的stdin的原因是bison的output.c里运用了pipe来和m4这个子进程来交互,也就是说创建一个m4的这样一个子进程于是m4就在pipe的那一头读,而bison着一头写入muscle,很自然的m4在另一头写结果,而这里一个细节我昨天没有意识到就是bison这里读结果使用了另一个flex就是scan-skel.l产生的代码来过滤,这就是为什么我如果仅仅看m4的输出结果还有好些宏没有替换比如basename。这其中的机制当然是很复杂的,单单使用m4通过pipe就是一个复杂的过程,我注意到m4是可以直接通过文件输入的,当然编程是不能接受这个做法的。我现在也明白了另一个细节就是为什么bison需要自带一个gnulib这么麻烦,原因就是它用到了很多如pipe以及创建子进程的syscall,而bison需要自持不依赖于任何平台的库,也就是跨平台跨语言实现,所以只好自己统统实现一遍,否则但从算法看没有必要把系统库都带着。
  2. 总之,我看清了一点点,目前我也明白了glr的算法就是存在于产生的代码里没有必要去看充满了宏的skeleton的glr2.cc除非你是开发者,这个只有精通m4的大师才有能力,这个无疑是很大的一个门槛,我想bison的开发是一个很难的挑战因为skeleton的代码本身就是非常的复杂还要使用m4来替换,这些是非常不容易的。不过略微窥探glr的算法还是可能的,就是通过产生后的代码。
  3. 在yy名字空间有一个类glr_stack它的很多方法就揭示了原理,这个是我重点要看的。它的很多私有方法是我感兴趣的。

十二月二十九日 等待变化等待机会

  1. 要读懂glr代码有一些要先搞清楚的,首先是setjmp/longjmp对于这个我从来就没有搞明白因为一开始就得到印象是不提倡的,实际中看到了就绕开,现在至少要明白其基本的longjmp的参数是设定setjmp的返回值来区分状态的概念。
  2. 其次是这个token/symbol始终是混淆的也许英文意思在我看来是一样的,不过token_kind_type特指terminal,而symbol_kind_type包含了terminal和nonterminal,所以,它作为c++接口的yylex的返回值类型。值得注意的是几个特殊预设值除了YYEMPTY=-2,YYEOF=0在两者重复定义了以外,其他S_YYerror,S_YYUNDEF重复定义值是不一样的!在后者作为yylex的返回值是连续的enum因为不需要考虑前者兼容255个ASCII字符值的羁绊,而且命名也很简单:都是.y文件里的terminal/nonterminal直接加上一个前缀S_,遇到bison允许的hyphen的情况就都改为underscore摒弃改前缀为S_number_,这里的number是当前的symbol_kind的enum序列号。最后就是一个特殊的值YYNTOKENS出现在第一个实际上的值是所有symbol的个数。这里它会和某一个七它的symbol值冲突因为总的enum还包含了那些特殊定义的,所以,要小心。
  3. 也许我对于c++的move的理解不够深刻我一开始感觉大师的做法有些overkill比如这个
    
    void parser::by_kind::move (by_kind& that){
        kind_ = that.kind_;
        that.clear ();
    }
    
    后来才体会到作为预留将来扩展这个是非常必要的,而实际上这个体现了对于c++move的深度,怎么实现才是关键!不是说你调用了std::move();就万事大吉等着天上掉馅饼而是要自己去真的做到让move起到应有的swap的作用!目前尽管by_kind是一个仅仅包含一个enum的空结构可是谁能保证它将来不扩展呢?我还是看不太清楚也许这个move改名为swap更好?因为它和标准的move意义混淆了?因为我的理解是真正的std::move是在对于rvalue-reference做的一种优化,可以在根本不增加任何运行期成本的情况下达到对于保证没有后遗症的常量作为再利用。因为区分rvalue-reference是编译期依赖模板类性的perfect-forwarding做到选择重载调用了相应的swap实现。那么大师这里是实现所谓的swap为真正的rvalue-reference的类型作为实现吗?这个问题比较复杂随后留心吧?
  4. 大师的核心代码在函数parse()里,这里是唯一设定了setjmp的地方,那么相应的我们看看在哪里调用了longjmp呢?一个是yyMemoryExhausted另一个是yyFail,所以,我们可以理解这是某种类似于try/catch的机制实现?前者返回值为2,后者为1,那么针对两个返回值分别跳转到相应的处理,不过这些都是意外的处理,真正的逻辑在下面。
  5. 第一步就是进入状态机的第一个0状态:this->yyglrShift (create_state_set_index(0), 0, 0, this->yyla.value);这个看起来没有多少的悬念,我摘录注释我唯一的报怨是这里使用shift让我感到困惑因为我还以为是某个terninal采用shift,状态不是用goto吗?
    
     /** Shift to a new state on stack #YYK of *YYSTACKP, corresponding to LR
         * state YYLRSTATE, at input position YYPOSN, with (resolved) semantic
         * value YYVAL_ARG and source location YYLOC_ARG.  */
        void
        yyglrShift (state_set_index yyk, state_num yylrState,
                    size_t yyposn,
                    const value_type& yyval_arg)
    
    这里我的疑问是glr是否要重新实现一遍LR的各种状态机制还是用继承式的修改呢?不过这个想法很愚蠢状态就是一个数字没有什么继承的机制,无非是各个函数重新实现而已。 这里我注意到一个细节就是我定义的sematic的高级的数据类型bison只能像c语言类似实现variant的方式用指针来传递,比如我定义了st::variant的高级类型,但是bison还是使用指针类对付所有的这些哪怕它不是variant,我看到过bison的文档解释说bison的实现不想依赖于boost::variant的实现,大概那个时候c++标准还没有采纳variant的实现吧?说白了就是怎么实现value_type的各种操作的问题,比如copy和创建传参数等等。
  6. 我现在体会到要真正理解bison的算法就要再次结合教授的讲解,因为首先是选择有default-action的运作,所谓的default-action就是一个default的reduce的状态转换,这里特别的default返回0是一个错误的状态要退出。这里要体会教授当初讲解的时候说的为什么我们可以使用default?这一点我以前一直不理解,比如你有好多不同的reduce你怎么能够选取一个作为default呢?难道不会错吗?我觉得这个一定在芯片设计里的predict有异曲同工之妙:只要你没有shift新的nonterminal你随便reduce将来一定会出错的,这个是由deterministice state automaton决定的!你以为一个傻子偶然随便一指碰巧得到了正确的选择那么你会一直这么好运气吗?不可能哪怕你一直有好运气你在错误的路口犯一次错误就死定了。但是这个有什么好处呢?我的理解就是先不用思考直接道路依赖走你认为最可能的道路,等到你碰壁在回头去做recovery,这个也许在实践中是有高效率的吧?因为大多数时候人凭本能而不是每一步都确定计算要快的多,回头调整反而总的时间更短,这个是我的理解,否则intel/AMD的CPU为何要做那种预测转向?只要你能区分真正的错误和这种投机取巧的default-action的错误就好了。
  7. 另一个大函数是yyglrReduce我摘录注释
    
       /** Pop items off stack #YYK of *YYSTACKP according to grammar rule YYRULE,
         *  and push back on the resulting nonterminal symbol.  Perform the
         *  semantic action associated with YYRULE and store its value with the
         *  newly pushed state, if YYFORCEEVAL or if *YYSTACKP is currently
         *  unambiguous.  Otherwise, store the deferred semantic action with
         *  the new state.  If the new state would have an identical input
         *  position, LR state, and predecessor to an existing state on the stack,
         *  it is identified with that existing state, eliminating stack #YYK from
         *  *YYSTACKP.  In this case, the semantic value is
         *  added to the options for the existing state's semantic value.
         */
        YYRESULTTAG yyglrReduce (state_set_index yyk, rule_num yyrule, bool yyforceEval)
    
    这个函数还是很复杂的要慢慢才能理解。状态机里就只有两个动作shift和reduce可是reduce比shift复杂不知道多少倍!看到新的token当然可能性是少很多了,复杂在于凭借目前的信息来预测和推理没有看到的,这样子的工作才是复杂的,单单识别客观世界反映现实还是一个比较初等级的工作吧?
  8. symbol_type yyla;几乎就是万能的,它就是当前处理的一切,它的kind()/value()/location(),
  9. 这里我看到了一个简单的语法是如此的容易产生歧义,其中一个核心就是不要在超过一个以上的地方出现%empty,你要表达一个循环当然要使用%empty,但是这个是循环的功能,除此之外。。。给我的感觉就像是军队领导体制一样要精干不要有越级现象,而要各司其职,不要有冗余。这个感觉说起来容易做起来真的好难。比如委员会的这个语法你敢质疑它的冗余度吗?这个语法只是为了说明想法并不是一个面向工作的精干的做法,描述同样的客观事物可以有容易的做法也可以有很困难和很精确的做法。前者一言以蔽之,大而无当四平八稳肯定不会出错仿佛大首长的官话套话没人敢说不对然而对于实际工作的指导鲜有所指;而后者精准犀利切中时弊深入浅出,而且更重要的要做到西方法庭宣誓的 the truth, the whole truth and nothing but the truth就是说覆盖了所有的情况但是又不覆盖任何不需要的情况,这个实在是很难。
  10. 每当我以为我明白了很多就遭到当头一棒,我连最基本的lexical conventions都没有理解,为什么我之前测试我的lexer的程序没有发现这些问题呢?比如header-name我一度认为是在预处理中就消灭了,可是新的module的语法要怎么办?真的是一头冷水。

十二月三十日 等待变化等待机会

  1. 我一下子陷入混乱因为这里面的确很复杂。首先我想仿照c++的接口做一个包含lexer的路径。官方的样板里有一个用户定义的location实现,我不想费劲想要使用默认的结果遇到很多编译错误,最后不知道怎么样子的又消失了:
    1. 首先在.y文件里我定义了如下的选项
      
      %skeleton "glr2.cc" // -*- C++ -*-
      %require "3.8.2"
      %header
      %define api.value.type variant
      %define api.token.constructor
      
    2. 因为我使用了api.token.constructor的选项于是lexer文件里我需要这么一些东西就是对于每一个token,我都要返回yy::parser::make_TOKENNAME();,这里我不打算自定义location而且我也没有在.y文件里定义选项%locations,于是这些token不需要location参数而使用默认实现。
    3. 但是我遇到一个这样的错误
      
      parser.hh: In constructor ‘yy::parser::value_type::value_type()’:
      parser.hh:135:20: error: ‘YY_NULLPTR’ was not declared in this scope
             , yytypeid_ (YY_NULLPTR)
                          ^~~~~~~~~~
      parser.hh: In member function ‘void yy::parser::value_type::destroy()’:
      parser.hh:293:19: error: ‘YY_NULLPTR’ was not declared in this scope
             yytypeid_ = YY_NULLPTR;
                         ^~~~~~~~~~
      
      我只好被迫在.y文集里加上一个定义
      
      # ifndef YY_NULLPTR
      #  if defined __cplusplus
      #   if 201103L <= __cplusplus
      #    define YY_NULLPTR nullptr
      #   else
      #    define YY_NULLPTR 0
      #   endif
      #  else
      #   define YY_NULLPTR ((void*)0)
      #  endif
      # endif
      
    4. 在.y文件里我对于没有semantic需求的token仅仅定义了它们的alias以便打印输出好看,而对于有赋值需求的terminal token,我要使用tag比如 %token <std::string> IDENTIFIER "identifier"这里我记错了,对于terminal你可以赋予一个alias,而对于%nterm你是不能给alias的。
    5. 最最核心的其实是yylex这个函数要如何定义,因为在c语言接口下这个就是很简单的返回整数不带参数的int yylex()由flex帮你去实现,可是在c++接口下这个函数变成了yy::parser::symbol_type yylex (),而且似乎你要让flex和bison都知道你要这么定义,这样子就最好定义在一个单独的头文件里让.l和.y都引用,而且把它定义为这个宏YY_DECL
    6. 另一个对于c++类的方式的烦人的问题就是一个所谓的driver的概念,如果使用c++的glr2.cc之类的skeleton那么就意味着我们的parser是一个在名字空间yy的类parser我们需要调用它的parse()成员函数看它的返回值如何。但是这里有一个传递lexer输入文件参数的问题。之前的c语言接口我都是使用flex定义的全局变量yyin那么现在我们的parser类如果能够自己传递这个文件名参数给lexer就好了,所以,我们在.y文件里定义了我们想把我们用户定义的driver类实例传递给我们的parser的constructor %param { driver& drv } 这样子在产生的代码里parser::parser (driver& drv_yyarg) 同样类似的我们也希望把这个driver类传递给yylex,我记得原本有一个参数%lex-param,但是我觉得这个是同时做%parse-param
      
      %param { argument-declaration } . . .                                           [Directive]
      Specify that argument-declaration are additional yylex/yyparse argument declaration.
      This is equivalent to 
      %lex-param {argument-declaration} ...
      %parse-param {argument-declaration} ...
      You may pass one or more declarations, which is equivalent to repeating %param.
      
      同时把我们自定义的driver类作为参数传递给lexer和parser的用意在lexer方面是为了接受用户输入的输入文件名来设定yyin以便开始扫描,同时对于一些我们无法指望bison自动定义api.token.constructor选项的我们要帮助实现。这里bison会自动根据token的名字实现一个简单的symbol_type的constructor,比如我们定义了这个IDENTIFIER的token,而且它的type tag是std::string,那么bison就会使用这个类型作参数返回一个这样的symbol:
      
      static symbol_type make_IDENTIFIER (std::string v)
      {
      	return symbol_type (token::IDENTIFIER, std::move (v));
      }
      
      这里的token::IDENTIFIER是flex使用的enum而bison在c++接口下使用一个更高级的symbol_type来包装它,这就是为什么我们需要调用一系列这种make_TOKENNAME函数的原因。 对于我们不在于semantical value的无类型的token那么bison产生的就是一个空空的无参数的constructor
      
      static symbol_type make_RPAREN ()
      {
      	return symbol_type (token::RPAREN);
      }
      
      它在.l文件里的就是这么简单的调用而已
      
      ")"        return yy::parser::make_RPAREN();
      
      那么我们在parser着一头传递我们自定义的driver类的目的是什么呢?当然是因为我们需要收集每个token的semantic value了,这个也就是c++接口最吸引人的地方,因为在c语言接口下使用union要繁琐的定义实在是很头疼的,比如我在GCC里看到的那个浩如烟海的巨大的union的森林s是能够把人折磨死了,不是说不好因为它是存储访问效率最高的做法,但是依赖于巨大数量的宏来访问是非常的难以维护以及debug的,我依赖代码注释里学习摸索了几个月也仅仅懂了很少数的常用的宏,还有巨大数量的union/macro藏在某些注释里,在这种没有办法维持的模式下开发实在是太困难了。我甚至常常认为最早的开发者是有一个观念大概c++语言在c++98就被封存因此他们的代码将成为不朽,而没有后人需要再去改变添加因为这个正如同历史终结论一样在当时很有说服力。那么新时代的语言引入了variant据说是一个更加安全的union,这个当然是使用bison/c++接口的期望,不过bison内部并非纯粹的c++的实现而是使用指针来包藏复杂的数据类型来实现一个variant,但是这个应该不是问题吧?总之在我们的driver里面隐藏这些数据结构来承接各个token的sematic值是这么做的最主要的目的吧? 这也就是我需要定义这个宏的原因,我之前遇到的种种的问题很多都是来源于这个宏
      # define YY_DECL yy::parser::symbol_type yylex (driver& drv)
      这里在yylex里传递我们这个driver。那么在parser那一端我们要如何告诉bison我们要传递我们driver给parser呢?首先就是定义的%param { driver& drv },然后在我们的driver里是这么调用的:
      
      yy::parser parse (*this);
      
      这里的this当然是我们的driver了。
    因为bison/flex以及我们自己的代码是错综交织的分布在好几个不同的时空,我的脑子也是纷乱无绪的,常常是错乱的。而这一系列的代码很多是自动产生的可想而知其中的复杂,我看手册的讲解就看的头昏脑胀,因为这里有太多的学习难点,很多不实践完全无法体会,我。。。
  2. 先把自己实验的例子保存一下再更新我的代码吧?我还是不太清楚。

十二月三十一日 等待变化等待机会

  1. 依照昨天的结果依样画葫芦总算成功编译不再有错误和警告,当然conflict是不可能解决的,准备下一步再做一个driver类来使用semantic值来处理。遇到.l之前爆出警告说关于spaceship<=>总是不能reach这个让我疑惑了很久最后把它放在第一位才解决,总是flex是能够发现你的regex过多的覆盖其他token的问题,我感觉这个不是最长match 的问题,因为flex的行号总是多一个让我一开始有这种困惑。这个也许可以算是一个里程碑吧?尽管几乎什么也没有做到,但是毕竟编译成功了。
  2. 还有一个问题就是如果我认真阅读手册就不太需要昨天那么辛苦的自我总结了,不过很多时候学习单靠阅读反而不够印象深刻。 我对于我是否可以使用这个不敢确定
    Directive: %define api.token.raw
    • Language(s): all
    • Purpose: The output files normally define the enumeration of the token kinds with Yacc-compatible token codes: sequential numbers starting at 257 except for single character tokens which stand for themselves (e.g., in ASCII, ‘'a'’ is numbered 65). The parser however uses symbol kinds which are assigned numbers sequentially starting at 0. Therefore each time the scanner returns an (external) token kind, it must be mapped to the (internal) symbol kind.

      When api.token.raw is set, the code of the token kinds are forced to coincide with the symbol kind. This saves one table lookup per token to map them from the token kind to the symbol kind, and also saves the generation of the mapping table. The gain is typically moderate, but in extreme cases (very simple user actions), a 10% improvement can be observed.

      When api.token.raw is set, the grammar cannot use character literals (such as ‘'a'’).

    • Accepted Values: Boolean.
    • Default Value: true in D, false otherwise
    • History: introduced in Bison 3.5. Was initially introduced in Bison 1.25 as ‘%raw’, but never worked and was removed in Bison 1.29.
    这里也是我早上烦恼了很久的东西,一个是token_kind一个是symbol_kind,前者应该是flex使用的enum定义的是lexer范畴可见的概念,后者是parser重新map的中间不再预留给ASCII字符256空间的连续enum,而且bison针对它重新命名添加前缀S_,这个之前我已经注意到了,这个指示可以修改默认的前缀:%define api.token.prefix {TOK_},之前我还没有明白这个指示的意义现在才彻底明白,就是说bison为flex产生的enum的token_kind的前缀加了TOK_,而bison自己产生的parser的symbol_kind的前缀依旧是S_这个没有亲自做实验还是一知半解理解错误,好在这个token_kind我们基本不会用到,原因是我们使用的yylex返回的已经是symbol_kind,而它使用的成员函数返回kind()的enum就是那个加前缀S_的symbol_kind,我们不会再用到flex的原始的token_kind,不过我的疑问其实是在于我是否保证我的语法中不会再有使用ASCII字符的问题?比如纯虚函数最后声明=0,这个0我是否也定义了?我感觉不行,因为我的语法里依旧把它定义为字符'0',所以,还是不要着急盲目。
  3. 使用操作符优先级的第一步就遇到基本的问题,你怎么能够区分在不同上下文里不同的操作符呢?作为乘法和dereference的terminal token是一样的*可是我的语法怎么表达它们是两个不同的操作符呢?这个方向一开始就是注定不可能的。我注意到这一类有重复的操作符它们的左右结合性是相反的,也就是说作为乘法的*Left-to-right 而作为dereference的时候是Right-to-left ,但是这个基本上也无法帮助因为即便我只定义所有的Left-to-right的操作符的优先级仅仅减少了三个冲突,而整个的shift/reduce仅仅从1925减少到1922,这个简直就是无意义的。
  4. 期盼

    十四亿人众志城, 百年党庆踏新征。 神州终将成一统, 万事具备盼东风。
  5. 有感于中美大乱斗世界不太平,看来明年又是一个斗争的年头

    又是年尾接年头, 东边欢喜西方愁。 当春不撒幸福种, 坐待秋来收恨仇。
  6. 岁末有感

    岁末年初夜色稠, 回首一载添乡愁。 全球变暖疫情乱, 地缘政治一锅粥。 大国崛起冲霄汉, 美帝岂有善罢休? 生逢乱世何所幸? 唯有亲情记心头。

Smiley face