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节。