赋值语句返回的原罪

今天翻看王垠的Blog,有一篇引起了我的注意 Yoda 表示法错在哪里

其实这是一个古老的问题,用王垠的话讲这是一个典型的“先辈的罪”(Sins of our Forefathers)。我更愿意叫它“历史遗留问题”。

这个问题世界上任何一个写C++或者C的人都应该遇到过,当我需要使用“==”进行判断的时候少写了一个“=”,然后开始了漫长的、痛苦的Debug之路。老实说,我曾经有过至少5次这样的错误,最深刻的一回我可能排查了2个小时才搞定。

1
2
3
if (a = 1) { // WTF
...
}

后来,痛定思痛,我成为了Yoda写法的忠实践行者

1
2
3
if (1 == a) { // looks like good
...
}

而当我工作之后,甚至很多情况下会故意写出这样的代码

1
2
3
if (DeclStmt *DS = dyn_cast_or_not<DeclStmt>(S)) {
...
}

不得不说这种写法非常诱惑,一行代码解决了两行代码的问题

1
2
3
if (isa<DeclStmt>(S)) {
DeclStmt *DS = dyn_cast<DeclStmt>(S);
}

但是同样不得不说的是,这样的写法确实从逻辑上讲是有问题的。这个问题的根源在于——为什么一条复制语句会拥有返回值?

对应的反例,可以看看Rust中的赋值语句

1
let a = 1;

程序的源代码是对内存状态的改变,赋值行为是一个具有特殊意义的行为,直接决定了程序的正确性。进行赋值操作的源代码,它的任何除赋值以外的效果都应该作为副作用被规避,以此来保证程序的正确性。

Rust中的赋值必然是一条语句,无法作为一个表达式,如此保证赋值行为的正确执行。但是很明显,C++中的赋值行为可以被定义为一个返回值有效的表达式。

可以从C++的AST中更加明显的看到这一点

1
2
3
4
5
6
7
8
int a = 0;
int b = 0;
if ( a = 1 ) {
b = 1;
}
if ( a == 0 ) {
b = 2;
}

对于这样一段代码,clang生成的AST是这样的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
|-IfStmt 0x9c0ba98 <line:5:5, line:7:5>                                       
| |-<<<NULL>>>
| |-<<<NULL>>>
| |-ImplicitCastExpr 0x9c0ba20 <line:5:10, col:14> '_Bool' <IntegralToBoolean>
| | `-ImplicitCastExpr 0x9c0ba10 <col:10, col:14> 'int' <LValueToRValue>
| | `-BinaryOperator 0x9c0b9f8 <col:10, col:14> 'int' lvalue '='
| | |-DeclRefExpr 0x9c0b9c0 <col:10> 'int' lvalue Var 0x9c0b8e0 'a' 'int'
| | `-IntegerLiteral 0x9c0b9d8 <col:14> 'int' 1
| |-CompoundStmt 0x9c0ba80 <col:18, line:7:5>
| | `-BinaryOperator 0x9c0ba68 <line:6:9, col:13> 'int' lvalue '='
| | |-DeclRefExpr 0x9c0ba30 <col:9> 'int' lvalue Var 0x9c0b958 'b' 'int'
| | `-IntegerLiteral 0x9c0ba48 <col:13> 'int' 1
| `-<<<NULL>>>
|-IfStmt 0x9c0bb80 <line:8:5, line:10:5>
| |-<<<NULL>>>
| |-<<<NULL>>>
| |-BinaryOperator 0x9c0bb00 <line:8:10, col:15> '_Bool' '=='
| | |-ImplicitCastExpr 0x9c0baf0 <col:10> 'int' <LValueToRValue>
| | | `-DeclRefExpr 0x9c0bab8 <col:10> 'int' lvalue Var 0x9c0b8e0 'a' 'int'
| | `-IntegerLiteral 0x9c0bad0 <col:15> 'int' 0
| |-CompoundStmt 0x9c0bb68 <col:19, line:10:5>
| | `-BinaryOperator 0x9c0bb50 <line:9:9, col:13> 'int' lvalue '='
| | |-DeclRefExpr 0x9c0bb18 <col:9> 'int' lvalue Var 0x9c0b958 'b' 'int'
| | `-IntegerLiteral 0x9c0bb30 <col:13> 'int' 2
| `-<<<NULL>>>

对比一下区别,“=”和“==”都是最为一个BinaryOperator的OpCode存在的。也就是说C++认为赋值行为和加减乘除相等这些都是一样的作为二元运算符存在的。事实上不单单是“==”,从OperationKinds.def中可以看到

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
// [C++ 5.5] Pointer-to-member operators.
BINARY_OPERATION(PtrMemD, ".*")
BINARY_OPERATION(PtrMemI, "->*")
// [C99 6.5.5] Multiplicative operators.
BINARY_OPERATION(Mul, "*")
BINARY_OPERATION(Div, "/")
BINARY_OPERATION(Rem, "%")
// [C99 6.5.6] Additive operators.
BINARY_OPERATION(Add, "+")
BINARY_OPERATION(Sub, "-")
// [C99 6.5.7] Bitwise shift operators.
BINARY_OPERATION(Shl, "<<")
BINARY_OPERATION(Shr, ">>")
// [C99 6.5.8] Relational operators.
BINARY_OPERATION(LT, "<")
BINARY_OPERATION(GT, ">")
BINARY_OPERATION(LE, "<=")
BINARY_OPERATION(GE, ">=")
// [C99 6.5.9] Equality operators.
BINARY_OPERATION(EQ, "==")
BINARY_OPERATION(NE, "!=")
// [C99 6.5.10] Bitwise AND operator.
BINARY_OPERATION(And, "&")
// [C99 6.5.11] Bitwise XOR operator.
BINARY_OPERATION(Xor, "^")
// [C99 6.5.12] Bitwise OR operator.
BINARY_OPERATION(Or, "|")
// [C99 6.5.13] Logical AND operator.
BINARY_OPERATION(LAnd, "&&")
// [C99 6.5.14] Logical OR operator.
BINARY_OPERATION(LOr, "||")
// [C99 6.5.16] Assignment operators.
BINARY_OPERATION(Assign, "=")
BINARY_OPERATION(MulAssign, "*=")
BINARY_OPERATION(DivAssign, "/=")
BINARY_OPERATION(RemAssign, "%=")
BINARY_OPERATION(AddAssign, "+=")
BINARY_OPERATION(SubAssign, "-=")
BINARY_OPERATION(ShlAssign, "<<=")
BINARY_OPERATION(ShrAssign, ">>=")
BINARY_OPERATION(AndAssign, "&=")
BINARY_OPERATION(XorAssign, "^=")
BINARY_OPERATION(OrAssign, "|=")
// [C99 6.5.17] Comma operator.
BINARY_OPERATION(Comma, ",")

“*=”这些类似的符号也是类似的,考虑到“-”、“+”和“=”的位置关系,历史上应该也是有人在需要“==”时写出了这样的代码

1
2
3
4
5
6
if (a -= 1) {
...
}
if (a += 1) {
...
}

23333….

错误地没有将赋值行为与其他二元操作符区分开来是C++此类错误的元凶,不过我估计C++有可能是兼容了C(代码直接拿过来用了),至于C为什么会这样做,可能是因为那时的程序员水平都比较高,保证能写出正确的代码吧(很符合C++的设计哲学嘛)。