目前 C++ 最新的正式標準、C++ 20 在去年年初就已經正式定案了,當時 Heresy 也有稍微記錄過;其中,也有針對 concepts 和 template 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::filter 將 aData 包裝成一個只有奇數的 view;然後,由於「|」這個運算子是設計成可以串接的,所以上面的例子是再透過 std::view::transform 把這個 view 打包成一個平方的 view、也就是最後的結果 myView(型別是對應 transform 的 std::ranges::transform_view)。
而像 filter 和 transform,就是 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::elements、std::views::keys、std::views::values 這三個,感覺應該算是比較特別的。
其中,std::views::elements 是可以用來讀取 tuple 這類的結構的資料(參考);而 std::views::keys、std::views::values 則是用來讀取 pair 這種 key – value 的資料。
Range factory
此外,Ranges 也有提供一些 range factory,可以用來產生 range 的資料。其中,比較可能會用到的,或許會是 std::views::iota 和 std::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 了?
參考: