0. 寫在最前面
本文持續更新地址:https://haoqchen.site/2020/06/08/all-kind-of-loop-2/
上一篇文章C++各種循環方式梳理及對比之深入到彙編看while和for深入到彙編對比了while和for的效率問題,這篇將集中在另外幾種看上去比較高大上的循環寫法。
這些寫法一般只是for或者while的一層封裝,效率與自己實現的for循環相當,甚至要差。但他們優勢在於簡化了代碼,並且減少了代碼出錯的可能。另外,C++17之後的algorithm庫實現了並行運算的功能,可以快捷地通過參數配置並行計算,不用自己敲多線程。我暫時還沒有到C++17,沒能力介紹這方面的內容,有興趣可以看看對應的官網鏈接,在參考中有給出。
如果覺得寫得還不錯,可以找我其他文章來看看哦~~~可以的話幫我github點個讚唄。
你的Star是作者堅持下去的最大動力哦~~~
1. std::for_each與std::for_each_n
1.1 定義
1.1.1 std::for_each
template <class InputIterator, class Function>
Function for_each (InputIterator first, InputIterator last, Function fn);
第一和第二個參數分別是迭代器的首尾地址,最後一個傳入的是函數對象。這就要求:
- 遍歷的對象必須是實現了迭代器的結構,比如std::vector、std::queue等。
- 要將處理方法封裝成函數對象,包括lambda表達式、仿函數對象、函數指針、std::function等。
官網說了,這個函數的功能跟下面的代碼是等效的:
template<class InputIterator, class Function>
Function for_each(InputIterator first, InputIterator last, Function fn)
{
while (first!=last) {
fn (*first);
++first;
}
return fn; // or, since C++11: return move(fn);
}
說白了就是一個利用迭代器實現的while遍歷,這是在C++11的auto之前出現的。
1.1.2 std::for_each_n
template< class InputIt, class Size, class UnaryFunction > // since C++17
InputIt for_each_n( InputIt first, Size n, UnaryFunction f );
std::for_each
只遍歷n個的版本,與下面的代碼等效:
template<class InputIt, class Size, class UnaryFunction>
InputIt for_each_n(InputIt first, Size n, UnaryFunction f)
{
for (Size i = 0; i < n; ++first, (void) ++i) {
f(*first);
}
return first;
}
在不設置並行執行規則ExecutionPolicy
的情況下,這兩個函數的執行是保證按順序執行的。
1.2 用法
最簡單就是使用lambda表達式來實現了:
#include <vector>
#include <algorithm>
std::vector<int> container(10, 0);
std::for_each(container.begin(), container.end(), [](int& i){
i+= 10;
});
std::for_each_n(container.begin(), 10, [](int& i){
i+= 10;
});
比如求平均等更多的應用可以參考如何使用std::for_each以及基於範圍的for循環 這篇文章。
我嘗試去找這種用法跟我們最原始的for-loop的區別,各位大佬的意思是,for_each是auto之前的產物,主要防止新手用for-loop各種出錯,而且能避免不會用而導致性能下降。還有降低圈複雜度的???
比如很多人會寫成for(auto it = c.begin(); it <= c.end(); ++it)
,但不是所有迭代器都實現了小於、大於號,要寫成for(auto it = c.begin(); it != c.end(); ++it)
2. 基於範圍(range-based)的for循環
2.1 定義
這是C++ 11新增的一種循環,主要作用是簡化一種常見的循環任務:對數組或容器類(如vector和array)的每個元素執行相同的操作。
attr(optional) for ( range_declaration : range_expression )
loop_statement // (until C++20)
attr(optional) for ( init-statement(optional)range_declaration : range_expression )
loop_statement // (since C++20)
- attr:函數前綴,貌似聲明一些特性有用的,可選。目前不是很清楚,有興趣可瞭解attribute specifier sequence(since C++11)
- init-statement(optional:這個是C++20才加上的,一個以分號
;
結尾的表達式。一般是一個初始化表達式 - range_declaration:聲明一個變量,變量的類型爲range_expression的類型或者這個類型的引用,一般用auto來自動匹配即可。這個可以是結構化綁定聲明(Structured binding declaration)。
- range_expression:需要循環的數組、容器或花括號初始化列表,如果爲容器,必須要實現begin函數和end函數。
基於範圍的for循環可等效成下面的for:
{
auto && __range = range_expression ;
for (auto __begin = begin_expr, __end = end_expr; __begin != __end; ++__begin) {
range_declaration = *__begin;
loop_statement
} // (until C++17)
}
結構化綁定聲明:
for (auto&& [first,second] : mymap) { // since C++17
// 使用 first 和 second
}
注意:
range_expression
不能返回臨時變量,例如不能是一個返回值的函數,否則將導致不確定行爲。- 如果
range_declaration
不是引用,而且存在copy-on-write
特性,基於範圍的for循環可能會觸發一個深拷貝
2.2 用法
借用cppreference的一個例子來說明:
#include <iostream>
#include <vector>
int main() {
std::vector<int> v = {0, 1, 2, 3, 4, 5};
for (const int& i : v) // 以 const 引用訪問
std::cout << i << ' ';
std::cout << '\n';
for (auto i : v) // 以值訪問,i 的類型是 int
std::cout << i << ' ';
std::cout << '\n';
for (auto& i : v) // 以引用訪問,i 的類型是 int&
std::cout << i << ' ';
std::cout << '\n';
for (int n : {0, 1, 2, 3, 4, 5}) // 初始化器可以是花括號初始化器列表
std::cout << n << ' ';
std::cout << '\n';
int a[] = {0, 1, 2, 3, 4, 5};
for (int n : a) // 初始化器可以是數組
std::cout << n << ' ';
std::cout << '\n';
for (int n : a)
std::cout << 1 << ' '; // 不必使用循環變量
std::cout << '\n';
}
3. std::transform
3.1 定義
這個函數的作用是將輸入的,具有迭代器的1個或2個容器InputIterator
做一定的操作,並將結果保存到result
的起始位置中,執行順序不做保證。
// 定義1
template <class InputIterator, class OutputIterator, class UnaryOperation>
OutputIterator transform (InputIterator first1, InputIterator last1,
OutputIterator result, UnaryOperation op);
// 定義2
template <class InputIterator1, class InputIterator2,
class OutputIterator, class BinaryOperation>
OutputIterator transform (InputIterator1 first1, InputIterator1 last1,
InputIterator2 first2, OutputIterator result,
BinaryOperation binary_op);
- unary operation將[first1,last1)範圍內的每一個元素進行op操作,並將每個op的的返回值存儲到result中
- binary operation將[first1,last1)的每一個元素和起始地址爲
first2
對應的元素,分別作爲參數1和參數2放到binary_op
中,並將每個返回值放到result中
根據官網的介紹,這個函數等效與一下循環:
template <class InputIterator, class OutputIterator, class UnaryOperator>
OutputIterator transform (InputIterator first1, InputIterator last1,
OutputIterator result, UnaryOperator op)
{
while (first1 != last1) {
*result = op(*first1); // or: *result=binary_op(*first1,*first2++);
++result; ++first1;
}
return result;
}
3.2 用法
借鑑官方的例子:
// transform algorithm example
#include <iostream> // std::cout
#include <algorithm> // std::transform
#include <vector> // std::vector
#include <functional> // std::plus
int op_increase (int i) { return ++i; }
int main () {
std::vector<int> foo;
std::vector<int> bar;
// set some values:
for (int i=1; i<6; i++)
foo.push_back (i*10); // foo: 10 20 30 40 50
bar.resize(foo.size()); // allocate space
std::transform (foo.begin(), foo.end(), bar.begin(), op_increase);
// bar: 11 21 31 41 51
// std::plus adds together its two arguments:
std::transform (foo.begin(), foo.end(), bar.begin(), foo.begin(), std::plus<int>());
// foo: 21 41 61 81 101
std::cout << "foo contains:";
for (std::vector<int>::iterator it=foo.begin(); it!=foo.end(); ++it)
std::cout << ' ' << *it;
std::cout << '\n';
return 0;
}
注意
result
可以指向輸入result
容器的size要大於等於[first1,last1)的大小,如果result
爲空時,需要使用std::back_inserter(result)
,std::back_inserter要求result實現了push_back
函數。這個時候會導致性能下降,詳情請參考我另一篇文章emplace_back VS push_back
4. std::transform、std::for_each、for的區別
for_each
返回的是函數,所以可以通過函數對象來對數據求和,比如:
class MeanValue
{
public:
MeanValue() : count_(0), sum_(0) {}
void operator() (int val)
{
sum_ += val;
++count_;
}
operator double()
{
if ( count_ <= 0 )
{
return 0;
}
return sum_ / count_;
}
private:
double sum_;
int count_;
};
//for_each returns a copy of MeanValue(), then use operator double().
// same with:
// MeanValue mv = for_each(coll2.begin(), coll2.end(), MeanValue());
// double meanValue = mv;
// for_each返回傳入MeanValue()的副本,然後調用operator double()轉換爲double.
double meanValue = for_each(coll2.begin(), coll2.end(), MeanValue());
transform
的參數要求更嚴格點,他要求操作有返回值,而for_each
忽略了操作返回值,所以沒有這個要求- C++17之後algorithm相關算法都支持並行計算,修改一個參數就行,如果是for循環,需要自己實現多線程。
- 需要注意一點,調用函數是有壓棧、出棧的性能損失的,循環地調用函數性能會受很大影響。可以將整個vertor傳入到函數中,再在函數中進行for循環,這樣可減少這樣的性能損失,這隻能通過自己實現最原始的for循環實現。
- 不併行運算的情況下,
for_each
保證執行的順序,而transform
不能保證執行的順序。 - for_each和transform都默認使用迭代器,原始for循環可以使用索引
[]
,在一些編譯器上,這兩者的效率是有很大區別的。具體可以參考這個測試:c++ - bool數組上的Raw循環比transform或for_each快5倍 - 在循環次數很大時,algorithm的一些實現就可以忽略不計,各種的效率幾乎是一樣的。
參考
喜歡我的文章的話Star一下唄Star
版權聲明:本文爲白夜行的狼原創文章,未經允許不得以任何形式轉載