摘要訊息 : 《A Contract Design》這篇提案提出了一個新的特性 Contract, 本篇文章詳細講述了 Contract 的用法.

0. 前言

本文基於 P0380R1《A Contract Design》撰寫. C++ 20 已經移除 Contract 特性, 它被待定在之後的 C++ 標準中加入. 本文存在極大的過時風險!

本文於 2022 年 4 月 8 日進行一次更新和修正. 修正之後本文已經歸檔, 不再享受更新.

1. 基本概念

在本節中, 我們將對 Contract 的基本概念進行介紹, 大家通過本節可以大致了解 Contract 並且學會一些初級用法.

1.1 關鍵字

Contract 引入了不少新的關鍵字 : expects, ensures, assert, axiomaudit. 另外, 還用到了一個已經有的關鍵字 default.

除了 default 之外, 剩餘的關鍵字是特殊標識符號, 類似於 C++ 11 引入的 finaloverride, C++ 20 引入的 importmodule. 這些關鍵字在 Contract 語境中屬於關鍵字, 而在其它語境中可能是一個變數的名稱.

1.2 定義

Contract 引入了合約表達式 : [[CONTRACT-KEYWORD CONTRACT-LEVEL IDENTIFIER : EXPRESSION]]. 其中, CONTRACT-KEYWORD 是合約關鍵字, 可以從 expects, ensuresassert 中選擇; CONTRACT-LEVEL 是合約表達式的等級, 可以從 axiom, defaultaudit 中選擇, 並且等級是可以不標識, 預設為 default; IDENTIFIER 是某個標識, 例如變數的名稱, 只能出現在 ensures 合約表達式; EXPRESSION 是一個表達式, 其結果必須是布林型別或者可以轉型到布林型別.

1.3 合約表達式

通過第 1.2 節, 我們知道合約表達式有三種 : expects 合約表達式, ensures 合約表達式和 assert 合約表達式. 本節我們將詳細介紹這三種合約表達式.

1.3.1 expects 合約表達式

expects 合約表達式是前條件, 它只能出現在函式的參數列表之後, 表達了函式對於某些變數的值的期望 :

int f(int x) [[expects : x >= 0]] {
    if(x == 0) {
        return 0;
    }
    int result {};
    for(auto i {1}; i <= x; ++i) {
        result += i;
    }
    return result;
}

對於 Code 1 中的函式 f, 我們期望引數 x 是大於等於 0 的. 可能是程式設計師考慮到負的引數對程式毫無意義.

1.3.2 assert 合約表達式

assert 合約表達式是斷言, 它只能出現在函式之中, 表達了在函式運作時對某些變數的值的期望. 但是需要注意的是, 此處的 assert 並不是巨集, 而是 Contract 的一個關鍵字, 它們之間也不會產生衝突, 因為這個 assert 被包含在 [[]] 之內 :

void func(int n) {
    int m {};
    //...
    [[assert : n > m]];
    assert(n > m);
}

1.3.3 ensures 合約表達式

ensures 合約表達式是後條件, 它只能出現在函式參數列表之後, 表達了函式終結之後對於某些變數值的期望 :

#include <vector>

void add_element(vector<int> &vec, int x) [[ensures : vec.size() > 0]] {
    // ...
}

Code 3 中的函式是向一個向量中添加一個元素, 因此添加之後不應該出現向量為空的情形, 於是我們為其添加了一個 ensures 合約表達式.

ensures 合約表達式除了可以限制變數之外, 還可以對回傳值進行約束 :

int f() [[ensures x : x > 0]] {
    int result {};
    // ...
    return result;
}

Code 3Code 4ensures 合約表達式的不同之處在於 Code 4ensures 關鍵字之後有一個標識, 這個是 Code 3 中沒有的, 這個標識代表了回傳值, 儘管 xresult 不同名.

1.3.4 合約表達式的結果

當合約表達式中的表達式回傳 true 的時候, 除了因為合約表達式的檢查對函式的運作效能產生了一些影響之外, 不會有什麼明顯的感知. 但是當合約表達式中的表達式回傳 false 的時候, 即代表合約表達式失敗, 這個時候程式會呼叫函式 std::termination 並且強行終結.

1.3.5 對函式的影響

合約表達式並不會對函式的型別產生影響, 所以合約表達式並不是函式型別的一部分. 對於通過函式指標 (這裡其實可以引申為函式物件) 呼叫對應的函式, Contract 至少會被檢查一次. 但是 Contract 什麼時候被檢查, 預設是否對 Contracts 進行檢查是實作定義行為. 在對於 Contract 的檢查中, Contract 中所包含的任何非 volatile 變數, 除了其生命週期之外, 不應該有任何其它變化; 否則的話, 會造成未定行為. 換句話說, 合約表達式中的任何變數在合約表達式中都不應該被更改, 但是編碼器並無需對此進行檢查. 例如 [[expects : x + 1 > 1]] 就是可行的, 而 [[expects : ++x > 1]] 就會產生未定行為.

當一個函式存在 Contract 的時候, 第一次對於函式的宣告必須同時宣告其具有的 Contract, 第二次以後的宣告可以省略 Contract. 如果之後在函式的宣告中, 同時宣告了函式的合約表達式的, 那麼應該和第一次宣告時的合約表達式相同 (包括形式也應該相同) :

void func(int i) [[expects : i >= 0]];        //第一次宣告, OK
void func(int i);        //第二次宣告, OK
void func(int j) [[expects : j >= 0]];        //第三次宣告, OK
void func(int i) [[expects : i > 0]];;        //第四次宣告, 病式宣告, 合約表達式與第一次宣告的合約表達式不同
void func(int i) [[expects : 0 <= i]];        //第五次宣告, 病式宣告, 合約表達式與第一次宣告的合約表達式不同

assert 合約表達式一般處於函式之中, 所以其可以訪問到的變數和函式是一樣的, 也就是可視範圍相同; 而 expects 合約表達式和 ensures 合約表達式處於函式的參數列表之後, 並沒有進入函式, 所以其變數可視範圍只有在函式之外和參數列表中的參數名稱. 在成員函式中, expects 合約表達式和 ensures 合約表達式對於私用成員也沒有任何訪問的特權.

對於被 constexpr 所標識的函式中, 若函式具有 expects 合約表達式或 ensures 合約表達式, 那麼其可視範圍只有函式之外的常數和其參數列表中的常數.

對於友誼函式來說, 合約表達式只可以出現在友誼函式實作時的參數列表之後.

當一個虛擬函式繼承自基礎類別的函式時, 它也同時會繼承函式的合約表達式. 另外, 在衍生類別中, 這個被覆蓋的虛擬函式的合約表達式是不可以被改變的.

對於具有高複雜度的 expects 合約表達式, 推薦使用 assert 合約表達式來替代.

當一個函式具有不同等級的合約表達式的時候, 合約表達式的檢查相互之間不受影響.

1.3.6 Contract 可能是高度實作定義的

P0380R1 中, 規定了這些行為屬於編碼器實作定義行為 :

  • 預設情況下是否檢查合約表達式;
  • 合約表達式失敗之後的處理;
  • 違例處理完成之後的行為.

對於合約表達式失敗之後的處理, 編碼器內部會有一個型別為 void (const std::contract_violation) noexcept 的函式. 其中, noexcept 為可選. 當合約表達式失敗之後, 編碼器會呼叫這樣型別的函式進行違例處理. 而這個型別的函式, 是可以由用戶自訂的. 其中, std::contract_violation (在 P0380R1 中, 它被具體地寫為函式型別 void (const violation_info &)), 其具有以下成員 :

  • int line_number;
  • const char *file_name;
  • const char *function_name;
  • const char *comment : 字面值形式的合約表達式.

根據 P0380R1, 預設的違例處理和 std::abort 有關.

2. 合約等級

通過第 1.2 節, 我們知道合約表達式是存在等級的, 可選的合約等級是 axiom, defaultaudit. 當一個合約表達式沒有標示出合約等級的時候, 該合約表達式的合約等級為預設等級, 即 default.

合約等級代表著檢查所需要的時間, 對於預設的合約等級 default 來說, 其運作時所耗費的時間較小. 而合約等級 axiom, 其並沒有運作期消耗. 因為它的作用只是用於精確註解或 IDE 的靜態分析, 它在運作時不進行檢查. 合約等級 audit, 它的運作期消耗是三者中最多的, 至於它的消耗相比於 default 多在哪裡, P0380R1 中並沒有提到.

對於 axiomaudit 的處理, P0380R1 提出如果出現這樣的合約表達式, 那麼都將其當作標識符號處理 :

void func(int axiom) [[expects axiom : axiom < 0]];        // OK, axiom 等級的 expects 合約表達式, 期望函式參數 axiom < 0

int func() [[ensures audit : audit > 0]];        // OK, audit 等級的 ensures 合約表達式, 期望函式回傳值 audit > 0

但是我們不應該寫出這樣的程式碼, 這種程式碼編碼器可以正確識別, 但是對閱讀程式碼的人可能造成困擾.

3. 評論

Contract 的引入有一定道理, 不過總感覺可有可無. 儘管它被 C++ 20 移除了, 但是之後的 C++ 新標準仍然可能將這個特性納入 C++.