C++ 語言規則與具名要求

最後更新: 2023-09-23 12:23:05 +0800 +0800
原文以英文撰寫。您正在閱讀AI翻譯版本。

C++ 語言規則決定程式是否為良構,以及其執行具有何種意義。函式庫的具名要求與概念則為泛型程式碼增添契約。相似術語可能掩蓋一項重要差異:編譯器可以檢查運算式與型別,卻無法證明型別承諾的所有語意、複雜度、有效性或相等性。

定義與單一定義規則 #

每個經 ODR 使用的非行內函式或變數,在程式中都需要一個定義。相容的宣告可以重複,因此能夠使用前向宣告。

cpp
class Node;

struct Node {
    Node* next = nullptr;
};

型別、範本、行內函式與行內變數只有在符合一致性要求時,才能於多個翻譯單元中具有定義。標頭防護可防止同一翻譯單元內的重複引入;它無法修復翻譯單元之間內容不同的定義。程式即使違反 ODR,仍可能通過編譯與連結,但其行為並不可靠。

具名要求與概念 #

在 C++20 以前,標準函式庫的泛型契約以 LegacyInputIterator 等具名要求表示。C++20 概念則將其中許多語法部分表示為條件約束。

cpp
#include <concepts>
#include <iterator>

template<std::input_iterator Iterator>
void consume(Iterator first, Iterator last);

受約束的宣告會拒絕不符合必要運算式及關聯型別關係的型別。符合約束並不代表語意行為成立。某個型別可以讓 ++i、*i 與 i == j 成為良構,卻仍違反多趟走訪、相等性保持或指定的複雜度界限。相同界線也適用於均勻隨機位元產生器:型別檢查無法證明其輸出呈均勻分布。

撰寫泛型契約 #

以分層方式規範泛型 API:

  1. 說明必要的運算式與關聯型別,並優先使用標準概念或專案概念。
  2. 說明語意不變量:操作的意義、哪些運算式會保持相等性,以及值在呼叫後是否仍然有效。
  3. 說明複雜度、迭代器失效、所有權與並行保證。
  4. 說明執行階段呼叫端必須遵守或觀察的前置條件與後置條件。

例如,用作鍵值述詞的相等關係,必須對演算法接受的值構成等價關係。與該述詞搭配的雜湊,必須對相等的鍵值產生相同結果。任何 requires 運算式都無法證明這兩項性質。

有效性也是執行階段性質。某個迭代器型別可在語法上符合 std::input_iterator,但該型別的特定迭代器可能已因容器操作而失效。除了型別要求,也要記載哪些操作會使現有迭代器、指標或參考失效。

編譯期指定詞具有不同契約 #

當呼叫與運算元符合適用規則時,constexpr 允許實體或函式參與常數求值。consteval 讓立即函式呼叫必須進行常數求值。constinit 適用於具有靜態或執行緒儲存期的變數,並要求常數初始化;它不會讓變數不可變,也不會讓變數可用作常數運算式。

cpp
constexpr int square(int value) { return value * value; }
consteval int checked_port(int value) { return value > 0 ? value : throw "port"; }
constinit int process_generation = 1;

應根據這些指定詞各自聲明的保證使用它們,而不要視為可互換的「編譯期」註記。const 物件可以在執行階段初始化;constinit 物件若未同時宣告為 const,之後仍可修改。

執行階段行為仍是契約的一部分 #

良構的範本實體化不代表程式正確。配置可能失敗、比較可能擲回例外、回呼可能違反可重新進入假設,而範圍也可能為空。應說明無法由型別系統表示的行為,並在無效輸入或外部狀態進入程式的邊界進行測試。

複雜度同樣屬於契約。以線性掃描取代常數時間查詢,可以保留所有必要運算式,卻破壞仰賴其複雜度界限的演算法。應將具名要求、概念與 API 文件視為互補:條件約束能產生實用的診斷,而語意文件則定義泛型程式碼可仰賴的行為。

評論

在 GitHub 上閱讀或評論(發表評論需要登入)