跳至內容

C++/ranges

維基教科書,自由的教學讀本
< C++

<ranges>是C++20引入的標準庫。<ranges>中定義了std::ranges和std::views命名空間。

基本概念

[編輯]

範圍」(range)定義為:任何能夠提供迭代器對 [begin, end) 的對象。即:能被begin/end遍歷的一段東西。這包括標準容器:vector, list, array等;原生數組:int arr[10];特殊的範圍range:istream_view (從流讀取),iota_view (生成序列)。

// Range 概念的基本要求
template<typename T>
concept Range = requires(T& t) {
    std::ranges::begin(t);  // 必须有 begin()
    std::ranges::end(t);    // 必须有 end()
};
#include <ranges>
#include <vector>
#include <string>
#include <array>

// 验证各种类型是否满足 Range 概念
static_assert(std::ranges::range<std::vector<int>>);     // true
static_assert(std::ranges::range<std::string>);          // true
static_assert(std::ranges::range<std::array<int, 5>>);   // true
static_assert(std::ranges::range<int[10]>);              // true

視圖」(view):輕量級的範圍range的包裝器(零拷貝),是輕量級、非擁有的range,支持常數時間複製、移動和賦值。惰性組合、惰性求值(操作延遲到迭代時)、時間複雜度O(1)構造/析構、可組合性(通過 | 管道符連接)、通常不擁有數據(或特例擁有少量數據),僅引用底層範圍range。從底層的範圍range返回數據,但不擁有任何數據。本質上,迭代時view返回下一個符合過濾條件的元素。

// View 概念的要求
template<typename T>
concept View = std::ranges::range<T> && 
               std::movable<T> && 
               std::ranges::view_base<T>;
#include <ranges>
#include <vector>

// 验证各种视图类型
static_assert(std::ranges::view<std::ranges::iota_view<int, int>>);  // true
static_assert(std::ranges::view<decltype(std::views::iota(0) | std::views::take(5))>);  // true

// 容器不是 View(因为复制成本高)
static_assert(!std::ranges::view<std::vector<int>>);  // false

範圍工廠(Range factory):從「無現成輸入range」開始,製造一個 range/view。包括:

  • 對象std::views::empty, void -> view, 使用std::views::empty<T>即可直接獲得對象
  • 對象std::views::single, any -> view, 單對象的std::ranges::view
  • 對象std::views::iota, iterator | (iterator, sentinel) -> view, 一般哨位邊界的有限或無限遞增序列 例如,itoa將生成一系列遞增的值,如auto rnums =std::views::iota(1,10);
  • 對象std::views::counted, (iterator, count) -> view, 計數哨位邊界的有限遞增序列
  • 對象std::views::istream<T>, istream<T> -> view, 輸入流轉std::ranges::view
  • 類型std::ranges::subrange, (iterator, sentinel, [size]) | (borrowed_range, [size]) -> subvrange, 迭代器-哨位對
  • 類型std::ranges::ref_view, range -> viewable_range 借用
  • 類型std::ranges::owning_view, range -> viewable_range 佔用
  • 對象std::views::repeat(C++23), 由重複產生相同值的生成序列組成的視圖
  • 對象std::views::cartesian_product(C++23), 由n元笛卡爾積計算出的結果元組組成的視圖

視圖適配器(Range adaptor)是一個函數對象,屬於命名空間std::views,可接受一個已有的range,返回一個range或者視圖對象view。視圖適配器可以使用|操作符連接到其他視圖適配器。視圖適配器從|操作符的左側獲得範圍操作數。|操作符會從左到右求值。為什麼叫 adaptor?因為它通常不擁有數據,而是對已有 range 做一層包裝。包括: // 元素操作

  • transform(fn) // 映射元素
  • filter(pred) // 條件過濾

// 結構操作

  • take(n) // 取前n元素
  • drop(n) // 跳過前n元素
  • reverse // 逆序
  • keys/values // 鍵/值提取(針對pair-like)

// 生成操作

  • iota(start) // 無限序列生成
  • empty<T> // 空範圍

一個簡單例子:

#include <ranges>
#include <vector>
#include <iostream>

int main() {
    std::vector<int> numbers = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
    
    // 适配器可以存储为对象
    auto filter_even = std::views::filter([](int n) { return n % 2 == 0; });
    auto take_three = std::views::take(3);
    auto square = std::views::transform([](int n) { return n * n; });
    
    // 组合使用
    auto result = numbers | filter_even | take_three | square;
    
    for (int n : result) {
        std::cout << n << " ";  // 输出: 4 16 36
    }
    
    return 0;
}

ranges庫在std::views命名空間中包括一些變換函數:

  • take(int)選出view里的前N個元素。如果改編後的視圖包含的元素少於N,則返回所有元素。
  • take_while()給定一個一元謂詞 pred 和一個視圖 r,它生成一個範圍 [begin(r), ranges::find_if_not(r, pred)) 的視圖。
  • reverse()逆序雙向視圖裏的所有元素
  • filter(invokable)視圖使用一個謂詞函數篩選出view里的特定元素,例如auto result = nums|std::views::filter([](int i){return 0==i%2;});
  • transform(invokable)視圖使用了一個轉換函數,對每個元素應用轉換函數後,返回底層range的視圖。例如auto result =nums|std::views::transform([](int i){return i*i;});
  • drop()排除view里的前N個元素,如果改編後的視圖包含少於 N 個元素,則返回一個空範圍。例如:rnums = std::views::drop(rnums,5);
  • drop_while()給定一個一元謂詞 pred 和一個視圖 r,它生成一個範圍 [ranges::find_if_not(r, pred), ranges::end(r)) 的視圖。
  • keys()選出pair里的first成員。是elements_view<views::all_t<R>, 0> 的別名。
  • values()選出pair里的second成員。是elements_view<views::all_t<R>, 1>的別名。
  • all() 把一個 range 轉成 view:如果輸入本身就是 view,直接返回;如果是左值容器,可能返回 ref_view;如果是右值且滿足條件,可能返回owning_view。
  • join()將二維/多維的範圍視圖平展為一維視圖
  • split(forward_range & view)接受一個視圖和一個分隔符,並將視圖劃分為分隔符上的子範圍。分隔符可以是單個元素,也可以是元素的視圖。
  • common()或common_range()獲取一個迭代器和哨兵具有不同類型的視圖,並將其轉換為具有相同類型迭代器和哨兵的相同元素的視圖。對於調用期望範圍的迭代器和哨點類型相同的遺留算法很有用。
  • counted()計數視圖顯示了迭代器i和非負整數 n 的計數範圍([iterator.requirements.general]) i+[0, n) 的元素的視圖。
  • elements()接受一個類元組值和 size_t 的視圖,並生成一個值類型為已改編視圖值類型的第 n 個元素的視圖。
  • zip(C++23) 由對已改編視圖的相應元素的引用的元組組成的視圖
  • zip_transform(C++23) 由轉換函數應用到所適應視圖的相應元素的結果元組組成的視圖
  • adjacent(C++23) 由對已改編視圖的相鄰元素的引用元組組成的視圖
  • adjacent_transform(C++23) 由轉換函數應用於所適應視圖的相鄰元素的結果元組組成
  • join_with(C++23) 一種視圖,由將範圍視圖平展得到的序列組成,元素之間有分隔符
  • slide(C++23) 第 M 個元素是另一個視圖的第 M 個到 (M + N - 1) 個元素的視圖
  • chunk(C++23) 一個由另一個視圖的元素組成的n個大小的不重疊連續塊的視圖範圍
  • chunk_by(C++23) 將視圖拆分為給定謂詞返回false的每對相鄰元素之間的子範圍
  • as_const(C++23) 將視圖轉換為常量範圍
  • as_rvalue(C++23) 將每個元素強制轉換為右值的序列視圖
  • stride(C++23) 由另一個視圖的元素組成的視圖,一次向前移動N個元素

range是容器上的一個抽象概念,可以理解成指明首末位置的迭代器,即pair<begin,end>,這樣range自身就包含能用於算法的足夠信息,大多數算法只要用一個range參數就可以工作。基於range的概念,C++在名字空間std::ranges提供了與標準算法同名、但卻使用range參數的算法,寫法很簡潔。從C++20開始,<algorithm>頭文件中的大多數算法都會基於「範圍」。這些版本在<algorithm>頭文件中,但是在std::ranges命名空間中,這使它們與傳統算法分離開。所以無需再調用兩個迭代器的算法。例如std::sort(v.begin(),v.end());可以用範圍來調用std::ranges::sort(v);使用試圖適配器更為直觀、易讀:std::ranges::sort(v|std::ranges::view::reverse|std::ranges::views::drop(5));

range的分類

[編輯]
標題文本
Concept Description
std::ranges::input_range can be iterated from beginning to end at least once
std::ranges::forward_range can be iterated from beginning to end multiple times
std::ranges::bidirectional_range iterator can also move backwards with --
std::ranges::random_access_range can jump to elements in constant-time []
std::ranges::contiguous_range elements are always stored consecutively in memory
std::ranges::sized_range 範圍的大小可由std::ranges::size()獲得,提供size函數,能常數時間獲取範圍的長度;如果未提供size函數, 那麼還要求

std::forward_iterator且iterator與sentinel(及iterator)可作差;如果size或者iterator的作差不能用常數時間實現, 那麼可以用特化:std::ranges::disable_sized_range<T> = true且std::disable_sized_sentinel_for<iterator, iterator> = true, 強制關閉std::ranges::sized_range和std::ranges::sized_sentinel_for的特徵

std::ranges::common_range 如果iterator == sentinel
std::ranges::viewable_range 如果std::ranges::view 或std::ranges::borrowed_range
std::ranges::borrowed_range 如果類型std::ranges::range T的值T t的t.begin()和t.end()獲得的迭代器的生命周期與t無關, 那麼可以認為是std::ranges::borrowed_range。由於語言層面無法自動識別生命周期的關係, 因此要特徵能被識別, 還要手動特化std::ranges::enable_borrowed_range<T>為true

對照實現C#的LINQ的功能

[編輯]

std::ranges基本上可以一對一的實現C#的LINQ的功能:

C# LINQ 方法 C++20 std::ranges/views 組件 功能說明
Where std::views::filter 過濾滿足條件的元素
Select std::views::transform 元素映射/轉換
Take std::views::take 獲取前 N 個元素
Skip std::views::drop 跳過前 N 個元素
TakeWhile std::views::take_while 一直取元素直到條件不成立
SkipWhile std::views::drop_while 一直跳過元素直到條件不成立
OrderBy std::ranges::sort 排序
Reverse std::views::reverse 反轉序列
Any / All / Contains std::ranges::any_of / std::ranges::all_of 判斷元素是否存在/全部滿足
Zip std::views::zip 合併多個序列
Concat std::views::concat 連接兩個序列
Range std::views::iota 生成連續數字序列

二者都是延遲執行(懶加載),不遍歷就不真正計算,零拷貝、高性能。都能操作任何可枚舉集合(數組、vector、string、list等等)。

例如,C#的LINQ程序:

var result = numbers
    .Where(x => x % 2 == 0)
    .Select(x => x * 2)
    .Skip(2)
    .Take(5);

對應於C++程序:

auto result = numbers
    | std::views::filter([](int x) { return x % 2 == 0; })
    | std::views::transform([](int x) { return x * 2; })
    | std::views::drop(2)
    | std::views::take(5);