C++20 概念與限制

最後更新: 2025-12-23 21:51:39 +0800 +0800
原文以英文撰寫。您正在閱讀AI翻譯版本。

C++20 概念讓樣板宣告能在其介面中陳述可由編譯器檢查的要求。只有在其相關限制滿足時,受限制的候選項才會參與多載解析。因此,限制可用暴露預期作業集合的宣告,取代許多 std::enable_if SFINAE 用法;但限制無法證明 API 文件所述的語意要求。

宣告限制 #

概念是命名空間範圍、類似變數的樣板,其定義是 bool 常數運算式:

cpp
#include <concepts>

template <class T>
concept Addable = requires(T value) {
    { value + value } -> std::convertible_to<T>;
};

使用 requires 子句、受限制的樣板參數或縮寫函式樣板套用概念:

cpp
template <class T>
    requires Addable<T>
T sum(T left, T right) {
    return left + right;
}

std::integral auto increment(std::integral auto value) {
    return value + 1;
}

每個 std::integral auto 都會引入獨立的樣板參數。兩個引數必須具有相同推導型別時,請使用明確參數。尾端 requires 子句在限制接續宣告子之後時很有用,包括類別樣板的成員:

cpp
template <class T>
struct Buffer {
    void compact() requires std::movable<T>;
};

requires 子句不能套用至非樣板的自由函式。多個宣告位置的限制會以 && 組合;替換依賴某項先決條件的要求時,請將先決條件放在前面。

Requires 運算式與語意要求 #

requires 運算式會檢查要求而不評估其運算元。它支援簡單要求、型別要求、複合要求與巢狀要求:

cpp
#include <concepts>
#include <iterator>

template <class T>
concept SortableRange = requires(T range) {
    typename T::value_type;                         // type
    { range.begin() } -> std::input_or_output_iterator; // compound
    range.end();                                    // simple
    requires std::movable<typename T::value_type>;  // nested
};

替換後無效的要求會使相依限制不滿足,而不是在呼叫點產生診斷。對所有可能替換都無效的要求則是不合法的,且不要求診斷。

滿足條件是語法性的。型別可能滿足相等運算式要求,卻違反 std::equality_comparable 預期的等價關係;型別可能讓 swap 編譯成功,卻違反其所述的複雜度或後置條件。請記錄並測試這些語意要求,因為編譯器無法建立它們。

從 enable_if 遷移 #

請遷移既有的選取原則,而不要僅因概念可用就擴大多載集合:

cpp
// Before C++20.
template <class T, std::enable_if_t<std::integral<T>, int> = 0>
T twice(T value) { return value + value; }

// C++20, with the same intended eligibility.
template <std::integral T>
T twice(T value) { return value + value; }

限制會選取候選項的可行性;它們不會將已選取函式本體中的錯誤轉為替換失敗。取代 enable_if 時,請保留排除案例、轉換與排序測試,尤其是舊多載使用預設樣板參數或回傳型別 SFINAE 的情況。

排序與包含 #

當多個受限制多載都滿足時,語言會使用限制包含關係選取限制較多的候選項。包含關係是結構性的,依據正規化的原子限制,而不是任意的布林等價。請命名共用述詞並組合它們:

cpp
template <class T>
concept Range = requires(T value) { value.begin(); value.end(); };

template <class T>
concept SizedRange = Range<T> && requires(T value) { value.size(); };

template <Range T>
void inspect(const T&);

template <SizedRange T>
void inspect(const T&); // selected when both are satisfied

重複恰好具有相同意義的運算式,可能建立不同的原子限制,使多載產生歧義。具名概念會保留排序規則所需的原子限制識別。

轉送參照與客製化點 #

對 T 已推導的 T&&,左值引數會將 T 推導為左值參照型別。契約涉及底層物件時,請限制 std::remove_cvref_t<T>:

cpp
#include <ranges>
#include <type_traits>

template <class R>
    requires std::ranges::range<std::remove_cvref_t<R>>
void consume(R&& range);

只有在刻意需要參照限定時,才直接限制 R。這項區別可避免左值僅因概念是為物件型別撰寫,就遭到拒絕。

客製化點物件(CPO)通常是 inline constexpr 函式物件,其受限制的呼叫運算子實作定義的客製化協定。例如,std::ranges::begin 可以從陣列、適合的成員 begin,或透過引數相依查找找到的未限定 begin 取得迭代器。限制應在 CPO 邊界描述支援的協定,讓不支援的型別在該介面失敗,而不是在選取的客製化內部深處失敗。

評論

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