作者loveme00835 (发箍)
看板C_and_CPP
标题Re: [问题] A[x++] = --x
时间Fri Sep 10 21:35:24 2021
※ 引述《CaliforCat (Cal)》之铭言:
: int main()
: {
: int A[3] = {0, 0, 0};
: int x = 1;
: A[x++] = --x;
: printf("A[0]=%d, A[1]=%d, A[2]=%d", A[0], A[1], A[2]);
: }
刚好藉这个机会分享遇到
UB (undefined behavior) 的原因, 以及
该如何减少 UB 带来的冲击. 後面会提到
程式理解 (program unde-
rstanding) 的观念, 有兴趣可以找相关文献阅读.
C/C++程式是跑在
抽象机器 (abstract machine) 上的
(一种只存在
想像中的机器),
语言设计上只规范了抽象机器的行为, 并要求编译
器输出在实体机器上的执行结果要和抽象机器一致.
Abstract Machine |
Physical Machine
C Program Program
┌────────┐ ┌────────┐
| statement #1 | │ statement #1 │
│ ↓ ←──┼─(✓ )─┼─── ↓ │
│ statement #2 │ │ statement #2 │
│ ↓ │ │ ↓ │
│ statement #3 │ │ statement #3 │
│ ↓ ←──┼─(✓ )─┼─── ↓ │
│ statement #4 │ │ statement #4 │
└────────┘ └────────┘
一个合格的编译器, 其产生的程式的 side effect, 都会和抽象机
器相同; 但也有些地方为了兼容计算能力较差的机器, 或是保留优
化的弹性, 语言并不会明确定义这时候抽象机器的行为, 所以才有
了 UB. 初学者可能因为不熟悉抽象机器的性质, 写出依赖特定实体
机器结果, 但行为却是 UB 的程式码.
遇到 UB 不是罪过, 但要意
识到切换编译器或实体机器可能带来的行为转变.
虽然抽象机器这个概念太虚无飘渺, 但不管是开发人员还是维护人
员, 他们日常生活中都有个逼近抽象机器, 建立
认知模型 (cognit-
ive model), 并拿它来预测程式行为的过程.
维护人员的认知模型
与开发人员的相同, 除错效率就高; 若两者都有和抽象机器对齐,
, 将程式移植到不同平台时遇到的意外就少.
Cognitive Model |
Physical Machine
C Program Program
┌────────┐ ┌────────┐
| statement #1 | │ statement #1 │
│ ↓ │ │ ↓ │
│ statement #2 │ │ statement #2 │
│ ↓ │ │ ↓ │
│ statement #3 │ │ statement #3 │
│ ↓ │ │ ↓ │
│ statement #4 │ │ statement #4 │
└────────┘ └────────┘
不过在大部分的情况下,
开发人员撰码时都不会将这个认知模型给
储存起来, 维护人员常需要透过逐步执行等方式将模型重建回来.
结果就是我们不仅不清楚程式实际的行为是什麽, 连预期该有的行
为也不晓得
(连结断掉了).
一旦有了认知模型, 拿它和编译出来的结果比对, 我们就容易检验
前者有没有和抽象机器对齐, 判断是否写出可携的程式码. 就我所
知描述认知模型最经济的方式是使用
contract programming, 我接
着会重建你程式码里的认知模型, 顺便演示作静态分析的思维.
int A[
3] = {
0,
0,
0};
size_t x =
1;
在阵列 A 定义的同时也描述了任何透过 A 名称的索引值必须在
[0, 2] 区间内, 而且索引值如果用变数储存, 它的型别应该是
size_t,
为了减少错误发生我们不仅应该使用合理的值, 也应该使
用合理的型别 (以语言习惯优先)
接着我们不仅要检查 x 的值作为索引合不合法, 也要检查它作为
size_t 有没有因为运算发生 wrap-around:
assert(x < SIZE_MAX);
const size_t index = x++;
assert(index < 3);
然後在将 x 转成
int 指派给阵列 A 元素前也要验证合法性:
assert((x - 1) <= INT_MAX);
const int value = (
int) (--x);
assert(0 <= value);
最後把程式码重新整理一下:
int A[
3] = {
0,
0,
0};
size_t x =
1;
// (1)
assert(x < SIZE_MAX);
const size_t index = x++;
assert(index < 3);
// (2)
assert((x - 1) <= INT_MAX);
const int value = (
int) (--x);
assert(0 <= value);
// (3)
A[index] = value;
assert(A[1] == 1); // our expectation
把 (1) 和 (2) 分开写可以看到 index 和 value 变数求值的顺序
会影响结果, 导致最後的 assert() 行为改变. 那我们需不需要把
程式码改成像上面一样? 或至少把 assert() 移除? 答案是不需要
(除非你有洁癖), 但至少要保留最後的 assert():
int A[
3] = {
0,
0,
0};
int x =
1;
A[x++] = --x;
assert(A[1] == 1); // our expectation
然後当你下次执行时触发 assertion failure, 就是踩到所谓的 UB,
或 implementation-defined behavior 等预期以外的行为, 到时候
再来修改程式码即可.
UB 非常多, 实务上无法靠死背来避开它们, 但你至少得知道自己的
假设还有对程式行为的预期. 藉由编译器的帮忙, 就可以知道想法
离抽象机器有多远, 再来慢慢修正认知模型.
--
[P1389R1] Standing Document for SG20: Guidelines for Teaching
C++ to Beginners
https://wg21.link/p1389r1
SG20 Education and Recommended Videos for Teaching C++
https://www.cjdb.com.au/sg20-and-videos
--
※ 发信站: 批踢踢实业坊(ptt.cc), 来自: 118.233.156.253 (台湾)
※ 文章网址: https://webptt.com/cn.aspx?n=bbs/C_and_CPP/M.1631280927.A.1ED.html
1F:推 Lipraxde: 这样写 code 太严格了 Orz 09/11 00:15
※ 编辑: loveme00835 (118.233.156.253 台湾), 09/11/2021 18:33:17
2F:推 howareuuu: 推 09/20 18:52