具代表性的面試主題

C++26 Contracts:如何安全落實前置條件、後置條件與 contract_assert?

程式題困難
Offer.cc 編輯團隊發佈 更新

題幹

如何解釋 C++26 的 pre、post 與 contract_assert,並在編譯器支援不完整時安全遷移既有 assert?

題幹與適用場景

你負責一個訂單函式庫,準備採用 C++26 Contracts 表達 API 的前置條件、後置條件與函式內不變量。請說明 prepostcontract_assert 的語意邊界、評估模式、違約處理,以及如何從既有 assert 遷移而不遺漏不可信輸入檢查。回答也要涵蓋編譯器尚未完整支援時的發布策略。

面試官考察點

  • 能否區分契約文件、執行期診斷與業務輸入檢查。
  • 能否解釋述詞可能被省略或重複評估,因此避免副作用。
  • 能否設計 violation handler、日誌與故障隔離,而非假設失敗必然拋出例外。
  • 能否用編譯器能力矩陣與灰度開關處理 C++26 支援差異。

澄清問題

  1. API 呼叫者是受信任的函式庫內部程式,還是直接接收網路請求?
  2. 生產環境要觀察違約、終止程序,還是繼續服務並隔離請求?
  3. 目標編譯器、標準函式庫版本與 ABI 發布週期各是什麼?

30 秒回答框架

先按責任邊界回答:pre 描述呼叫者必須滿足的條件,post 描述函式返回時的承諾,contract_assert 描述函式內的局部契約。接著說明述詞必須無副作用,因為實作可選擇省略或重複評估;違約交給統一 handler 與部署策略。最後補充不可信輸入仍要明確檢查,並用特性偵測、編譯器矩陣與灰度發布控制遷移風險。

分步驟深入解答

1. 建立契約層次

前置條件是呼叫者責任,後置條件是被呼叫函式責任。它們適合表達 API 的可組合約束,例如容量必須為正、返回值符合排序關係。函式內的 contract_assert 用於局部不變量或演算法階段假設。三者都應讓程式碼審查者容易理解,並與錯誤處理策略分開。

2. 撰寫無副作用述詞

述詞只讀取狀態並計算布林結果。不要在述詞中遞增計數器、修改快取、釋放資源或依賴一次性隨機值。C++26 契約語意允許實作選擇不同評估模式,某些模式可省略評估,另一些模式可能重複評估;副作用會讓正確程式的行為依賴建置選項。

cpp
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;傳統 assertNDEBUG 與巨集副作用逐項清理,不能機械替換。這樣既能得到穩定的內部不變量檢查,也不會把線上恢復策略寄託在尚未統一的實作行為上。

常見錯誤

  • pre 當成所有輸入的安全檢查,忽略邊界責任。
  • 在述詞中寫日誌、遞增指標或修改物件,讓重複評估產生不同結果。
  • 斷言違約一定拋例外或一定終止程序,忽略實作和建置模式。
  • 直接把 assert 搜尋替換成 contract_assert,遺漏 NDEBUG、巨集參數和副作用。
  • 使用不存在的通用 old(...) 語法描述 C++26 後置條件。

追問及應對

如果面試官問「為什麼不全用例外?」

契約先表達呼叫者與被呼叫者的責任,能參與靜態審查和建置期策略;例外則是控制流和恢復協定。兩者可以協作,不能互相取代。

如果問「述詞重複執行會不會很慢?」

因此要先限制述詞為純讀、低成本表達式,並按建置模式量測開銷。昂貴檢查可放到明確診斷路徑,不要把副作用藏進契約。

如果問「虛擬函式能否直接寫契約?」

C++26 目前契約斷言對虛擬函式仍有邊界,WG21 後續路線圖把虛擬函式支援列為擴充方向。設計多型 API 時應查目標編譯器與標準版本,避免把後續提案當成 C++26 已有能力。

公開來源

同類題目

相關面試工具

用 Screenshot 處理演算法題

截圖題目後,依序看約束、解法、程式碼、邊界條件和複雜度。

查看工具