Re: [問題] A[x++] = --x
※ 引述《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://www.ptt.cc/bbs/C_and_CPP/M.1631280927.A.1ED.html
推
09/11 00:15,
3年前
, 1F
09/11 00:15, 1F
※ 編輯: loveme00835 (118.233.156.253 臺灣), 09/11/2021 18:33:17
討論串 (同標題文章)
C_and_CPP 近期熱門文章
PTT數位生活區 即時熱門文章