关于编译器警告设计的两点思考

2020-09-03 08:58:47

今天,我终于写下了一些关于编译器诊断设计的两个外围方面的思考,我认为这两个方面对某些类型的警告非常重要,我的第一个思考是关于修复程序,第二个是关于抑制机制。

现在大多数C++编译器并不只是告诉你发生了问题(就像ed的有名的?提示符),以及问题是什么(例如,“预期标识符”),他们会试图告诉您如何解决问题。

将GCC 4.1的错误消息与GCC 4.6的错误消息进行比较。(Godbol.)对于此输入:

GCC 4.1只是抱怨您在一个声明中放入了多个类型,而GCC4.6更有帮助的是抱怨它应该在结构定义之后,并指出它认为应该插入分号的确切位置。

GCC 7更进一步,他建议了一个补丁--一个机器可读的“拼写检查建议”,一些IDE只需点击一下就可以应用。

用医学术语来说,编译器已经从简单地检测症状(“这个声明似乎包含两种类型,这不是有效的C++”)变成了对疾病的诊断(“我打赌您把分号忘在什么地方了”);而支持修复的编译器实际上是在开一个治疗计划。这些步骤中的每一个在用户友好性和实现复杂性方面都比前一个步骤有了质的飞跃。

每个主流编译器都会对这个可疑的代码发出警告(GCC和碰壁;msvc带-W4)。以下是GCC的诊断:

警告:建议使用圆括号将赋值用作真值[-Wparenths]4|IF(argc=1)|~^~~

请注意,GCC甚至没有费心解释它的真正诊断是什么;它发出了一个被动的、攻击性的警告信息,除了建议如何关闭自己之外,什么也做不了!

当然,诊断结果是人们经常想写if(argc==1),但是他们的手指滑倒了,他们改写if(argc=1),不幸的是,这也是有效的C++代码。每个主流编译器都发展了相同的方法来处理这一问题:如果您真的想要=,那么您可以在表达式两边多加一对括号,如下所示:

额外的一对括号就是我所说的“抑制机制”,它只是一种使警告诊断静音的任意方式-告诉编译器“不,我真的想要编写这个合法但不寻常的构造。”

另一种众所周知的抑制机制是在未使用的结果前面写入(Void),以使Wunused值和WunusedResult警告(Godbolt)静音:

在这里,编译器只想让您添加一对括号,以便为读者阐明优先顺序。

冥想:单个补丁必须保留(实际的)行为或(可能的)意图,但不能两者兼而有之。

程序员很容易“修复”像-Wunuse-result和-Wlogic-op-括号这样的诊断,因为他们只是要求程序员澄清已经非常清楚的意图:是的,我的意思是丢弃这个结果。是的,我是指评估(a&;&;b)||c。

更有趣的诊断是编译器认为它发现了代码的实际行为和程序员的意图之间的不匹配。我们上面的if(argc=1)示例是这样的。一个更现实的例子可能是这样的

当编译器为此警告建议修复程序时,它必须做出选择:我们是应该建议将代码的行为更改为您可能想要键入的修复程序-“拼写检查”选项?还是应该演示如何在保持现有代码行为的同时使警告静默-如果您愿意,还可以使用“添加到字典”选项?(或者,继续我们的医学类比,有“治疗”选项和“DNR”选项。)。

仅显示“Treatment”选项会使编译器陷入“建议”更改代码行为的尴尬境地,毕竟,这是完全合法的C++代码;盲目地将修复程序应用于正常工作的代码可能会破坏该代码。但是,仅显示“抑制”选项可能会鼓励程序员盲目地将修复程序应用于损坏的代码,从而保留错误,但使其更难在未来检测到。

对于某些类型的警告,Clang会同时显示两种修复程序。在Clang 10are(Godbolt)中有两个这样的示例:

警告:使用赋值结果作为不带括号的条件[-Wparenters]if(x=foo())~~^~注意:如果(x=foo())^()注意:如果(x=foo())^==,则使用括号将此赋值转换为相等比较。注意:如果(x=foo())^==,则使用';==';将此赋值转换为相等比较。

警告:&;的优先级低于!=;!=将首先计算[-Wparenology]return(foo()&;ask!=0);^~注意:将圆括号放在';!=';表达式周围以使此警告返回静音(foo()&;ask!=0);^()注意:将括号放在&;表达式两边以首先计算它的返回(foo()&;掩码!

请注意,在这两种情况下,Clang决定先打印“抑制”选项,然后再打印“治疗”选项。但有时Clang会先打印“治疗”选项,然后再打印“抑制”选项:

警告:';strncmp';调用中的大小参数是比较[-Wmemsize-比较]返回strncmp(a,b,len<;0);~^~~注意:您是否想要比较';strncmp';的结果?Return strncmp(a,b,len<;0);^~)备注:将参数显式强制转换为size_t,以使此警告保持静音。return strncmp(a,b,len<;0);^(Size_T)()。

警告:Logical NOT仅应用于此比较的左侧[-Wlogic-NOT-PARROLATES]return x==y&;&;!x==z;^~~注意:在';!';之后添加括号以计算比较第一个返回x==y&;&;!x==z;^()注意:在左侧表达式两边添加括号以使此警告返回x==y&;&;!x==z;

当然,还有更多的情况下,Clang只发出“抑制”选项作为修复,让程序员自己解决“处理”问题。(GCC 10.2只为这四个示例中的最后一个发出了修复程序;它是“抑制”选项。)。

即使在没有任何机器可读的固定装置的情况下,警告信息本身的措辞也可以诱导人类读者从抑制或治疗的角度进行思考。警告消息可以表示为“请确认您的意思是X”;或者“您做了X;您的意思是Y吗?”;甚至可以说是“您尝试做Y的尝试失败了”。

我们在这里讨论的那种编译器警告基本上是这样的:“您写的是X,但我想您的意思是Y。”只有当X和Y在某种意义上接近时,才会发生这种情况。有时“接近”是语义上的,而不是语法上的(比如程序员想调用复制省略,但是编写代码返回std::move(X)),但是为了我们今天的目的,让我们只考虑语法上的接近。你想写Y,但是一个小小的印刷错误导致你写了X。在上面的例子中,这些错误是类似于“省略1=”、“省略一对括号”或“把<;0放在括号内而不是放在括号外”之类的东西。

以X=“相等-比较!a到b”和Y=“否定a==b的意义”为例。有一些代码明显想要表达X,比如(!a)==(B)。有些代码明显想要表达Y,比如a!=b。但是您写的(!a==b)属于灰色区域:不清楚您真正想要表达的是X和Y中的哪一个。

编译器诊断开发人员的工作是在编译器认为“明确的X”的输入空间和认为“明确的Y”的输入空间之间创建一些分隔。本质上,我们通过故意增加不等价的C++程序对之间的编辑距离来创建纠错代码-故意增加程序员必须搞砸的键数,以便将正在工作(并且没有警告)的C++程序转换成不等价的(但没有警告的)C++程序。

此外,在Y比X更常用的情况下,编写Y应该比编写X相对容易。在我们的示例中,即使在核心语言级别也是如此:a!=b已经比!a==b更容易编写。但是,当我们增加X空间和Y空间之间的距离时,我们收缩X空间的幅度要大于收缩Y空间的程度。如果您真的想要表达!a==b,我们将强制您一直退回到(!a)==b。这有点类似于经济学或进化生物学中的信号传递原则:基本上,如果您希望编译器接受您的意图,那么您的代码必须采用一些繁琐且明显不适应的修饰,以证明其对编译器的价值。

根据动物界流行的性选择理论,当一只孔雀决定是否接受某一只孔雀时,她会用它的装饰尾巴来代表它的成功。一只尾巴又大又笨重的孔雀必须是健康和成功的,因此是孔雀的好配偶。一只尾巴不起眼的孔雀不值得与之交配。

当C++编译器决定是否无怨无悔地接受特定程序时,它会使用某些装饰性的语法花饰作为代码意图的代理。采用这些花饰的可疑代码段-比如说,带有额外括号的if((argc=1)),或(Void)x;,或strncmp(a,b,size_t(len<;0))-一定是故意的,因此对编译器来说是一个很好的匹配。缺少任何花边的有问题的代码段-例如,if(argc=1)或x;或strncmp(a,b,len<;0)-不值得取消警告。

这种沉思的好处是:在决定是否对粗略的构造发出警告时,先对绝对规则进行编码,比如“如果它被一对括号括起来,就不要发出警告(argc=1)。”但是,我们应该从相对规则的角度来思考--这些规则将程序员实际编写的代码的“装饰程度”与没有(假设)打字错误的代码的“装饰程度”进行比较。请注意:

截至2020年9月,朗和GCC都没有发现这种单字打字错误。他们看到子表达式(argc=2)用圆括号括起来,这足以抑制他们的警告。

我认为他们应该做的是将编写的程序与“已更正打字”的程序进行比较。

这显然是一个似是而非、平淡无奇的程序,所以程序员很有可能确实打错了=for==。如果程序员真的想在这种情况下使警告静音,我认为应该强迫程序员编写。

这是值得注意的,也是不可信的-它显然有过多的括号装饰!

换一种说法:编译器应该将可接受程序的空间分成“那些使用=且括号数量非常多的程序”和“那些使用==但括号数量不多的程序”。然后,出于警告诊断的目的,编译器应该实质上将=和==视为同义词;它可以依靠括号的数量来指示程序员的意图。

X==2;//哎呀,可能表示x=2bool help 1=argc=1;//oops,可能表示argc==1,因为这是合理的bool help 2=(argc=1);//oops,可能表示(argc==1)bool help 3=((argc=1));//OK:显然不是指((argc==1))int i1=(argc=1)?1:2;//OK:显然不是这个意思((argc==1))。

截至2020年9月,浪10.1和GCC 10.2在这些测试用例上表现不是很好。

这篇帖子的灵感来自于邮件列表帖子“[CFE-dev]括号旗帜警告”(2020年5月)。