看到一個介紹 C++17 的系列博文(原文),有十來篇的樣子,以爲挺好,看看有時間能不能都簡單翻譯一下,這是第七篇~php
C++17 對 STL 算法的改動,概念上其實很簡單.標準庫以前有超過100個算法,內容包括搜索,計數,區間及元素操做等等.新標準重載了其中69個算法並新增了7個算法.重載的算法和新增的算法都支持指定一個所謂執行策略(execution policy)的參數,經過調整這個參數,你能夠指定算法是以串行,並行或者矢量並行的方式來運行.ios
我以前的文章介紹了不少重載的標準庫算法,有興趣的朋友能夠看看.web
此次,我要介紹一下 C++17 新引入的7個算法:算法
std::for_each_n std::exclusive_scan std::inclusive_scan std::transform_exclusive_scan std::transform_inclusive_scan std::parallel::reduce std::parallel::transform_reduce
其中除了 std::for_each_n 以外,其餘幾個算法的名字都很特殊.爲了理解方便,我先介紹一下 Haskell 中相關的內容,以後再回到C++的講解中.數組
A short detour
C++17 新引入的算法在純函數式語言 Haskell 中都有對應的方法.svg
- for_each_n 對應的方法爲 map.
- exclusive_scan 和 inclusive_scan 對應的方法爲 scanl 和 scanl1
- transform_exclusive_scan 等同於組合使用 map 和 scanl, 而 transform_inclusive_scan 等同於組合或者 map 和 scanl1.
- reduce 對應 foldl 或者 foldl1.
- transform_reduce 對應 map 和 foldl 的組合或者 map 和 foldl1 的組合.
開始講解以前,讓我簡單說一下這些方法的功能做用.函數
- map 能夠對一個列表應用一個函數
- foldl 和 foldl1 能夠對一個列表應用一個二元運算並將結果概括爲一個數值.foldl 與 foldl1 相比額外須要一個初始值.
- scanl 和 scanl1 的操做與 foldl 和 foldl1 基本一致,可是他們會產生全部的中間結果,因此最終你會得到一個列表,而不是一個數值.
- foldl, foldl1, scanl 和 scanl1 的操做都是從列表的左側開始.
下面是一個 Haskell 的相關示例spa
(1) 和 (2) 處的代碼分別定義了一個整數列表(ints)和一個字符串列表(strings).在 (3) 中,我給整數列表(ints)應用了一個 lambda 函數(\a -> a * a).(4) 和 (5) 則更加複雜些:(4) 中我將整數列表中的全部整數對相乘(乘法單位元素1做爲初始元素).(5) 中則作了全部整數對相加的操做.(6), (7), 和 (9) 中的操做可能有些難以理解,你必須從右往左來閱讀這幾個表達式.scanl1 (+) . map(\a -> length a) (即(7)) 是一個函數組合,其中的點號(.)用以組合左右兩個函數.第一個函數將列表中的元素映射爲元素的長度,第二個函數則將這些映射的長度相加.(9) 中的操做和 (7) 很類似,不一樣之處在於 foldl 只產生一個數值(而不是列表)而且須要一個初始元素(我指定初始元素爲0),如今,表達式(8)看起來應該比較簡單了,他以":"做爲分隔符鏈接了列表中的各個字符串元素..net
我想你也許好奇爲何我要在介紹C++的文章中寫這麼多 Haskell 的內容(這些內容還頗具挑戰性),那是由於兩個緣由:翻譯
- 你能夠知道 C++ 中相應算法的歷史
- 比照 Haskell 的對應方法能夠幫助咱們理解 C++ 中 的相應算法.
好了,咱們終於看個C++的示例了.
The seven new algorithms
作好心理準備,下面的代碼可能有些難讀.
#include <iostream> #include <string> #include <vector> #include <execution> int main() { std::cout << std::endl; // for_each_n std::vector<int> intVec{ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 }; // 1 std::for_each_n(std::execution::par, // 2 intVec.begin(), 5, [](int& arg) { arg *= arg; }); std::cout << "for_each_n: "; for (auto v : intVec) std::cout << v << " "; std::cout << "\n\n"; // exclusive_scan and inclusive_scan std::vector<int> resVec{ 1, 2, 3, 4, 5, 6, 7, 8, 9 }; std::exclusive_scan(std::execution::par, // 3 resVec.begin(), resVec.end(), resVec.begin(), 1, [](int fir, int sec) { return fir * sec; }); std::cout << "exclusive_scan: "; for (auto v : resVec) std::cout << v << " "; std::cout << std::endl; std::vector<int> resVec2{ 1, 2, 3, 4, 5, 6, 7, 8, 9 }; std::inclusive_scan(std::execution::par, // 5 resVec2.begin(), resVec2.end(), resVec2.begin(), [](int fir, int sec) { return fir * sec; }, 1); std::cout << "inclusive_scan: "; for (auto v : resVec2) std::cout << v << " "; std::cout << "\n\n"; // transform_exclusive_scan and transform_inclusive_scan std::vector<int> resVec3{ 1, 2, 3, 4, 5, 6, 7, 8, 9 }; std::vector<int> resVec4(resVec3.size()); std::transform_exclusive_scan(std::execution::par, // 6 resVec3.begin(), resVec3.end(), resVec4.begin(), 0, [](int fir, int sec) { return fir + sec; }, [](int arg) { return arg *= arg; }); std::cout << "transform_exclusive_scan: "; for (auto v : resVec4) std::cout << v << " "; std::cout << std::endl; std::vector<std::string> strVec{ "Only", "for", "testing", "purpose" }; // 7 std::vector<int> resVec5(strVec.size()); std::transform_inclusive_scan(std::execution::par, // 8 strVec.begin(), strVec.end(), resVec5.begin(), [](auto fir, auto sec) { return fir + sec; }, [](auto s) { return s.length(); }); std::cout << "transform_inclusive_scan: "; for (auto v : resVec5) std::cout << v << " "; std::cout << "\n\n"; // reduce and transform_reduce std::vector<std::string> strVec2{ "Only", "for", "testing", "purpose" }; std::string res = std::reduce(std::execution::par, // 9 strVec2.begin() + 1, strVec2.end(), strVec2[0], [](auto fir, auto sec) { return fir + ":" + sec; }); std::cout << "reduce: " << res << std::endl; // 11 std::size_t res7 = std::transform_reduce(std::execution::par, strVec2.begin(), strVec2.end(), 0u, [](std::size_t a, std::size_t b) { return a + b; }, [](std::string s) { return s.length(); } ); std::cout << "transform_reduce: " << res7 << std::endl; std::cout << std::endl; return 0; }
與 Haskell 中的示例對應,我使用 std::vector 建立了整數列表 (1) 和字符串列表 (7).
在代碼 (2) 處,我使用 for_each_n 將(整數)列表的前5個整數映射成了整數自身的平方.
exclusive_scan (3) 和 inclusive_scan (5) 很是類似,都是對操做的元素應用一個二元運算,區別在於 exclusive_scan 的迭代操做並不包含列表的最後一個元素, Haskell 中對應的表達式爲: scanl (*) 1 ints.(譯註:結果並不徹底等同, Haskell 的 scanl 操做包含列表最後一個元素,後面提到的相關 Haskell 對應也是如此,注意區別)
transform_exclusive_scan (6) 執行的操做有些複雜,他首先將 lambda 函數 function [](int arg){ return arg *= arg; } 應用到列表 resVec3 的每個元素上,接着再對中間結果(由上一步 lambda 函數產生的臨時列表)應用二元運算 [](int fir, int sec){ return fir + sec; }(以 0 做爲初始元素),最終結果存儲於 resVec4.begin() 開始的內存處.Haskell 中對應表達式爲:
scanl (+) 0 . map(\a -> a * a) $ ints.
(8) 中的 transform_inclusive_scan 和 transform_exclusive_scan 所執行的操做很相似,其中第一步的 lambda 函數將元素映射爲了元素的長度,對應的 Haskell 表達式爲:
scanl1 (+) . map(\a -> length a) $ strings.
如今,代碼中的 reduce 函數 (9) 看起來就比較簡單了,他須要在各個(字符串)元素之間放置 「:」 字符.由於結果的開頭不能帶有 「:」 字符, reduce 的迭代是從第二個元素開始的(strVec2.begin() + 1) ,並以第一個元素做爲初始元素(strVec2[0]). Haskell 中對應表達式爲: foldl1 (\l r -> l ++ 「:」 ++ r) strings.
若是你想深刻了解一下 (11) 中的 transform_reduce,能夠看看我以前的文章,這裏一樣給出 Haskell 中對應的表達式: foldl (+) 0 . map (\a -> length a) $ strings.
程序的輸出以下,有興趣的朋友能夠仔細看看.
Final remarks
C++17 新引入的這7個算法有不少重載版本,調用的時候,你能夠指定初始元素,也能夠不指定初始元素,一樣的,你能夠指定執行策略,也能夠不指定執行策略.你甚至能夠在不指定二元運算的狀況下調用須要二元運算的算法(例如std::reduce),這種狀況下,這些算法會默認使用二元加法運算.爲了可以以並行或者矢量並行的方式運行這些算法,指定給算法的二元運算必須知足可結合性,這個限制也很容易理解,由於並行化的算法很容易會在多個CPU核上同時運行(這種狀況下,二元運算不可結合的話就會致使錯誤結果).更深刻的一些信息你能夠看看這裏和這裏.
本文同步分享在 博客「tkokof1」(CSDN)。
若有侵權,請聯繫 support@oschina.cn 刪除。
本文參與「OSC源創計劃」,歡迎正在閱讀的你也加入,一塊兒分享。