4.2.3 条件表达式
条件表达式也称为三目运算,根据比较表达式1得到的结果进行选择。如果是真值,选择执行表达式2;如果是假值,选择执行表达式3。语句的构成如下:
表达式1?表达式2:表达式3
条件表达式也属于表达式的一种,所以表达式1、表达式2、表达式3都可以套用到条件表达式中。条件表达式被套用后,其执行顺序依然是由左向右,自内向外。
条件表达式的构成应该是先判断再选择。但是,编译器并不一定会按照这种方式进行编译,当表达式2与表达3都为常量时,条件表达式可以被优化;而当表达式2或表达式3中的一个为变量时,条件表达式不可以被优化,会转换成分支结构。当表达式1为一个常量值时,编译器会在编译期间得到答案,将不会有条件表达式存在。下面来讨论一下编译器是如何优化,如何避免使用分支结构的。
条件表达式有如下4种转换方案:
方案1:表达式1为简单比较,而表达式2和表达3两者的差值等于1;
方案2:表达式1为简单比较,而表达式2和表达3两者的差值大于1;
方案3:表达式1为复杂比较,而表达式2和表达3两者的差值大于1;
方案4:表达式2和表达式3有一个为变量,于是无优化。
通过反汇编形式对比这4种转换方案,找出它们的特性,分析它们之间的区别。方案1见代码清单4-14。
代码清单4-14 条件表达式—转换方案1
//C++源码说明:条件表达式
int Condition(int argc, int n){
//比较参数argc是否等于5,真值返回5,假值返回6
return argc==5?5:6;
}
//C++源码与对应汇编代码讲解
//略去无关代码,argc为函数参数
//C++源码对比,比较变量argc,选择返回数据
return argc==5?5:6;
;清空eax
00401678 xor eax, eax
0040167A cmp dword ptr[ebp+8],5
;setne检查ZF标记位,当ZF==1,则赋值al为0,反之则赋值al为1
0040167E setne al
;若argc等于5则al==0,反之al==1,执行这句后,eax正好为5或6
00401681 add eax,5
;略去无关代码
代码清单4-14利用表达式2和表达式3之间的差值1,使用setne指令进行平衡。这种情况是三目运算中最为简单的转换方式。当表达式2和表达式3之间的差值大于1后,setne指令就无法满足要求了,如代码清单4-15所示。
代码清单4-15 条件表达式—转换方案2
//C++源码说明:条件表达式
int Condition(int argc, int n){
//比较参数argc是否等于5,真值返回4,假值返回10
return argc==5?4:10;
}
//C++源码与对应汇编代码讲解
int Condition(int argc, int n){
//略去无关代码,argc为函数参数
//C++源码对比,比较变量argc,选择返回数据
return argc==5?4:10;
00401678 mov eax, dword ptr[ebp+8]
0040167B sub eax,5
0040167E neg eax
00401680 sbb eax, eax
;在这个时候,eax的取值只可能为0或者0xffffffff
00401682 and eax,6
00401685 add eax,4
}
在代码清单4-15中,对于argc==5这种等值比较,VC++会使用减法和求补运算来判断其是否为真值,只要argc不为5,在执行sub指令后eax的值就不为0;接下来执行neg指令,eax的符号位就会发生改变,CF置1。接下来执行借位减法指令sbb eax, eax,等价于eax=eax-eax-CF。当CF位为1时,eax中的值将会为0xFFFFFFFF,否则为0。使用eax与6做位与运算后,如果eax中的数值原来为-1,则结果为6,加4后得到数值10,正好为条件表达式中为假值的选择结果。
当条件表达式中argc==5为真值时,那么eax中始终为0。用0值与6做位与运算结果还是0,加4后还是4。
总结:
;遇到sub/neg/sbb就表明是等值比较了,其判定值为A
sub reg, A
neg reg
sbb reg, reg
and reg, B
add reg, C;若等值条件成立,其结果为C,否则为B+C
这样的代码块,可以直接还原为如下形式的高级代码:
reg==A?C:B+C
如果表达式2大于表达式3,那么最后加的数字为一个负数。这是由表达式3减去表达式2得到的数值。
当表达式1为一个区间比较时,会使用另一种转换方案,这种方案是前两种方案的结合,如代码清单4-16所示。
代码清单4-16 条件表达式—转换方案3
//C++源码说明:条件表达式
int Condition(int argc, int n){
return argc<=8?4:10;
}
//C++源码与对应汇编代码讲解
int Condition(int argc, int n){
//略去无关代码,argc为函数参数
//C++源码对比,比较变量argc,选择返回数据
return argc<=8?4:10;
;清空eax,与方案1类似
00401678 xor eax, eax
0040167A cmp dword ptr[ebp+8],8
;根据变量与8进行比较的结果,使用setg指令,当标记位SF=OF且ZF=0赋值al为1
;用于检查变量数据是否大于8,大于则赋值1
0040167E setg al
;此时al中只能为0或1,执行自减操作,eax中为0xFFFFFFFF或0
00401681 dec eax
;使用al与0xFA做位与运算。eax中为0xFFFFFFFA或0
;此数值为表达式2减去表达式3得到的数值
00401682 and al,0FAh
;由于eax只能有两个结果0xFFFFFFFA(-6)或0,加0x0A后结果必然为4或10
00401684 add eax,0Ah
}
代码清单4-16使用到了方案1的解决办法,通过比较表达式1,根据结果使用指令setg将al置1或0,dec将eax转换成0或-1用于和0FAh做位与运算,加上0Ah进行结果调整,得到最终结果。方案3中的调整数和方案2在逻辑上是相反的,它是由表达式2减去表达式3得到的数值。
总结:
先调整reg为0或者-1
and reg, A
add reg, B
遇到这样的代码块,需要重点考察and前的指令,以辨别真假逻辑的处理方式。对于上例中dec reg这样的指令,之前reg只能是0或者是1,因此这里的dec其实是对reg进行修正,如果原来reg为1,dec后修正为0,否则为0xffffffff,便于其后的and运算。这时候要根据and前的指令流程分析原来的判定在什么情况下会导致reg为0xffffffff或者0,以便于还原。编译器这样做是为了避免产生分支语句。而对于顺序结构,处理器会预读下一条指令,以提高运行效率。
当表达式2或表达式3中的值为未知数时,就无法使用之前的方案去优化了。编译器会按照常规根据语句流程进行比较和判断,选择对应的表达式,如代码清单4-17所示。
代码清单4-17 条件表达式—无优化使用分支结果
//C++源码说明:条件表达式
int Condition(int argc, int n){
//比较参数argc,真值返回8,假值返回n
return argc?8:n;
}
//C++源码与对应汇编代码讲解
//略去无关代码,argc为函数参数1,n为函数参数2
//C++源码对比,比较变量argc,选择返回数据
return argc?8:n;
;比较变量argc
00401448 cmp dword ptr[ebp+8],0
;使用JE跳转,检查变量argc是否等于0,跳转的地址为0x00401457处
0040144C je Condition+27h(00401457)
;跳转失败说明操作数1为真,将表达式1的值(立即数8)存入临时局部变量ebp-4中
0040144E mov dword ptr[ebp-4],8
;跳转到返回值赋值处
00401455 jmp Condition+2Dh(0040145d)
;参数2的数据存入eax中
00401457 mov eax, dword ptr[ebp+0Ch]
;由于没有优化,所以显的很烦琐
0040145A mov dword ptr[ebp-4],eax
0040145D mov eax, dword ptr[ebp-4]
}
;略去无关代码
00401466 ret
分析代码清单4-17中的汇编代码发现,条件表达式最后转换的汇编代码与分支结构的表现形式非常相似,它的判断比较与代码清单4-13中的表达式短路很类似。实际上,在经过O2选项优化后的Release版中,这些代码都会被编译为分支结构,详细讲解见5.3节。