C++ 範圍與檢視

最後更新: 2021-10-01 09:45:43 +0800 +0800
原文以英文撰寫。您正在閱讀AI翻譯版本。

範圍函式庫將迭代能力與所有權、走訪強度及延遲轉換分開。範圍提供迭代界限;檢視是適合配接器管線的範圍;配接器會建立新的檢視,其元素會在迭代要求時評估。

cpp
#include <ranges>
#include <vector>

std::vector<int> source{1, 2, 3, 4, 5};
auto even_squares = source
    | std::views::filter([](int value) { return value % 2 == 0; })
    | std::views::transform([](int value) { return value * value; });

建構 even_squares 不會轉換每個元素。迭代會呼叫述詞與轉換,因此迭代前對 source 的變更可能被觀察到,但仍受來源容器失效規則約束。

核心概念 #

當 std::ranges::begin 與 std::ranges::end 對該型別的左值有效時,該型別便符合 std::ranges::range 模型。迭代器與哨兵型別不必相同。此概念只建立迭代可行性;它不涉及所有權、大小、多次走訪、連續性或迭代器生命週期。

cpp
template<class T>
concept range = requires(T& value) {
    std::ranges::begin(value);
    std::ranges::end(value);
};

標準的定義具有等效意圖,但使用自己的僅供說明細節。std::vector<int> 與 std::vector<int>& 都符合 range 模型。

std::ranges::view 為輕量且可調適的範圍物件細化 range。其語法定義結合 range、std::movable 與 std::ranges::enable_view;重要要求是語意上的。檢視必須具有常數時間移動建構,而複製、移動、指派與解構要求會防止配接器管線意外執行逐元素工作。std::vector 是範圍但不是檢視,因為複製其元素不是常數時間。std::string_view、std::ranges::ref_view 與許多產生的範圍都是檢視。

程式定義的檢視通常衍生自 std::ranges::view_interface 或 std::ranges::view_base。view_interface<D> 是 CRTP 輔助器,只有在 D 具備所需範圍能力時,才有條件地提供 empty、front、back、data、size 與索引等成員。程式可以將 enable_view 特化為 true,但只有對實際符合檢視要求的 cv 未限定程式定義型別才能這樣做;特化是語意承諾,而不是繞過限制。

viewable_range 與 views::all #

viewable_range 回答不同問題:某個特定範圍運算式是否可以轉換成檢視。它接受可建構檢視與適當的左值或可移動非檢視輸入,同時排除有問題的 initializer-list 案例。規格自 C++20 以來持續演進,因此應用程式程式碼應使用該概念,而不是重現其定義。

std::views::all 是轉換邊界。概念上,它保留可建構的輸入檢視,以 ref_view 包裝非檢視左值,並以 owning_view 包裝合格的非檢視右值。

cpp
std::vector<int> values{1, 2, 3};
auto borrowed = std::views::all(values);              // ref_view
static_assert(std::ranges::view<decltype(borrowed)>);

auto owned = std::views::all(std::vector<int>{1, 2}); // owning_view

ref_view 不會延長來源物件的生命週期。因此,回傳參照區域容器的檢視是錯誤的。並非每個檢視都不擁有內容:owning_view 擁有其基礎範圍,而 single_view 儲存其值。請根據具體檢視與來源運算式判斷生命週期,而不是根據「view」一詞判斷。

borrowed_range 是另一項獨立屬性。它允許演算法從右值範圍回傳迭代器,而不是 std::ranges::dangling,因為具體範圍的迭代器可以在範圍物件銷毀後存活。它不代表每個迭代器都永久有效;容器變更與底層儲存的生命週期仍然重要。

工廠與配接器 #

工廠建立來源檢視,而配接器轉換現有的 viewable_range。

工具類型標準基線
views::empty<T>空來源檢視C++20
views::single(value)一個儲存的值C++20
views::iota(first, bound)遞增的產生序列C++20
views::istream<T>(stream)延遲擷取的串流值C++20
views::repeat(value, bound)重複值C++23
views::cartesian_product(...)笛卡兒積C++23
views::counted(iterator, count)計數來源範圍C++20

views::transform、filter、take、drop、reverse、split、lazy_split、join、common、elements、keys 與 values 是 C++20 配接器。C++23 增加 adjacent、slide、chunk、stride、zip、zip_transform、join_with 與 as_const 等配接器。可用性取決於選取的語言模式與標準函式庫的實作。

配接器物件可以直接呼叫或部分套用:

cpp
auto doubled = std::views::transform(source, [](int value) {
    return value * 2;
});

auto double_each = std::views::transform([](int value) {
    return value * 2;
});
auto also_doubled = double_each(source);

部分套用的結果是範圍配接器閉包物件。對閉包 closure 與 viewable_range range,range | closure 的效果等同於 closure(range)。閉包可以組合:

cpp
auto useful = std::views::filter(is_valid) | std::views::transform(normalize);
auto result = source | useful;

C++23 提供 std::ranges::range_adaptor_closure,用於可攜式的使用者定義閉包。在 C++23 之前,實作內部的閉包基底不是應用程式介面。

範圍屬性與工具 #

細化概念描述範圍的獨立屬性:

概念意義
input_range支援單次走訪輸入。
forward_range支援多次向前走訪。
bidirectional_range支援向後走訪。
random_access_range支援常數時間的索引迭代器移動。
contiguous_range元素佔用連續儲存空間。
sized_rangeranges::size 有效且攤銷後為常數複雜度。
common_range迭代器與哨兵具有相同型別。
borrowed_range右值來源迭代器不會僅因範圍銷毀而懸空。

走訪概念會從 input 細化至 contiguous。sized_range、common_range 與 borrowed_range 描述分開的維度。C++23 增加 constant_range;只有在專案基線提供時才使用它。

自訂點物件能在泛型程式碼中正確處理陣列、成員與 ADL:

cpp
auto first = std::ranges::begin(source);
auto last = std::ranges::end(source);
auto count = std::ranges::size(source);

其他工具物件包括 cbegin、cend、rbegin、rend、empty、data、cdata 與 ssize。優先使用它們,而不要假設存在 .begin() 成員。

範圍型別別名會保留迭代器與代理參照行為:

cpp
template<std::ranges::range R>
using iterator_t = std::ranges::iterator_t<R>;

template<std::ranges::range R>
using value_t = std::ranges::range_value_t<R>;

template<std::ranges::range R>
using reference_t = std::ranges::range_reference_t<R>;

相關別名包括 sentinel_t、range_difference_t、range_size_t 與 range_rvalue_reference_t。const_iterator_t 與 range_const_reference_t 等 const 導向別名受 C++23 可用性限制。

演算法、投影與具體化 #

範圍演算法直接接受範圍,並可以在主要操作前套用投影。投影會改變一個演算法觀察元素的方式;轉換檢視則改變迭代產生的內容。

cpp
struct Record {
    int id;
    std::string name;
};

std::ranges::sort(records, {}, &Record::name);

空比較器會選取預設順序,而投影會將成員名稱提供給它。投影不會具體化轉換後的值。

需要擁有結果、來源變更後的穩定走訪,或急切邊界時,請具體化檢視。在 C++23 中,std::ranges::to 直接表達轉換:

cpp
auto output = std::ranges::to<std::vector<int>>(even_squares);

在 C++20 基線上,只有當容器支援該迭代器與哨兵配對,或透過適當範圍/迭代器介面插入時,才從檢視的迭代器與哨兵建構相容容器。具體化是執行延遲工作並建立分開結果的時點;它應該是刻意行為,而不是選擇容器 API 的意外副作用。

評論

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