对我来说,它始于六年半前,当时,由于缘分,我被吸引到一个封闭的项目中。 谁的项目-不要问,我不会告诉你。 我只能说他的想法很简单:将clang前端嵌入到IDE中。 好吧,就像最近在QtCreator中,在CLion中(某种意义上)在CLion等中所做的那样。Clang当时是一颗冉冉升起的新星,许多人都在为最终几乎免费使用成熟的C ++解析器的可能性而苦恼。 可以这么说,这个想法实际上是悬而未决的(而内置在clang API中的代码的自动完成是Be所暗示的),您只需要接受并执行它即可。 但是,正如Boromir所说,“您不能随便拿走,然后……”。 因此在这种情况下发生了。 有关详细信息-Wellcome(在猫下)。
首先关于好
当然,使用clang作为IDE C ++中的内置解析器有很多好处。 最后,IDE功能不仅限于编辑文件。 这是一个字符,导航任务,依赖项以及更多内容的数据库。 在这里,成熟的编译器将其发挥到了极致,因为在一个相对简单的自编写解析器中克服预处理器和模板的所有功能是一项艰巨的任务。 因为您通常必须做出很多妥协,这显然会影响代码解析的质量。 谁在乎-可以看一下QtCeator的内置解析器: Qt Creator C ++解析器
在同一地方,在QtCreator的源代码中,您可以看到以上内容并不是IDE解析器所需的全部。 另外,您至少需要:
- 语法高亮(词汇和语义)
- 在符号上显示信息的各种提示“实时”
- 提示代码有什么问题以及如何修复/补充代码
- 多种情况下的代码完成
- 最多样化的重构
因此,在前面列出的好处(真的很严重!)上,优点结束了,痛苦开始了。 为了更好地理解这种痛苦,您可以首先查看Anastasia Kazakova( anastasiak2512 )的报告,其中介绍了IDE内置的代码解析器实际需要的内容:
问题的实质
但这很简单,尽管乍看之下可能并不明显。 简而言之,clang是一个编译器 。 并将代码称为编译器 。 并因已将代码提供给他的事实(而不是现在在IDE编辑器中打开的文件的存根)而更加清晰。 编译器不喜欢文件的某些部分,例如不完整的构造,不正确地编写的标识符,重新运行而不是返回,以及不时出现在编辑器中的其他乐趣。 当然,在编译之前,所有这些都将被清理,修复和整合。 但现在,在编辑器中,它就是它。 IDE内置的解析器就是通过这种形式每5-10秒到达一次表。 如果它的手写版本完美地“理解”了它正在处理半成品,那么会发出叮当声-不。 非常惊讶。 他们说,由于这种意外而发生的事情取决于“取决于”。
幸运的是,clang可以容忍代码错误。 但是,可能还会有一些意外-背光突然消失,自动完成曲线,奇怪的诊断。 您需要为此做好准备。 另外,c不是杂食性的。 他有权不接受编译器标头中的任何内容,编译器标头现在和此处都用于构建项目。 棘手的内在函数,非标准扩展名和其他(嗯...)功能-所有这些都可能导致在最意想不到的位置解析错误。 当然,还有性能。 在Boost.Spirit上编辑语法文件或在基于llvm的项目上工作将是一件乐事。 但是,关于所有细节的细节。
预制代码
因此,假设您开始了一个新项目。 您的环境为main.cpp生成了一个默认空白,并在其中写入:
#include <iostream> int main() { foo(10) }
坦率地说,从C ++的角度来看,该代码是无效的。 文件中没有函数foo(...)的定义,该行未完成,等等。但是...您才刚刚开始。 此代码具有此类型的权利。 此代码如何感知带有自定义解析器(在本例中为CLion)的IDE?

如果单击灯泡,则可以看到以下内容:

这样的IDE,了解一些事情,更多地了解正在发生的事情,提供了非常期望的选择:从使用的上下文中创建一个函数。 我认为很好的报价。 基于the的IDE的行为如何(在本例中为Qt Creator 4.7)?

为了纠正这种情况又提出了什么建议? 但是什么都没有! 仅标准重命名!

出现这种情况的原因非常简单:对于clang来说,此文本是完整的(不能再有其他任何内容)。 并且他基于此假设构建了AST。 然后一切都很简单:clang看到一个以前未定义的标识符。 这是C ++(不是C)中的文本。 没有对标识符的性质做任何假设-未定义标识符,因此一段代码无效。 在AST中,此行什么也没有出现。 她只是不在那里。 AST中没有的东西是无法分析的。 真是可惜,烦人,好。
IDE中内置的解析器来自其他一些假设。 他知道代码还没有完成。 程序员现在正在仓促思考,而背后的手指没有时间。 因此,并非所有标识符都可以定义。 从高标准的编译器质量的角度来看,这样的代码当然是不正确的,但是解析器知道使用这样的代码可以做什么,并提供了选项。 相当合理的选择。
至少在3.7版(含)之前,此代码中发生了类似的问题:
#include <iostream> class Temp { public: int i; }; template<typename T> class Foo { public: int Bar(Temp tmp) { Tpl(tmp); } private: template<typename U> void Tpl(U val) { Foo<U> tmp(val); tmp. } int member; }; int main() { return 0; }
在模板类方法内部,基于clang的自动完成功能不起作用。 据我设法找出的原因是模板的两次遍历解析。 当有关实际使用的类型的信息可能不足时,会在第一遍触发clang中的自动完成功能。 在clang 5.0(根据发行说明判断)中,此问题已修复。
一种或另一种情况是,编译器无法在已编辑的代码中构建正确的AST(或从上下文中得出正确的结论)。 并且在这种情况下,IDE根本不会“看到”文本的相应部分,并且将无法以任何方式帮助程序员。 当然,这不是很好。 在IDE中解析器需要的是使用错误代码有效工作的能力,而常规编译器根本不需要。 因此,IDE中的解析器可以使用许多启发式方法,这对于编译器不仅是无用的,而且是有害的。 并且要在其中实现两种操作模式-好的,您仍然需要说服开发人员。
“这个角色是侮辱性的!”
程序员的IDE通常是一个(两个),但是有很多项目和工具链。 而且,当然,我不想做任何额外的手势来从工具链切换到工具链,从项目切换到项目。 一两次单击,然后将构建配置从“调试”更改为“发行版”,将编译器从MSVC更改为MinGW。 但是IDE中的代码解析器保持不变。 而且他必须与构建系统一起,从一种配置切换到另一种配置,从一种工具链切换到另一种。 工具链可以是异国情调的,也可以是交叉的。 解析器的任务是继续正确解析代码。 如果可能,错误最少。
lang杂食。 可以强制它接受来自Microsoft的gcc编译器扩展。 可以采用这些编译器格式传递选项,而clang甚至可以理解它们。 但这并不能保证clang会接受gcc坦克收集的内脏的任何标题。 任何__builtin_intrinsic_xxx都可能成为他的绊脚石。 或语言构造不支持IDE中的当前clang版本。 这很可能不会影响当前编辑文件的AST构造质量。 但是建立全局字符库或保存预编译的标头可能会失败。 这可能是一个严重的问题。 可能不是在工具链或第三方的标题中,而是在项目的标题或源代码中,类似的问题可能是相似的代码。 顺便说一句,所有这些充分重要的理由足以明确告知构建系统(和IDE)项目的哪些头文件是“异类”。 它可以使生活更轻松。
同样,IDE最初被设计为可与不同的编译器,设置,工具链等一起使用。 设计为必须处理代码,其中某些元素不受支持。 IDE的发布周期(并非全部:)比编译器要短,因此,有可能更快地提取新功能并响应发现的问题。 在编译器世界中,所有内容都略有不同:发布周期至少需要一年,交叉编译器兼容性问题可以通过条件编译解决,并传递给开发人员。 编译器不必通用且杂乱无章-它的复杂性已经很高。 lang也不例外。
为速度而战
那部分时间花在IDE上,当程序员不坐在调试器中时,他编辑文本。 他在这里的自然愿望是使它变得舒适(否则,为什么要使用IDE?我可以使用记事本吗!)舒适性尤其涉及编辑器对文本更改和按热键的快速反应。 正如Anastasia在她的报告中正确指出的那样,如果在按Ctrl + Space键五秒钟后,环境没有出现菜单或自动完成列表的响应,那就太糟糕了(严重的是,请您自己尝试一下)。 从数量上讲,这意味着内置在IDE中的解析器大约有一秒钟的时间来评估文件中的更改并重建AST,而另一半则是一两个半的时间来为开发人员提供上下文相关的选择。 第二。 好吧,也许两个。 此外,预期的行为是,如果开发人员更改了.h昵称,然后又切换到了.cpp-shnik,则所做的更改将是“可见的”。 这些文件在这里在相邻窗口中打开。 现在进行简单的计算。 如果从命令行启动的clang可以在大约十到二十秒内处理源代码,那么为什么有理由相信从IDE中启动它可以更快地处理源代码并适合第二或第二个源代码呢? 也就是说,它将更快地工作一个数量级吗? 总的来说,这可以完成,但我不会。
我夸大了大概十到二十秒的时间。 虽然,如果其中包含一些繁重的API,或者说是包含准备好Hana的boost.spirit,然后在文本中积极使用所有这些,那么10到20秒仍然是不错的选择。 但是,即使AST在内置解析器启动后的三到四秒内准备就绪,也已经很长时间了。 前提是此类启动应是常规的(以将代码模型和索引保持在一致的状态,突出显示,提示等状态)以及按需启动-代码完成也是编译器的启动。 是否有可能减少这种时间? 不幸的是,在使用clang作为解析器的情况下,可能性不大。 原因:这是第三方工具,在其中不能进行更改( 理想情况下 )。 也就是说,使用perftool深入研究clang代码,优化,简化某些分支-这些功能不可用,您必须与外部API提供的功能有关(在使用libclang的情况下,它也非常狭窄)。
第一个,显然也是,实际上,唯一的解决方案是使用动态生成的预编译头。 通过适当的实施,解决方案将成为杀手er。 至少有时提高编译速度。 它的本质很简单:环境将所有第三方标头(或项目根目录外部的标头)收集到单个.h文件中,从该文件中创建pch,然后在每个源中隐式包含此pch。 当然,会出现明显的副作用:在源代码中( 在编辑阶段 ),可以看到未包含在其中的符号。 但这是速度的代价。 我必须选择 如果不是一个小问题,一切都会很好:clang仍然是编译器。 而且,作为编译器,他不喜欢代码错误。 并且,如果突然(突然!-参见上一节)标题中存在错误,则不会创建.pch文件。 至少在3.7版之前是正确的。 从那以后,这方面有什么变化吗? 我不知道,有人怀疑没有。 las,不再有任何检查的机会。
,由于相同的原因,无法使用其他选项:clang是编译器,并且是“本身”的东西。 积极干预AST生成过程,以某种方式使它从不同的部分合并AST,维护外部符号库以及te te te te te-te,所有这些功能都不可用。 通过编译选项只能使用外部API,只有核心和设置。 然后分析产生的AST。 如果您使用API的C ++版本,那么将有更多机会。 例如,您可以使用自定义的FrontendAction进行操作,为编译选项进行更精细的设置等。但是在这种情况下,要点不会改变-编辑(或索引)的文本将独立于其他文本而完全地被编译。 仅此而已。 重点。
也许(也许!)总有一天,会有一个上游clang的分叉,专门用于IDE。 可能吧 但是目前,一切都保持原样。 假设Qt Creator团队与libclang的整合(到“最终”阶段)花了7年的时间。 我使用基于libclang的引擎尝试了QtC 4.7-我承认,我个人更喜欢旧版本(在自己编写的版本上),只是因为它在我的情况下效果更好:提示和突出显示,以及其他所有功能。 我不会承诺估计他们在此集成上花费了多少工时,但我敢建议在这段时间内可以完成自己的解析器。 据我所知(通过间接指示),致力于CLion的团队谨慎地考虑与libclang / clang ++集成。 但是这些纯属个人假设。 在Language Server Protocol级别进行集成是一个有趣的选择,但是特别是对于C ++情况,由于上述原因,我倾向于将其更多地视为姑息治疗。 它只是将问题从一个抽象级别转移到另一个抽象级别。 但是也许我误会了LSP-未来。 让我们看看。 但是无论如何,现代C ++的IDE开发人员的生活充满了冒险-将clang作为后端,也可能没有clang。