C++各種循環方式梳理及對比(2)高級循環

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);

第一和第二個參數分別是迭代器的首尾地址,最後一個傳入的是函數對象。這就要求:

  1. 遍歷的對象必須是實現了迭代器的結構,比如std::vector、std::queue等。
  2. 要將處理方法封裝成函數對象,包括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的區別

  1. 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());                       
  1. transform的參數要求更嚴格點,他要求操作有返回值,而for_each忽略了操作返回值,所以沒有這個要求
  2. C++17之後algorithm相關算法都支持並行計算,修改一個參數就行,如果是for循環,需要自己實現多線程。
  3. 需要注意一點,調用函數是有壓棧、出棧的性能損失的,循環地調用函數性能會受很大影響。可以將整個vertor傳入到函數中,再在函數中進行for循環,這樣可減少這樣的性能損失,這隻能通過自己實現最原始的for循環實現。
  4. 不併行運算的情況下,for_each保證執行的順序,而transform不能保證執行的順序。
  5. for_each和transform都默認使用迭代器,原始for循環可以使用索引[],在一些編譯器上,這兩者的效率是有很大區別的。具體可以參考這個測試:c++ - bool數組上的Raw循環比transform或for_each快5倍
  6. 在循環次數很大時,algorithm的一些實現就可以忽略不計,各種的效率幾乎是一樣的。

參考


喜歡我的文章的話Star一下唄Star

版權聲明:本文爲白夜行的狼原創文章,未經允許不得以任何形式轉載

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章