C++26 Contracts:如何安全落實前置條件、後置條件與 contract_assert?
題幹與適用場景
你負責一個訂單函式庫,準備採用 C++26 Contracts 表達 API 的前置條件、後置條件與函式內不變量。請說明 pre、post、contract_assert 的語意邊界、評估模式、違約處理,以及如何從既有 assert 遷移而不遺漏不可信輸入檢查。回答也要涵蓋編譯器尚未完整支援時的發布策略。
面試官考察點
- 能否區分契約文件、執行期診斷與業務輸入檢查。
- 能否解釋述詞可能被省略或重複評估,因此避免副作用。
- 能否設計 violation handler、日誌與故障隔離,而非假設失敗必然拋出例外。
- 能否用編譯器能力矩陣與灰度開關處理 C++26 支援差異。
澄清問題
- API 呼叫者是受信任的函式庫內部程式,還是直接接收網路請求?
- 生產環境要觀察違約、終止程序,還是繼續服務並隔離請求?
- 目標編譯器、標準函式庫版本與 ABI 發布週期各是什麼?
30 秒回答框架
先按責任邊界回答:pre 描述呼叫者必須滿足的條件,post 描述函式返回時的承諾,contract_assert 描述函式內的局部契約。接著說明述詞必須無副作用,因為實作可選擇省略或重複評估;違約交給統一 handler 與部署策略。最後補充不可信輸入仍要明確檢查,並用特性偵測、編譯器矩陣與灰度發布控制遷移風險。
分步驟深入解答
1. 建立契約層次
前置條件是呼叫者責任,後置條件是被呼叫函式責任。它們適合表達 API 的可組合約束,例如容量必須為正、返回值符合排序關係。函式內的 contract_assert 用於局部不變量或演算法階段假設。三者都應讓程式碼審查者容易理解,並與錯誤處理策略分開。
2. 撰寫無副作用述詞
述詞只讀取狀態並計算布林結果。不要在述詞中遞增計數器、修改快取、釋放資源或依賴一次性隨機值。C++26 契約語意允許實作選擇不同評估模式,某些模式可省略評估,另一些模式可能重複評估;副作用會讓正確程式的行為依賴建置選項。
int withdraw(Account& a, int amount)
pre (amount > 0)
pre (amount <= a.balance())
post (a.balance() == old_balance - amount);
{
contract_assert(a.is_open());
return a.debit(amount);
}範例中的 old_balance 只是說明設計問題;C++26 目前沒有通用的 postcondition capture,不能假定存在 old(...) 語法。需要舊值時應在函式內明確保存,並確認保存動作不改變業務語意,或等待後續標準擴充。
3. 選擇評估與違約處理
工程上至少定義 observe、enforce 等模式對應的行為:開發與測試收集完整診斷,關鍵服務在 enforce 模式下快速失敗,線上請求邊界則由上層把錯誤轉成可觀測的失敗結果。不要把「違約一定拋例外」寫進介面承諾;具體行為取決於實作與建置設定。handler 應記錄契約位置、請求關聯 ID 與版本,並避免遞迴呼叫同一契約路徑。
4. 把輸入檢查留在邊界
網路欄位、使用者金額與權限都屬於不可信輸入。先用明確檢查回傳可預期的業務錯誤,再呼叫帶契約的內部函式。契約可捕捉內部呼叫者的程式錯誤,卻不能取代驗證、授權、限流或格式檢查,也不能作為資料清洗的唯一防線。
5. 規劃遷移與發布
以特性巨集和編譯器版本建立能力矩陣,分別驗證語法、handler、除錯資訊和最佳化建置。先在單元測試和 canary 啟用觀察模式,比較違約率與效能,再逐步提高 enforcement。傳統 assert 的巨集展開、NDEBUG 和副作用假設不能直接等價遷移;需要逐點審計並保留既有失敗語意,必要時透過適配層統一上報。
高品質示範回答
我會把 Contracts 視為可執行的設計約束,而不是輸入檢查或例外系統。pre 約束呼叫者,post 約束被呼叫函式,contract_assert 約束函式內的階段性不變量。所有述詞保持純讀,因為實作可以省略或重複評估;我會禁止 I/O、計數器修改與資源釋放。違約透過統一 handler 產生結構化診斷,再由建置設定決定觀察、強制或快速失敗策略,程式不假定一定拋例外。請求入口仍先做驗證、授權與不可信資料檢查。遷移時建立編譯器能力矩陣,先測試和灰度,再逐步開啟 enforcement;傳統 assert 的 NDEBUG 與巨集副作用逐項清理,不能機械替換。這樣既能得到穩定的內部不變量檢查,也不會把線上恢復策略寄託在尚未統一的實作行為上。
常見錯誤
- 把
pre當成所有輸入的安全檢查,忽略邊界責任。 - 在述詞中寫日誌、遞增指標或修改物件,讓重複評估產生不同結果。
- 斷言違約一定拋例外或一定終止程序,忽略實作和建置模式。
- 直接把
assert搜尋替換成contract_assert,遺漏NDEBUG、巨集參數和副作用。 - 使用不存在的通用
old(...)語法描述 C++26 後置條件。
追問及應對
如果面試官問「為什麼不全用例外?」
契約先表達呼叫者與被呼叫者的責任,能參與靜態審查和建置期策略;例外則是控制流和恢復協定。兩者可以協作,不能互相取代。
如果問「述詞重複執行會不會很慢?」
因此要先限制述詞為純讀、低成本表達式,並按建置模式量測開銷。昂貴檢查可放到明確診斷路徑,不要把副作用藏進契約。
如果問「虛擬函式能否直接寫契約?」
C++26 目前契約斷言對虛擬函式仍有邊界,WG21 後續路線圖把虛擬函式支援列為擴充方向。設計多型 API 時應查目標編譯器與標準版本,避免把後續提案當成 C++26 已有能力。