Re: [問題] A[x++] = --x

看板C_and_CPP (C/C++)作者 (髮箍)時間3年前 (2021/09/10 21:35), 3年前編輯推噓1(100)
留言1則, 1人參與, 3年前最新討論串2/3 (看更多)
※ 引述《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
這樣寫 code 太嚴格了 Orz
09/11 00:15, 1F
※ 編輯: loveme00835 (118.233.156.253 臺灣), 09/11/2021 18:33:17
文章代碼(AID): #1XEryV7j (C_and_CPP)
討論串 (同標題文章)
本文引述了以下文章的的內容:
29
179
完整討論串 (本文為第 2 之 3 篇):
29
179
文章代碼(AID): #1XEryV7j (C_and_CPP)