在表达式中不应多次读写同一对象
10.2.3 ID_confusingAssignment
如果在表达式中多次读写同一对象,很可能会因为非预期的求值顺序而产生错误的结果。
关于表达式的求值顺序,以及副作用何时生效等问题,相关标准既复杂又存在大量未声明的情况。为了避免意料之外的错误,要求表达式中的任一对象:
- 被写入的次数不应超过 1 次
- 不应既被读取又被写入,除非是为了计算对象的新状态而写入对象
- 如果对象由 volatile 限定,被读取的次数不应超过 1 次
对于逻辑与、逻辑或、三元以及逗号表达式,标准明确规定了子表达式从左至右求值,左子表达式的副作用也会在右子表达式求值前生效,所以相关子表达式之间可不受本规则限制,但子表达式本身仍受本规则限制,进一步可参见“序列点(sequence point)”以及“求值顺序”等概念。
本规则是 ID_evaluationOrderReliance 的特化。
示例:
a = a++; // Non-compliant
a = ++a; // Non-compliant
++b = b; // Non-compliant
b = a++ + a; // Non-compliant
arr[i] = ++i; // Non-compliant
p->fun(p++); // Non-compliant
fun(a, a++); // Non-compliant
fun(++a, a++); // Non-compliant
例中表达式均不符合要求。
设 a 是值为 0 的整型变量,如下表达式:
a = a++; // Non-compliant
对变量 a 有两次写入,分别是增 1 和赋值为 0(子表达式 a++ 的值为 0),这两次写入的次序在 C 和 C++17 之前的标准中是未声明的,如果先增 1 再赋 0,a 的值最终为 0,如果先赋 0 再增 1,a 的值最终为 1,这种不确定的结果应当避免,C++17 规定了右子表达式的副作用先于赋值生效,所以在 C++17 之后该表达式是无效的。
虽然新标准强化了求值顺序,但这种代码使人费解,很容易造成理解上的偏差,故不应使用,应改为:
a++; // Compliant, or
a += 1; // Compliant, or
a = a + 1; // Compliant
又如:
volatile int* v = DEV;
fun(*v, *v); // Non-compliant
例中 volatile 对象 *v 在一个表达式中被读取两次是不符合要求的。volatile 对象的值可在程序之外被改变,对 volatile 对象的读取相当于更新对象的值,也是一种副作用。
应在简单的赋值语句中访问 volatile 对象:
volatile int* v = DEV;
int tmp = *v;
fun(tmp, tmp); // Compliant
相关
依据
ISO/IEC 9899:1999 6.5(2)-undefined
ISO/IEC 9899:1999 Annex C
ISO/IEC 9899:2011 6.5(2)-undefined
ISO/IEC 9899:2011 Annex C
ISO/IEC 14882:2003 5(4)-unspecified
ISO/IEC 14882:2011 1.9(15)-undefined
ISO/IEC 14882:2011 5.17(1)
ISO/IEC 14882:2017 8.18(1)
参考
C++ Core Guidelines ES.43
MISRA C 2012 13.2
MISRA C++ 2008 5-0-1
SEI CERT EXP50-CPP