C++ 20 Ranges

| | 0 Comments| 09:25
Categories:

目前 C++ 最新的正式標準、C++ 20 在去年年初就已經正式定案了,當時 Heresy 也有稍微記錄過;其中,也有針對 conceptstemplate lambda 稍微紀錄了一下自己玩的想法。

這篇呢,則是來看一下 C++ 20 的 Ranges 這個新的函式庫吧~

C++20 的 ranges 這個函式庫(CppReference),基本上是針對一組範圍的資料來做處理的函示庫,重點是提供了本身不擁有資料的「view」、以及用來快速串聯操作的「view adapter」的架構。

由於 view 本身基本上都是採用使用 iterator 來做存取的概念,所以在個人來看,應該可以算是 C++ containter(vector 這些)的一個延伸。

<!–more–>

基本的使用

Ranges 這個函示庫主要的 header 檔案是 <ranges>,只要 include 它就可以使用;目前 g++ 10 和 Visual Studio 2019 都有支援。

而在 C++20 的 Ranges 裡面,其實 range 是一種概念(concept),實際上也有很多細緻的定義;實際上拿來操作的主要會是 view。

一個使用 C++20 Ranges 的程式大概長怎樣呢?下面是一個簡單的例子:

#include <iostream>
#include <ranges>
int main()
{
  int aData[] = { 1, 2, 3, 4, 5, 6, 7 };
  auto myView = aData | std::views::filter([](int v) {return v % 2; })
                      | std::views::transform([](int v) {return v * v; });

  for (auto v : myView )
    std::cout << v << "n";
}

在上面的例子裡面,在做的事情就是「找出 aData 這個陣列裡面的奇數、然後平方後輸出」。他的執行結果是:

1
9
25
49

在細節的部分,這邊是透過「|」這個運算子,透過 std::views::filteraData 包裝成一個只有奇數的 view;然後,由於「|」這個運算子是設計成可以串接的,所以上面的例子是再透過 std::view::transform 把這個 view 打包成一個平方的 view、也就是最後的結果 myView(型別是對應 transform 的 std::ranges::transform_view)。

而像 filtertransform,就是 ranges 提供的「range adaptors」了。

而除了透過實際上,他也可以寫成建構子的形式,像下面兩行其實就是一樣的意義。

auto v1 = aData | std::views::filter([](int v) {return v % 2; });
auto v2 = std::views::filter(aData, [](int v) {return v % 2; });

Range 的特性

實際上,他的計算的部分和下面的程式很像:

std::vector<int> vRes;
for (auto v : aData)
{
  if (v % 2)
    vRes.push_back(v * v);
}

使用 ranges 感覺好像變得更複雜了?這邊只是隨便舉例,所以就不要在乎這小問題了。 :p

實際上,使用 ranges 時的狀況還是有點不同的。

Ranges 的各種 view 的一個很大的特色,就是 view 本身不包含資料、也不會在建立時就去做資料的處理;真正的處理是會等到實際要去存取時、才會進行的。

也就是說,除了在資料量很大的時候,view 不會額外占用記憶體外(上面的 vRes 就會占用大量記憶體),技術上也是可以先將 view 建立出來,然後再去填資料。例如:

std::vector<int> vData;
auto myView = vData | std::views::filter([](int v) {return v % 2; })
                    | std::views::transform([](int v) {return v * v; }); vData = { 1,2,3,4,5,6,7 }; for (auto v : myView)
    std::cout << v << "n";

除此之外,也可以先把 view 的組合建立好,套用在不同的資料上。例如:

auto myAdapter = std::views::filter([](int v) {return v % 2; })
               | std::views::transform([](int v) {return v * v; }); int aData[] = { 1,2,3,4,5,6,7 }; for (auto v : aData | myAdapter)
    std::cout << v << "n";

其他 adapter

Ranges 目前也有提供不少 adaptor 和 view 可以使用,除了上面的 filter、transform 外,還有 take、drop、split 等等,基本上已經可以組出很多東西了,有興趣可以自己參考 CppReference(連結)。

上面雖然是拿 int 做例子,但是實際上他不限於用來處理數字,對於文字也是可以用的。像是下面就是一個透過 std:views::join參考)合併字串的例子:

std::vector<std::string> vData = { "Hello", "World"};
for (char c : vData | std::views::join)
{
     std::cout << c; }

而透過 std::views::split參考)也可以做到對字串的切割。

std::string sText = "This is an apple";
for (auto sWord : sText | std::views::split(' '))
{
     for (char c : sWord)
         std::cout << c;
     std::cout << "n"; }

可以看到上面的例子後來都是得逐字元去處理,這是因為得到的結果實際上都不是字串、所以不能直接輸出的關係。

雖然看 CppReference 的例子似乎是可以用 string_view 來玩,但是 Heresy 這邊不管是 MSVC 還是 g++ 都編譯不過就是了…

另外,像是 std::views::elementsstd::views::keysstd::views::values 這三個,感覺應該算是比較特別的。

其中,std::views::elements 是可以用來讀取 tuple 這類的結構的資料(參考);而 std::views::keysstd::views::values 則是用來讀取 pair 這種 key – value 的資料。


Range factory

此外,Ranges 也有提供一些 range factory,可以用來產生 range 的資料。其中,比較可能會用到的,或許會是 std::views::iotastd::ranges::istream_view

std::views::iota參考)可以動態生成資料,下面是一個例子:

auto myView = std::views::iota(1,10);

在上面的例子裡面,會產生 1 – 10 的數值來做後續的操作。

而如果不確定要產生多少的話,則也可以不要加第二個引數,這時候會變成要多少的狀況~所以,後面則可以搭配 std::views::take 這類的 adapter 來做控制。

例如下面就是一個找出五個奇數的例子:

auto myView = std::views::iota(1)
            | std::views::filter([](int v) {return v % 2; })
            | std::views::take(5);

當然在這種條件簡單的狀況,這樣寫的意義不大,但是如果 filter 的條件較為複雜、或是比較沒有規律性的時候,是有可能會有用的。

std::ranges::istream_view 的話,則是可以透過 istream 的資料透過 operator>> 來依序讀出來使用;基本上應該是可以用來搭配 istringstream 這類的東西使用的(參考)。


搭配 STL 演算法

而由於這些 views 都是透過 iterator 操作的,所以在一定程度上是可以搭配 STL 提供的演算法來使用的;例如:

int aData[] = {1,4,6,7,23,45,563,457,47,124,546,5,8};
auto v1 = aData | std::views::filter([](int v) {return v % 2; });
int num = std::count_if(v1.begin(), v1.end(), [](int v) {return v > 100; });

而比較有趣的,是 C++20 在 <algorithm> 中、對應既有的演算法也加入了以 ranges 作為 namespace 的「constrained algorithms」(參考);例如上面的 std::count_if,也有新的 std::ranges::count_if 的版本:

int num = std::ranges::count_if(v1, [](int v) {return v > 100; });

新版本除了一樣可以傳一組 iterator(一般就是 begin()end())進去外,也可以直接傳一個 range object 進去,看起來相對更簡潔了。

此外,新的版本還有支援「projection」的功能,可以在處理每一項之前,就先套用一些計算、處理,讓使用 STL 的演算法變得更為彈性。

不過相對地,目前的 constrained algorithms 似乎都不支援平行化處理(Execution policies、參考),讓人覺得有點可惜…

另外,由於 ranges 各種 view 可能有不同的性質、限制,所以似乎不一定都能套用在既有的 STL 演算法上;像是 std::views::iota 在 MSVC 上似乎就不能在平行的 for_each 使用。

auto v = std::views::iota(1, 100);
std::for_each(std::execution::par, v.begin(), v.end(), [](int i) {
  std::cout << i << std::endl;
});

上面的程式在 MSVC 是無法正確編譯的,但是在 g++10 上似乎沒有問題?這部分可能和實作方法有關就是了。

不過這邊最大的問題…因該還是 Heresy 本身很少會去用 STL 的 algorithm 吧? XD


這篇 Ranges 的紀錄大概就先這樣了。

整個看了一下,老實說…個人是覺得有點微妙,因為同樣的需求,感覺既有的方法也都可以相對簡單地實做出來,不一定要用到 ranges;所以,變成現階段還不知道能用在哪裡?

或許等以後真的碰到了,才會知道該怎麼用比較合適吧…

最後,現在主要的編譯器也都已經有支援 C++20 大部分的功能了(參考),所以好像也可以開始考慮把開發環境轉換到 C++20 了?


參考:

Leave a Reply

發佈留言必須填寫的電子郵件地址不會公開。 必填欄位標示為 *