日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 编程语言 > c/c++ >内容正文

c/c++

《C++ Primer 5th》笔记(10 / 19):泛型算法

發(fā)布時間:2023/12/13 c/c++ 38 豆豆
生活随笔 收集整理的這篇文章主要介紹了 《C++ Primer 5th》笔记(10 / 19):泛型算法 小編覺得挺不錯的,現在分享給大家,幫大家做個參考.

文章目錄

    • 概述
      • 算法如何工作
      • 迭代器令算法不依賴于容器
      • 但算法依賴于元素類型的操作
      • 關鍵概念:算法永遠不會執(zhí)行容器的操作
    • 初識泛型算法
      • 只讀算法
        • 算法和元素類型
        • 操作兩個序列的算法
      • 寫容器元素的算法
        • 關鍵概念:迭代器參數
        • 算法不檢查寫操作
      • 介紹back_inserter
        • 拷貝算法
      • 重排容器元素的算法
        • 消除重復單詞
        • 使用unique
        • 使用容器操作刪除元素
    • 定制操作(lambda)
      • 向算法傳遞函數
        • 謂詞
        • 排序算法
      • lambda表達式
        • 介紹lambda
        • 向lambda傳遞參數
        • 使用捕獲列表
        • 調用find_ if
        • for_each算法
        • 完整的biggies
      • lambda捕獲和返回
        • 值捕獲
        • 引用捕獲
        • 建議:盡量保持lambda的變量捕獲簡單化
        • 隱式捕獲
        • 可變lambda捕獲值
        • 指定lambda返回類型
      • 參數綁定
        • 標準庫bind函數
        • 綁定check_size的sz參數
        • 使用placeholders名字(使用命名空間using namespace)
        • bind的參數順序
        • 用bind重排參數順序(實例)
        • 綁定引用參數
        • 向后兼容:參數綁定
    • 再探迭代器
      • 插入迭代器
      • iostream迭代器
        • istream_iterator操作
        • 使用算法操作流迭代器
        • istream_iterator允許使用懶惰求值
        • ostream_iterator操作
        • 使用流迭代器處理類類型
    • 泛型算法結構
      • 5類迭代器
        • 迭代器類別
      • 算法形參模式
        • 接受單個目標迭代器的算法
        • 接受第二個輸入序列的算法
      • 算法命名規(guī)范
        • 一些算法使用重載形式傳遞一個謂詞
        • \_if版本的算法
        • 區(qū)分拷貝元素的版本和不拷貝的版本
    • 特定容器算法(鏈表)
      • splice成員
      • 鏈表特有的操作會改變容器

順序容器只定義了很少的操作:在多數情況下,我們可以

  • 添加和刪除元素、
  • 訪問首尾元素、
  • 確定容器是否為空
  • 獲得指向首元素或尾元素之后位置的迭代器。

我們可以想象用戶可能還希望做其他很多有用的操作:

  • 查找特定元素、
  • 替換或刪除一個特定值、
  • 重排元素順序

標準庫并未給每個容器都定義成員函數來實現這些操作,而是定義了一組泛型算法(generic algorithm),稱它們?yōu)椤八惴ā?#xff0c;是因為它們實現了一些經典算法的公共接口,如排序搜索;稱它們是“泛型的”,是因為它們可以用于不同類型的元素和多種容器類型(不僅包括標準庫類型,如 vector或list,還包括內置的數組類型),以及我們將看到的,還能用于其他類型的序列。

概述

大多數算法都定義在頭文件algorithm中。標準庫還在頭文件numeric中定義了一組數值泛型算法。

例一)一般情況下,這些算法并不直接操作容器,而是遍歷由兩個迭代器指定的一個元素范圍來進行操作。通常情況下,算法遍歷范圍,對其中每個元素進行一些處理。例如,假定我們有一個int的vector,希望知道vector中是否包含一個特定值。回答這個問題最方便的方法是調用標準庫算法find:

int val = 42; // value we'll look for // result will denote the element we want if it's in vec, or vec.cend() if not auto result = find(vec.cbegin(), vec.cend(), val); // report the result cout << "The value " << val<< (result == vec.cend()? " is not present" : " is present") << endl;

傳遞給find 的前兩個參數是表示元素范圍的迭代器,第三個參數是一個值。find 將范圍中每個元素與給定值進行比較。它返回指向第一個等于給定值的元素的迭代器。如果范圍中無匹配元素,則find返回第二個參數來表示搜索失敗。因此,我們可以通過比較返回值和第二個參數來判斷搜索是否成功。我們在輸出語句中執(zhí)行這個檢測,其中使用了條件運算符來報告搜索是否成功。

例二)由于find操作的是迭代器,因此我們可以用同樣的find函數在任何容器中查找值。例如,可以用find在一個string的list中查找一個給定值:

string val = "a value"; //我們要查找的值 //此調用在list中查找string元素 auto result = find(lst.cbegin(), lst.cend(), val);

例三)類似的,由于指針就像內置數組上的迭代器一樣,我們可以用find在數組中查找值:

int ia[] = {27,210,12,47,109,83 }; int val = 83; int* result = find (begin(ia), end(ia), val) ;

此例中我們使用了標準庫 begin和end函數來獲得指向ia中首元素和尾元素之后位置的指針,并傳遞給find。

例四)還可以在序列的子范圍中查找,只需將指向子范圍首元素和尾元素之后位置的迭代器
(指針)傳遞給find。例如,下面的語句在ia[1]、ia [2]和ia[3]中查找給定元素:

//在從ia[1]開始,直至(但不包含)ia[4]的范圍內查找元素 auto result = find(ia + 1, ia + 4, val);

算法如何工作

為了弄清這些算法如何用于不同類型的容器,讓我們更近地觀察一下 find。find的工作是在一個未排序的元素序列中查找一個特定元素。概念上,find應執(zhí)行如下步驟:

  • 訪問序列中的首元素。
  • 比較此元素與我們要查找的值。
  • 如果此元素與我們要查找的值匹配,find返回標識此元素的值。
  • 否則,find前進到下一個元素,重復執(zhí)行步驟2和3。
  • 如果到達序列尾,find應停止。
  • 如果 find到達序列末尾,它應該返回一個指出元素未找到的值。此值和步驟3返回的值必須具有相容的類型。
  • 這些步驟都不依賴于容器所保存的元素類型。因此,只要有一個迭代器可用來訪問元素,find就完全不依賴于容器類型(甚至無須理會保存元素的是不是容器)。

    迭代器令算法不依賴于容器

    在上述find函數流程中,除了第2步外,其他步驟都可以用迭代器操作來實現:

    • 利用迭代器解引用運算符可以實現元素訪問;
    • 如果發(fā)現匹配元素,find可以返回指向該元素的迭代器;
    • 用迭代器遞增運算符可以移動到下一個元素;
    • 尾后迭代器可以用來判斷find是否到達給定序列的末尾;
    • find可以返回尾后迭代器來表示未找到給定元素。

    但算法依賴于元素類型的操作

    雖然迭代器的使用令算法不依賴于容器類型,但大多數算法都使用了一個(或多個)元素類型上的操作。例如,在步驟2中,find用元素類型的==運算符完成每個元素與給定值的比較。其他算法可能要求元素類型支持 < 運算符。不過,我們將會看到,大多數算法提供了一種方法,允許我們使用自定義的操作來代替默認的運算符。

    關鍵概念:算法永遠不會執(zhí)行容器的操作

    泛型算法本身不會執(zhí)行容器的操作,它們只會運行于迭代器之上,執(zhí)行迭代器的操作。泛型算法運行于迭代器之上而不會執(zhí)行容器操作的特性帶來了一個令人驚訝但非常必要的編程假定:算法永遠不會改變底層容器的大小。算法可能改變容器中保存的元素的值,也可能在容器內移動元素。但永遠不會直接添加或刪除元素。

    隨后將介紹到,標準庫定義了一類特殊的迭代器,稱為插入器(inserter)。與普通迭代器只能遍歷所綁定的容器相比,插入器能做更多的事情。當給這類迭代器賦值時,它們會在底層的容器上執(zhí)行插入操作。因此,當一個算法操作一個這樣的迭代器時,迭代器可以完成向容器添加元素的效果,但算法自身永遠不會做這樣的操作

    初識泛型算法

    標準庫提供了超過100個算法。幸運的是,與容器類似,這些算法有一致的結構。比起死記硬背全部100多個算法,理解此結構可以幫助我們更容易地學習和使用這些算法。

    在本章中,我們將展示如何使用這些算法,并介紹刻畫了這些算法的統(tǒng)一原則。附錄A按操作方式列出了所有算法。

    除了少數例外,標準庫算法都對一個范圍內的元素進行操作。我們將此元素范圍稱為“輸入范圍”。接受輸入范圍的算法總是使用前兩個參數來表示此范圍,兩個參數分別是指向要處理的第一個元素和尾元素之后位置的迭代器。
    雖然大多數算法遍歷輸入范圍的方式相似,但它們使用范圍中元素的方式不同。

    理解算法的最基本的方法就是了解它們是否讀取元素、改變元素或是重排元素順序。

    只讀算法

    一些算法只會讀取其輸入范圍內的元素,而從不改變元素。find就是這樣一種算法。頭文件algorithm種的count(返回給定值在序列中出現的次數)也是這樣一種算法。

    另一個只讀算法是accumulate,它定義在頭文件numeric中。accumulate函數接受三個參數,前兩個指出了需要求和的元素的范圍,第三個參數是和的初值。假定vec是一個整數序列,則:

    //對vec中的元素求和,和的初值是0 int sum = accumulate(vec.cbegin(), vec.cend(), 0);

    這條語句將sum設置為vec中元素的和,和的初值被設置為0。

    Note:accumulate的第三個參數的類型決定了函數中使用哪個加法運算符以及返回值的類型。

    算法和元素類型

    accumulate將第三個參數作為求和起點,這蘊含著一個編程假定將元素類型加到和的類型上的操作必須是可行的。即,序列中元素的類型必須與第三個參數匹配,或者能夠轉換為第三個參數的類型。在上例中,vec中的元素可以是int,或者是double、long long或任何其他可以加到int上的類型。

    下面是另一個例子,由于 string定義了+運算符,所以我們可以通過調用accumulate來將vector中所有string元素連接起來:

    string sum = accumulate(v.cbegin(), v.cend(), string(""));

    此調用將v中每個元素連接到一個string 上,該string 初始時為空串。

    注意,我們通過第三個參數顯式地創(chuàng)建了一個string。將空串當做一個字符串字面值傳遞給第三個參數是不可以的,會導致一個編譯錯誤。

    //錯誤:const char*上沒有定義+運算符 string sum = accumulate(v.cbegin(), v.cend(), "");

    原因在于,如果我們傳遞了一個字符串字面值,用于保存和的對象的類型將是const char*。如前所述,此類型決定了使用哪個+運算符。由于 const char*并沒有+運算符,此調用將產生編譯錯誤。

    Best Practices:對于只讀取而不改變元素的算法,通常最好使用cbegin()和cend()。但是,如果你計劃使用算法返回的迭代器來改變元素的值,就需要使用begin()和end()的結果作為參數。

    操作兩個序列的算法

    另一個只讀算法是equal,用于確定兩個序列是否保存相同的值。它將第一個序列中的每個元素與第二個序列中的對應元素進行比較。如果所有對應元素都相等,則返回true,否則返回false。此算法接受三個迭代器:前兩個(與以往一樣)表示第一個序列中的元素范圍,第三個表示第二個序列的首元素:

    //roster2中的元素數目應該至少與roster1一樣多 equal(roster1.cbegin(), roster1.cend(), roster2.cbegin());

    由于equal利用迭代器完成操作,因此我們可以通過調用equal來比較兩個不同類型的容器中的元素。而且,元素類型也不必一樣,只要我們能用來比較兩個元素類型即可。例如,在此例中,roster1可以是vector<string>,而 roster2是list<const char*>。

    但是,equal基于一個非常重要的假設:它假定第二個序列至少與第一個序列一樣長。此算法要處理第一個序列中的每個元素,它假定每個元素在第二個序列中都有一個與之對應的元素。

    WARNING:那些只接受一個單一迭代器來表示第二個序列的算法,都假定第二個序列至少與第一個序列一樣長。

    寫容器元素的算法

    一些算法將新值賦予序列中的元素。當我們使用這類算法時,必須注意確保序列原大小至少不小于我們要求算法寫入的元素數目。記住,算法不會執(zhí)行容器操作,因此它們自身不可能改變容器的大小。

    一些算法會自己向輸入范圍寫入元素。這些算法本質上并不危險,它們最多寫入與給定序列一樣多的元素。

    例如,算法fill接受一對迭代器表示一個范圍,還接受一個值作為第三個參數。fill將給定的這個值賦予輸入序列中的每個元素。

    fill(vec.begin(), vec.end(), 0); //將每個元素重置為0 //將容器的一個子序列設置為10 fill(vec.begin(), vec.begin() + vec.size() / 2, 10);

    由于fill向給定輸入序列中寫入數據,因此,只要我們傳遞了一個有效的輸入序列,寫入操作就是安全的。

    關鍵概念:迭代器參數

    一些算法從兩個序列中讀取元素。

    • 構成這兩個序列的元素可以來自于不同類型的容器。
      • 例如,第一個序列可能保存于一個 vector中,而第二個序列可能保存于一個list、deque、內置數組或其他容器中。
    • 而且,兩個序列中元素的類型也不要求嚴格匹配。算法要求的只是能夠比較兩個序列中的元素。
      • 例如,對equal 算法,元素類型不要求相同,但是我們必須能使用一來比較來自兩個序列中的元素。

    操作兩個序列的算法之間的區(qū)別在于我們如何傳遞第二個序列。一些算法,例如equal,接受三個迭代器:前兩個表示第一個序列的范圍,第三個表示第二個序列中的首元素。其他算法接受四個迭代器:前兩個表示第一個序列的元素范圍,后兩個表示第二個序列的范圍。

    用一個單一迭代器表示第二個序列的算法都假定第二個序列至少與第一個一樣長。確保算法不會試圖訪問第二個序列中不存在的元素是程序員的責任。例如,算法 equal會將其第一個序列中的每個元素與第二個序列中的對應元素進行比較。

    如果第二個序列是第一個序列的一個子集,則程序會產生一個嚴重錯誤——equal會試圖訪問第二個序列中末尾之后(不存在)的元素。

    算法不檢查寫操作

    一些算法接受一個迭代器來指出一個單獨的目的位置。這些算法將新值賦予一個序列中的元素,該序列從目的位置迭代器指向的元素開始。

    例如,函數fill_n接受一個單迭代器、一個計數值和一個值。它將給定值賦予迭代器指向的元素開始的指定個元素。我們可以用fill_n將一個新值賦予vector中的元素:

    vector<int> vec;//空vector //使用vec,賦子它不同值 fill_n(vec.begin(), vec.size(), 0);//將所有元素重置為0

    函數fill_n假定寫入指定個元素是安全的。即,如下形式的調用

    fill_n(dest, n, val)

    fill_n假定dest指向一個元素,而從dest開始的序列至少包含n個元素。

    一個初學者非常容易犯的錯誤是在一個空容器上調用fill_n(或類似的寫元素的算法):

    vector<int> vec; //空向量 //災難:修改vec中的10個(不存在)元素 fill_n(vec.begin(), 10, 0);

    這個調用是一場災難。我們指定了要寫入10個元素,但vec中并沒有元素——它是空的。這條語句的結果是未定義的。

    Note:向目的位置迭代器寫入數據的算法假定目的位置足夠大,能容納要寫入的元素。(MyNote:量體裁衣。)

    介紹back_inserter

    一種保證算法有足夠元素空間來容納輸出數據的方法是使用插入迭代器(insert iterator)。插入迭代器是一種向容器中添加元素的迭代器。

    通常情況,當我們通過一個迭代器向容器元素賦值時,值被賦予迭代器指向的元素。而當我們通過一個插入迭代器賦值時,一個與賦值號右側值相等的元素被添加到容器中。

    隨后將詳細介紹插入迭代器的內容。但是,為了展示如何用算法向容器寫入數據,我們現在將使用back_inserter,它是定義在頭文件iterator中的一個函數。

    back_inserter接受一個指向容器的引用,返回一個與該容器綁定的插入迭代器。當我們通過此迭代器賦值時,賦值運算符會調用push_back將一個具有給定值的元素添加到容器中:

    vector<int> vec; //空向量 auto it = back_inserter(vec); //通過它賦值會將元素添加到vec中 *it = 42; //vec中現在有一個元素,值為42

    我們常常使用back_inserter來創(chuàng)建一個迭代器,作為算法的目的位置來使用。例如:

    vector<int> vec; //空向量 //正確: back_inserter創(chuàng)建一個插入迭代器,可用來向vec添加元素 fill_n(back_inserter(vec), 10, 0);//添加10個元素到vec

    在每步迭代中,fill_n向給定序列的一個元素賦值。由于我們傳遞的參數是back_inserter返回的迭代器,因此每次賦值都會在vec上調用push_back。最終,這條fill_n調用語句向vec的末尾添加了10個元素,每個元素的值都是0。

    拷貝算法

    拷貝(copy)算法是另一個向目的位置迭代器指向的輸出序列中的元素寫入數據的算法。

    此算法接受三個迭代器,前兩個表示一個輸入范圍,第三個表示目的序列的起始位置。

    此算法將輸入范圍中的元素拷貝到目的序列中。傳遞給copy的目的序列至少要包含與輸入序列一樣多的元素,這一點很重要。

    我們可以用copy 實現內置數組的拷貝,如下面代碼所示:

    int a1[] = { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 }; int a2[sizeof(a1) / sizeof(*a1)];//a2與a1大小一樣 //ret指向拷貝到a2的尾元素之后的位置 auto ret = copy(begin(a1), end(a1), a2); //把a1的內容拷貝給a2

    此例中我們定義了一個名為a2的數組,并使用sizeof確保a2與數組a1包含同樣多的元素。接下來我們調用copy 完成從 a1到a2的拷貝。在調用copy后,兩個數組中的元素具有相同的值。

    copy返回的是其目的位置迭代器(遞增后)的值。即,ret恰好指向拷貝到a2的尾元素之后的位置。

    多個算法都提供所謂的“拷貝”版本。這些算法計算新元素的值,但不會將它們放置在輸入序列的末尾,而是創(chuàng)建一個新序列保存這些結果。

    例如,replace算法讀入一個序列,并將其中所有等于給定值的元素都改為另一個值。此算法接受4個參數:前兩個是迭代器,表示輸入序列,后兩個一個是要搜索的值,另一個是新值。它將所有等于第一個值的元素替換為第二個值:

    //將所有值為0的元素改為42 replace(ilst.begin(), ilst.end(), 0, 42);

    此調用將序列中所有的0都替換為42。

    如果我們希望保留原序列不變,可以調用replace_copy。此算法接受額外第三個迭代器參數,指出調整后序列的保存位置:

    //使用back_inserter按需要增長目標序列 replace_copy(ilst.cbegin(), ilst.cend(),back_inserter(ivec), 0, 42);

    此調用后,ilst并未改變,ivec包含ilst的一份拷貝,不過原來在ilst中值為0的元素在ivec中都變?yōu)?2。

    重排容器元素的算法

    某些算法會重排容器中元素的順序,一個明顯的例子是sort。調用sort會重排輸入序列中的元素,使之有序,它是利用元素類型的<運算符來實現排序的。

    例如,假定我們想分析一系列兒童故事中所用的詞匯。假定已有一個vector,保存了多個故事的文本。我們希望化簡這個vector,使得每個單詞只出現一次,而不管單詞在任意給定文檔中到底出現了多少次。

    為了便于說明問題,我們將使用下面簡單的故事作為輸入:

    the quick red fox jumps over the slow red turtle

    給定此輸入,我們的程序應該生成如下vector:

    foxjumpsoverquickredslowtheturtle

    消除重復單詞

    為了消除重復單詞,首先將vector排序,使得重復的單詞都相鄰出現。一旦vector排序完畢,我們就可以使用另一個稱為unique的標準庫算法來重排vector,使得不重復的元素出現在vector的開始部分。由于算法不能執(zhí)行容器的操作,我們將使用vector的erase成員來完成真正的刪除操作:

    void elimDups (vector<string> &words){//按字典序排序words,以便查找重復單詞sort(words.begin(), words.end());// unique重排輸入范圍,使得每個單詞只出現一次//排列在范圍的前部,返回指向不重復區(qū)域之后一個位置的迭代器auto end_unique = unique(words.begin(), words.end());//使用向量操作erase刪除重復單詞words.erase(end_unique, words.end()); }

    sort算法接受兩個迭代器,表示要排序的元素范圍。在此例中,我們排序整個vector。完成sort后,words的順序如下所示:

    foxjumpsoverquickredredslowthetheturtle

    注意,單詞red和 the各出現了兩次。

    使用unique

    words排序完畢后,我們希望將每個單詞都只保存一次。unique算法重排輸入序列,將相鄰的重復項“消除”,并返回一個指向不重復值范圍末尾的迭代器。調用unique后,vector將變?yōu)?#xff1a;

    foxjumpsoverquickredslowtheturtle??????

    end_unique
    (最后一個不重復元素之后的位置)

    words的大小并未改變,它仍有10個元素。但這些元素的順序被改變了——相鄰的重復元素被“刪除”了。我們將刪除打引號是因為unique并不真的刪除任何元素,它只是覆蓋相鄰的重復元素,使得不重復元素出現在序列開始部分。unique返回的迭代器指向最后一個不重復元素之后的位置。此位置之后的元素仍然存在,但我們不知道它們的值是什么。

    Note:標準庫算法對迭代器而不是容器進行操作。因此,算法不能(直接)添加或刪除元素。

    使用容器操作刪除元素

    為了真正地刪除無用元素,我們必須使用容器操作,本例中使用erase。我們刪除從end_unique開始直至words末尾的范圍內的所有元素。這個調用之后,words包含來自輸入的8個不重復的單詞。

    值得注意的是,即使words中沒有重復單詞,這樣調用erase也是安全的。在此情況下,unique會返回words.end()。因此,傳遞給erase的兩個參數具有相同的值:words.end()。迭代器相等意味著傳遞給erase的元素范圍為空。刪除一個空范圍沒有什么不良后果,因此程序即使在輸入中無重復元素的情況下也是正確的。

    定制操作(lambda)

    很多算法都會比較輸入序列中的元素。默認情況下,這類算法使用元素類型的<或==運算符完成比較。標準庫還為這些算法定義了額外的版本,允許我們提供自己定義的操作來代替默認運算符。

    例如,sort算法默認使用元素類型的 < 運算符。但可能我們希望的排序順序與<所定義的順序不同,或是我們的序列可能保存的是未定義 < 運算符的元素類型(如Sales_data)。在這兩種情況下,都需要重載sort的默認行為。

    向算法傳遞函數

    作為一個例子,假定希望在調用elimDups(在上一章)后打印vector的內容。此外還假定希望單詞按其長度排序,大小相同的再按字典序排列。為了按長度重排vector,我們將使用sort的第二個版本,此版本是重載過的,它接受第三個參數,此參數是一個謂詞(predicate)。

    謂詞

    謂詞是一個可調用的表達式,其返回結果是一個能用作條件的值。標準庫算法所使用的謂詞分為兩類:

  • 一元謂詞(unary predicate,意味著它們只接受單一參數)
  • 二元謂詞(binary predicate,意味著它們有兩個參數)
  • 接受謂詞參數的算法對輸入序列中的元素調用謂詞。因此,元素類型必須能轉換為謂詞的參數類型。

    接受一個二元謂詞參數的sort版本用這個謂詞代替 < 來比較元素。當前,我們只需知道,此操作必須在輸入序列中所有可能的元素值上定義一個一致的序。在第6章中定義的 isShorter就是一個滿足這些要求的函數,因此可以將isShorter傳遞給sort。這樣做會將元素按大小重新排序:

    //比較函數,用來按長度排序單詞 bool isShorter(const string &s1, const string &s2){return s1.size() < s2.size(); } //按長度由短至長排序words sort(words.begin() ,words.end(), isShorter);

    調用sort會將words重排,使得所有長度為3的單詞排在長度為4的單詞之前,然后是長度為5的單詞,依此類推。

    排序算法

    在我們將words按長度大小重排的同時,還希望具有相同長度的元素按字典序排列。為了保持相同長度的單詞按字典序排列,可以使用stable_sort算法。這種穩(wěn)定排序算法維持相等元素的原有順序。

    通常情況下,我們不關心有序序列中相等元素的相對順序,它們畢竟是相等的。但是,在本例中,我們定義的“相等”關系表示“具有相同長度”。而具有相同長度的元素,如果看其內容,其實還是各不相同的。通過調用stable_sort,可以保持等長元素間的字典序:

    elimDups(words); //將words按字典序重排,并消除重復單詞 //按長度重新排序,長度相同的單詞維持字典序 stable_sort(words.begin(), words.end() ,isShorter); for(const auto &s : words) //無須拷貝字符串cout << s << "" ; //打印每個元素,以空格分隔 cout << endl;

    假定在此調用前words是按字典序排列的,則調用之后,words會按元素大小排序,而長度相同的單詞會保持字典序。如果我們對原來的vector內容運行這段代碼,輸出為:

    fox red the over slow jumps quick turtle

    lambda表達式

    根據算法接受一元謂詞還是二元謂詞,我們傳遞給算法的謂詞必須嚴格接受一個或兩個參數。但是,有時我們希望進行的操作需要更多參數,超出了算法對謂詞的限制。

    一個相關的例子是,我們將修改上一節(jié)的程序,求大于等于一個給定長度的單詞有多少。我們還會修改輸出,使程序只打印大于等于給定長度的單詞。(設計目標)

    我們將此函數命名為biggies,其框架如下所示:

    void biggies(vector<string> &words, vector<string>::size_type sz) {elimDups(words); // put words in alphabetical order and remove duplicates// resort by length, maintaining alphabetical order among words of the same lengthstable_sort(words.begin(), words.end(), isShorter);// get an iterator to the first element whose size() is >= sz// compute the number of elements with size >= sz// print words of the given size or longer, each one followed by a space }

    我們的新問題是在vector中尋找第一個大于等于給定長度的元素。一旦找到了這個元素,根據其位置,就可以計算出有多少元素的長度大于等于給定值。

    我們可以使用標準庫 find_if 算法來查找第一個具有特定大小的元素。類似find,find_if算法接受一對迭代器,表示一個范圍。但與find不同的是,find_if的第三個參數是一個謂詞。find_if 算法對輸入序列中的每個元素調用給定的這個謂詞。它返回第一個使謂詞返回非О值的元素,如果不存在這樣的元素,則返回尾迭代器。

    編寫一個函數,令其接受一個string 和一個長度,并返回一個bool值表示該string的長度是否大于給定長度,是一件很容易的事情。但是,find_if接受一元謂詞——我們傳遞給 find_if的任何函數都必須嚴格接受一個參數,以便能用來自輸入序列的一個元素調用它。沒有任何辦法能傳遞給它第二個參數來表示長度。為了解決此問題,需要使用另外一些語言特性。

    介紹lambda

    我們可以向一個算法傳遞任何類別的可調用對象(callable object)。對于一個對象或一個表達式,如果可以對其使用調用運算符,則稱它為可調用的。即,如果e是一個可調用的表達式,則我們可以編寫代碼e(args),其中args是一個逗號分隔的一個或多個參數的列表。

    到目前為止,我們使用過的僅有的兩種可調用對象是

  • 函數
  • 函數指針。
  • 還有其他兩種可調用對象:

  • 重載了函數調用運算符的類(第14章介紹)
  • lambda表達式(lambda expression)。
  • 一個lambda表達式表示一個可調用的代碼單元。我們可以將其理解為一個未命名的內聯(lián)函數。與任何函數類似,一個lambda具有一個返回類型、一個參數列表和一個函數體。但與函數不同,lambda可能定義在函數內部。一個lambda表達式具有如下形式

    [capture list](parameter list) -> return type { function body }

    其中,

  • capture list(捕獲列表)是一個lambda所在函數中定義的局部變量的列表(通常為空);
  • return type、parameter list和 function body與任何普通函數一樣,分別表示返回類型、參數列表和函數體。
  • 但是,與普通函數不同,lambda必須使用尾置返回來指定返回類型。

    尾置返回類型( trailing return type)。任何函數的定義都能使用尾置返回,但是這種形式對于返回類型比較復雜的函數最有效,比如返回類型是數組的指針或者數組的引用。尾置返回類型跟在形參列表后面并以一個->符號開頭。為了表示函數真正的返回類型跟在形參列表之后,我們在本應該出現返回類型的地方放置一個auto:

    //func接受一個int類型的實參,返回一個指針,該指針指向含有10個整數的數組 auto func(int i) -> int (*)[10];

    來自第6章

    我們可以忽略參數列表和返回類型,但必須永遠包含捕獲列表和函數體

    auto f = []{ return 42;};

    此例中,我們定義了一個可調用對象f,它不接受參數,返回42。

    lambda 的調用方式與普通函數的調用方式相同,都是使用調用運算符:

    cout << f() << endl;//打印42

    在lambda中忽略括號和參數列表等價于指定一個空參數列表。在此例中,當調用f時,參數列表是空的。如果忽略返回類型,lambda根據函數體中的代碼推斷出返回類型。

    如果函數體只是一個return語句,則返回類型從返回的表達式的類型推斷而來。否則,返回類型為void。

    Note:如果 lambda 的函數體包含任何單一 return語句之外的內容,且未指定返回類型,則返回void。

    向lambda傳遞參數

    與一個普通函數調用類似,調用一個lambda時給定的實參被用來初始化lambda的形參。通常,實參和形參的類型必須匹配。但與普通函數不同,lambda不能有默認參數。因此,一個lambda調用的實參數目永遠與形參數目相等。一旦形參初始化完畢,就可以執(zhí)行函數體了。

    作為一個帶參數的 lambda 的例子,我們可以編寫一個與 isshorter 函數完成相同功能的 lambda:

    [](const string &a, const string &b){return a.size() < b.size();}

    空捕獲列表表明此lambda不使用它所在函數中的任何局部變量。lambda 的參數與isShorter 的參數類似,是const string 的引用。lambda 的函數體也與isShorter類似,比較其兩個參數的size ( ),并根據兩者的相對大小返回一個布爾值。

    使用此 lambda來調用stable_sort的示例:

    //按長度排序,長度相同的單詞維持字典序 stable_sort(words.begin(), words.end(),[](const string &a, const string &b){return a.size() < b.size();});

    當stable_sort需要比較兩個元素時,它就會調用給定的這個lambda表達式。

    使用捕獲列表

    我們現在已經準備好解決原來的問題了——編寫一個可以傳遞給find_if的可調用表達式。我們希望這個表達式能將輸入序列中每個string 的長度與biggies函數中的sz參數的值進行比較。

    雖然一個lambda可以出現在一個函數中,使用其局部變量,但它只能使用那些明確指明的變量。一個lambda通過將局部變量包含在其捕獲列表中來指出將會使用這些變量。捕獲列表指引lambda在其內部包含訪問局部變量所需的信息。

    在本例中,我們的 lambda會捕獲sz,并只有單一的 string 參數。其函數體會將string的大小與捕獲的sz的值進行比較:

    [sz](const string &a){return a.size() >= sz;};

    lambda以一對[]開始,我們可以在其中提供一個以逗號分隔的名字列表,這些名字都是它所在函數中定義的。

    由于此lambda捕獲sz,因此lambda的函數體可以使用sz。lambda不捕獲words,因此不能訪問此變量。如果我們給lambda提供一個空捕獲列表,則代碼會編譯錯誤:

    //錯誤:sz未捕獲 [](const string &a){return a.size() >= sz;};

    Note:一個lambda只有在其捕獲列表中捕獲一個它所在函數中的局部變量,才能在函數體中使用該變量。

    調用find_ if

    使用此lambda,我們就可以查找第一個長度大于等于sz的元素:

    //獲取一個迭代器,指向第一個滿足size() >= sz的元素 auto wc = find_if(words.begin(), words.end(),[sz](const string &a){return a.size() >= sz; });

    這里對find_ if 的調用返回一個迭代器,指向第一個長度不小于給定參數sz的元素。如果這樣的元素不存在,則返回words.end()的一個拷貝。

    我們可以使用find_ if返回的迭代器來計算從它開始到words的末尾一共有多少個元素:

    //計算滿足size >= sz的元素的數目 auto count = words.end() - wc; cout << count << " " << make_plural(count, "word", "s")<< "of length" << sz <<or longer" << endl;

    我們的輸出語句調用make_ plural(第6章內容)來輸出"word"或"words",具體輸出哪個取決于大小是否等于1。

    for_each算法

    問題的最后一部分 是打印words中長度大于等于sz的元素。為了達到這一目的, 我們可以使用for_each算法。此算法接受一個可調用對象,并對輸入序列中每個元素調用此對象:

    //打印長度大于等于給定值的單詞,每個單詞后面接一個空格 for_each(wc, words.end(),[](const string &s) {cout << s << "";}); cout << endl;

    此lambda中的捕獲列表為空,但其函數體中還是使用了兩個名字: s和cout,前者是它自己的參數。

    捕獲列表為空,是因為我們只對lambda所在函數中定義的(非static)變量使用捕獲列表。一個lambda可以直接使用定義在當前函數之外的名字。在本例中,cout不是定義在biggies中的局部名字,而是定義在頭文件iostream中。因此,只要在biggies出現的作用域中包含了頭文件iostream,我們的lambda就可以使用cout。

    Note:捕獲列表只用于局部非static變量,lambda可以直接使用局部static變量和在它所在函數之外聲明的名字。

    完整的biggies

    //求大于等于一個給定長度的單詞有多少。我們還會修改輸出,使程序只打印大于等于給定長度的單詞 void biggies(vector<string> &words,vector<string>::size_type sz) {elimDups(words); // put words in alphabetical order and remove duplicates// sort words by size, but maintain alphabetical order for words of the same sizestable_sort(words.begin(), words.end(),[](const string &a, const string &b){ return a.size() < b.size();});// get an iterator to the first element whose size() is >= szauto wc = find_if(words.begin(), words.end(),[sz](const string &a){ return a.size() >= sz; });// compute the number of elements with size >= szauto count = words.end() - wc;cout << count << " " << make_plural(count, "word", "s")<< " of length " << sz << " or longer" << endl;// print words of the given size or longer, each one followed by a spacefor_each(wc, words.end(),[](const string &s){cout << s << " ";});cout << endl; }

    lambda捕獲和返回

    當定義一個lambda時,編譯器生成一個與lambda對應的新的(未命名的)類類型。我們將在第14章介紹這種類是如何生成的。

    目前,可以這樣理解,當向一個函數傳遞一個lambda 時,同時定義了一個新類型和該類型的一個對象:傳遞的參數就是此編譯器生成的類類型的未命名對象。類似的,當使用auto定義一個用lambda初始化的變量時,定義了一個從lambda生成的類型的對象。

    默認情況下,從lambda生成的類都包含一個對應該lambda所捕獲的變量的數據成員。類似任何普通類的數據成員,lambda的數據成員也在 lambda對象創(chuàng)建時被初始化。

    值捕獲

    類似參數傳遞,變量的捕獲方式也可以是值或引用。下列出了幾種不同的構造捕獲列表的方式。

    操作說明
    []空捕獲列表。lambda不能使用所在函數中的變量。一個lambda只有捕獲變量后才能使用它們
    [names]names是一個逗號分隔的名字列表,這些名字都是lambda所在函數的局部變量。默認情況下,捕獲列表中的變量都被拷貝。名字前如果使用了&,則采用引用捕獲方式
    [&]隱式捕獲列表,采用引用捕獲方式。lambda體中所使用的來自所在函數的實體都采用引用方式使用
    [=]隱式捕獲列表,采用值捕獲方式。lambda體將拷貝所使用的來自所在函數的實體的值
    [&, identifier_list]identifier_list是一個逗號分隔的列表,包含0個或多個來自所在函數的變量。這些變量采用值捕獲方式,而任何隱式捕獲的變量都采用引用方式捕獲。identifier_list中的名字前面不能使用&
    [=, identifier_list]identifier_list中的變量都采用引用方式捕獲,而任何隱式捕獲的變量都采用值方式捕獲。identifier_list中的名字不能包括this,且這些名字之前必須使用&

    到目前為止,我們的 lambda采用值捕獲的方式。與傳值參數類似,采用值捕獲的前提是變量可以拷貝。與參數不同,被捕獲的變量的值是在lambda創(chuàng)建時拷貝,而不是調用時拷貝:

    void fcn1(){size_t v1 = 42; //局部變量//將v1拷貝到名為f的可調用對象auto f = [v1] {return v1;};v1 = 0 ;auto j = f(); // j為42;f保存了我們創(chuàng)建它時v1的拷貝 }

    由于被捕獲變量的值是在 lambda創(chuàng)建時拷貝,因此隨后對其修改不會影響到lambda內對應的值。

    引用捕獲

    void fcn2() {size_t v1 = 42; // local variable// the object f2 contains a reference to v1auto f2 = [&v1] { return v1; };v1 = 0;auto j = f2(); // j is 0; f2 refers to v1; it doesn't store it }

    v1之前的&指出v1應該以引用方式捕獲。一個以引用方式捕獲的變量與其他任何類型的引用的行為類似。當我們在lambda函數體內使用此變量時,實際上使用的是引用所綁定的對象。在本例中,當lambda返回v1時,它返回的是v1指向的對象的值。

    引用捕獲與返回引用有著相同的問題和限制。如果我們采用引用方式捕獲一個變量,就必須確保被引用的對象在lambda執(zhí)行的時候是存在的。lambda捕獲的都是局部變量,這些變量在函數結束后就不復存在了。如果 lambda可能在函數結束后執(zhí)行,捕獲的引用指向的局部變量已經消失。

    引用捕獲有時是必要的。例如,我們可能希望biggies函數接受一個ostream 的引用,用來輸出數據,并接受一個字符作為分隔符:

    void biggies(vector<string> &words,vector<string>::size_type sz,ostream &os = cout, char c = ' ') {// code to reorder words as before// statement to print count revised to print to osfor_each(words.begin(), words.end(),[&os, c](const string &s) { os << s << c; }); }

    我們不能拷貝ostream對象,因此捕獲os 的唯一方法就是捕獲其引用(或指向os的指針)。

    當我們向一個函數傳遞一個lambda 時,就像本例中調用for_each那樣,lambda會立即執(zhí)行。在此情況下,以引用方式捕獲 os 沒有問題,因為當for_each 執(zhí)行時,biggies 中的變量是存在的。

    我們也可以從一個函數返回lambda。函數可以直接返回一個可調用對象,或者返回一個類對象,該類含有可調用對象的數據成員。如果函數返回一個lambda,則與函數不能返回一個局部變量的引用類似,此lambda也不能包含引用捕獲。

    WARNING:當以引用方式捕獲一個變量時,必須保證在lambda執(zhí)行時變量是存在的。

    建議:盡量保持lambda的變量捕獲簡單化

    一個 lambda捕獲從lambda被創(chuàng)建(即,定義 lambda的代碼執(zhí)行時)到 lambda自身執(zhí)行(可能有多次執(zhí)行)這段時間內保存的相關信息。確保lambda每次執(zhí)行的時候這些信息都有預期的意義,是程序員的責任。

    捕獲一個普通變量,如int、string或其他非指針類型,通??梢圆捎煤唵蔚?strong>值捕獲方式。在此情況下,只需關注變量在捕獲時是否有我們所需的值就可以了。

    如果我們捕獲一個指針或迭代器,或采用引用捕獲方式,就必須確保在lambda執(zhí)行時,綁定到迭代器、指針或引用的對象仍然存在。而且,需要保證對象具有預期的值。在lambda從創(chuàng)建到它執(zhí)行的這段時間內,可能有代碼改變綁定的對象的值。也就是說,在指針(或引用)被捕獲的時刻,綁定的對象的值是我們所期望的,但在 lambda執(zhí)行時,該對象的值可能已經完全不同了。

    一般來說,我們應該盡量減少捕獲的數據量,來避免潛在的捕獲導致的問題。而且,如果可能的話,應該避免捕獲指針或引用。

    隱式捕獲

    除了顯式列出我們希望使用的來自所在函數的變量之外,還可以讓編譯器根據lambda體中的代碼來推斷我們要使用哪些變量。為了指示編譯器推斷捕獲列表,應在捕獲列表中寫一個&或=。&告訴編譯器采用捕獲引用方式,=則表示采用值捕獲方式。例如,我們可以重寫傳遞給find_if的lambda:

    // sz implicitly captured by value wc = find_if(words.begin(), words.end(),[=](const string &s){ return s.size() >= sz; });

    如果我們希望對一部分變量采用值捕獲,對其他變量采用引用捕獲,可以混合使用隱式捕獲和顯式捕獲:

    void biggies(vector<string> &words,vector<string>::size_type sz,ostream &os = cout, char c = ' ') {// other processing as before// os implicitly captured by reference; c explicitly captured by valuefor_each(words.begin(), words.end(),[&, c](const string &s) { os << s << c; });// os explicitly captured by reference; c implicitly captured by valuefor_each(words.begin(), words.end(),[=, &os](const string &s) { os << s << c; }); }

    當我們混合使用隱式捕獲和顯式捕獲時,捕獲列表中的第一個元素必須是一個&或=。此符號指定了默認捕獲方式為引用或值。

    當混合使用隱式捕獲和顯式捕獲時,顯式

    捕獲的變量必須使用與隱式捕獲不同的方式。即,如果隱式捕獲是引用方式(使用了&),則顯式捕獲命名變量必須采用值方式,因此不能在其名字前使用&。類似的,如果隱式捕獲采用的是值方式(使用了=),則顯式捕獲命名變量必須采用引用方式,即,在名字前使用&。

    可變lambda捕獲值

    默認情況下,對于一個值被拷貝的變量,lambda不會改變其值。如果我們希望能改變一個被捕獲的變量的值,就必須在參數列表首加上關鍵字mutable。因此,可變lambda能省略參數列表:

    void fcn3() {size_t v1 = 42; // local variable// f can change the value of the variables it capturesauto f = [v1] () mutable { return ++v1; };v1 = 0;auto j = f(); // j is 43 }

    mutable
    英 [?mju?t?bl] 美 [?mju?t?bl]
    adj. 可變的;會變的

    一個引用捕獲的變量是否(如往常一樣)可以修改依賴于此引用指向的是一個const類型還是一個非const類型:

    void fcn4() {size_t v1 = 42; // local variable// v1 is a reference to a non const variable// we can change that variable through the reference inside f2auto f2 = [&v1] { return ++v1; };v1 = 0;auto j = f2(); // j is 1 }

    指定lambda返回類型

    到目前為止,我們所編寫的lambda都只包含單一的return語句。因此,我們還未遇到必須指定返回類型的情況。默認情況下,如果一個lambda體包含return之外的任何語句,則編譯器假定此lambda返回void。與其他返回void的函數類似,被推斷返回void的lambda不能返回值。

    下面給出了一個簡單的例子,我們可以使用標準庫transform算法和一個lambda來將一個序列中的每個負數替換為其絕對值:

    transform (vi.begin() ,vi.end() ,vi.begin(),[](int i){ return i < 0 ? -i : i;});

    函數transform接受三個迭代器和一個可調用對象。前兩個迭代器表示輸入序列,第三個迭代器表示目的位置。

    算法對輸入序列中每個元素調用可調用對象,并將結果寫到目的位置。如本例所示,目的位置迭代器與表示輸入序列開始位置的迭代器可以是相同的。當輸入迭代器和目的迭代器相同時,transform 將輸入序列中每個元素替換為可調用對象操作該元素得到的結果。

    在本例中,我們傳遞給transform 一個lambda,它返回其參數的絕對值。lambda體是單一的return語句,返回一個條件表達式的結果。我們無須指定返回類型,因為可以根據條件運算符的類型推斷出來。

    但是,如果我們將程序改寫為看起來是等價的if語句,就會產生編譯錯誤

    //錯誤:不能推斷l(xiāng)ambda的返回類型 transform (vi.begin(), vi.end(), vi.begin(),[](int i){ if (i< 0) return -i; else return i; ));

    編譯器推斷這個版本的lambda返回類型為void,但它返回了一個int值。

    當我們需要為一個lambda定義返回類型時,必須使用尾置返回類型(第6章內容):

    transform (vi.begin() ,vi.end() ,vi.begin() ,[](int i) -> int{ if (i < 0) return -i; else return i; });

    在此例中,傳遞給transform的第四個參數是一個lambda,它的捕獲列表是空的,接受單一int參數,返回一個int值。它的函數體是一個返回其參數的絕對值的if語句。

    尾置返回類型( trailing return type)。任何函數的定義都能使用尾置返回,但是這種形式對于返回類型比較復雜的函數最有效,比如返回類型是數組的指針或者數組的引用。尾置返回類型跟在形參列表后面并以一個->符號開頭。為了表示函數真正的返回類型跟在形參列表之后,我們在本應該出現返回類型的地方放置一個auto:

    //func接受一個int類型的實參,返回一個指針,該指針指向含有10個整數的數組 auto func(int i) -> int (*)[10];

    來自第6章

    參數綁定

    對于那種只在一兩個地方使用的簡單操作,lambda表達式是最有用的。如果我們需要在很多地方使用相同的操作,通常應該定義一個函數,而不是多次編寫相同的 lambda表達式。類似的,如果一個操作需要很多語句才能完成,通常使用函數更好。

    如果 lambda的捕獲列表為空,通??梢杂煤瘮祦泶嫠?。如前面章節(jié)所示,既可以用一個lambda,也可以用函數isShorter來實現將vector中的單詞按長度排序。類似的,對于打印 vector內容的lambda,編寫一個函數來替換它也是很容易的事情,這個函數只需接受一個string并在標準輸出上打印它即可。

    (MyNote:談論使用lambda、函數的時機。)

    但是,對于捕獲局部變量的lambda,用函數來替換它就不是那么容易了。例如,我們用在find_if調用中的 lambda比較一個string和一個給定大小。我們可以很容易地編寫一個完成同樣工作的函數:

    bool check_size(const string &s, string::size_type sz){return s.size() >= sz; }

    但是,我們不能用這個函數作為find_if的一個參數。如前文所示,find_if接受一個一元謂詞,因此傳遞給find_if 的可調用對象必須接受單一參數。biggies傳遞給find_if的lambda使用捕獲列表來保存sz。為了用check_size來代替此lambda必須解決如何向sz形參傳遞一個參數的問題

    標準庫bind函數

    我們可以解決向 check_size傳遞一個長度參數的問題,方法是使用一個新的名為bind的標準庫函數,它定義在頭文件functional中。可以將bind函數看作一個通用的函數適配器,它接受一個可調用對象,生成一個新的可調用對象來“適應”原對象的參數列表。

    調用bind 的一般形式為:

    auto newCallable = bind(callable, arg_list);

    其中,newCallable本身是一個可調用對象,arg_list 是一個逗號分隔的參數列表,對應給定的callable的參數。即,當我們調用newCallable時,newCallable會調用callable,并傳遞給它arg_list中的參數。

    arg_list中的參數可能包含形如_n的名字,其中n是一個整數。這些參數是“占位符”,
    表示newCallable的參數,它們占據了傳遞給newCallable的參數的“位置”。數值n表示生成的可調用對象中參數的位置:_1為newCallable的第一個參數,_2為第二個參數,依此類推。

    綁定check_size的sz參數

    作為一個簡單的例子,我們將使用bind生成一個調用check_size的對象,如下所示,它用一個定值作為其大小參數來調用check_size:

    //check6是一個可調用對象,接受一個string類型的參數 //并用此 string和值6來調用check_size auto check6 = bind(check_size, _1, 6);

    此bind調用只有一個占位符,表示 check6只接受單一參數。占位符出現在 arg_list 的第一個位置,表示 check6 的此參數對應 check_size 的第一個參數。此參數是一個const string&。因此,調用check6必須傳遞給它一個string類型的參數,check6會將此參數傳遞給check_size。

    string s = "hello" ; bool b1 = check6(s); //check6(s)會調用check_size(s,6)

    使用bind,我們可以將原來基于lambda的find_if調用:

    auto wc = find_if(words.begin(), words.end(),[sz](const string &a)

    替換為如下使用check_size的版本:

    auto wc = find_if(words.begin(), words.end(),bind(check_size, _1, sz));

    此bind調用生成一個可調用對象,將check_size的第二個參數綁定到sz的值。當find_if對words中的string調用這個對象時,這些對象會調用check_size,將給定的string和sz傳遞給它。因此,find_if可以有效地對輸入序列中每個string調用check_size,實現string的大小與sz的比較。

    使用placeholders名字(使用命名空間using namespace)

    名字_n都定義在一個名為placeholders的命名空間中,而這個命名空間本身定義在std命名空間中。為了使用這些名字,兩個命名空間都要寫上。與我們的其他例子類似,對bind的調用代碼假定之前已經恰當地使用了using聲明。例如,_1對應的using聲明為:

    using std::placeholders::_1;

    此聲明說明我們要使用的名字_1定義在命名空間placeholders 中,而此命名空間又定義在命名空間std中。

    對每個占位符名字,我們都必須提供一個單獨的using聲明。編寫這樣的聲明很煩人,也很容易出錯。可以使用另外一種不同形式的using 語句(詳細內容將在第18章中介紹),而不是分別聲明每個占位符,如下所示:

    using namespace namespace_name;

    這種形式說明希望所有來自namespace_name的名字都可以在我們的程序中直接使用。例如:

    using namespace std::placeholders;

    使得由placeholders定義的所有名字都可用。與 bind函數一樣,placeholders命名空間也定義在functional頭文件中。

    bind的參數順序

    如前文所述,我們可以用bind修正參數的值。更一般的,可以用bind綁定給定可調用對象中的參數或重新安排其順序。例如,假定f是一個可調用對象,它有5個參數,則下面對bind的調用:

    //g是一個有兩個參數的可調用對象 auto g = bind(f, a, b, _2, c, _1);

    生成一個新的可調用對象,它有兩個參數,分別用占位符_2和_1表示。這個新的可調用對象將它自己的參數作為第三個和第五個參數傳遞給f。f的第一個、第二個和第四個參數分別被綁定到給定的值a、b和c 上。

    傳遞給g的參數按位置綁定到占位符。即,第一個參數綁定到_1,第二個參數綁定到_2。因此,當我們調用g 時,其第一個參數將被傳遞給f作為最后一個參數,第二個參數將被傳遞給f作為第三個參數。

    實際上,這個bind 調用會將

    g(_1, _2)

    映射為

    f(a, b, _2, c, _1)

    即,對g的調用會調用f,用g的參數代替占位符,再加上綁定的參數a、b和c。

    例如,調用g(X, Y)會調用

    f(a, b, Y, c, X)

    用bind重排參數順序(實例)

    下面是用bind重排參數順序的一個具體例子,我們可以用bind顛倒isShoter的含義:

    //按單詞長度由短至長排序 sort(words.begin(), words.end(), isShorter);//按單詞長度由長至短排序 sort(words.begin(), words.end(), bind(isShorter, _2, _1));

    在第一個調用中,當sort需要比較兩個元素A和B時,它會調用isShorter (A, B)。在第二個對sort 的調用中,傳遞給isShorter的參數被交換過來了。因此,當sort比較兩個元素時,就好像調用isShorter(B, A)一樣。

    綁定引用參數

    默認情況下,bind的那些不是占位符的參數被拷貝到bind返回的可調用對象中。但是,與 lambda類似,有時對有些綁定的參數我們希望以引用方式傳遞,或是要綁定參數的類型無法拷貝

    例如,為了替換一個引用方式捕獲ostream的lambda:

    //os是一個局部變量,引用一個輸出流 //c是一個局部變量,類型為char for_each(words.begin(), words.end(),[&os, c](const string &s) { os << s << c;);

    可以很容易地編寫一個函數,完成相同的工作:

    ostream &print(ostream &os, const string &s, char c){return os << s << c; }

    但是,不能直接用bind來代替對os的捕獲:

    //錯誤:不能拷貝os for_each(words.begin(), words.end(), bind(print, os, _1, ' '));

    原因在于bind拷貝其參數,而我們不能拷貝一個ostream。如果我們希望傳遞給bind一個對象而又不拷貝它,就必須使用標準庫ref函數:

    for_each(words.begin(), words.end(),bind(print, ref(os), _1, ' '));

    函數ref返回一個對象,包含給定的引用,此對象是可以拷貝的。

    標準庫中還有一個cref函數,生成一個保存 const引用的類。與bind一樣,函數ref和 cref也定義在頭文件functional 中。

    向后兼容:參數綁定

    舊版本C++提供的綁定函數參數的語言特性限制更多,也更復雜。標準庫定義了兩個分別名為bind1st和 bind2nd的函數。類似bind,這兩個函數接受一個函數作為參數,生成一個新的可調用對象,該對象調用給定函數,并將綁定的參數傳遞給它。但是,這些函數分別只能綁定第一個或第二個參數。由于這些函數局限太強,在新標準中已被棄用(deprecated)。所謂被棄用的特性就是在新版本中不再支持的特性。新的C++程序應該使用bind。

    再探迭代器

    除了為每個容器定義的迭代器之外,標準庫在頭文件 iterator 中還定義了額外幾種迭代器。這些迭代器包括以下幾種。

    • 插入迭代器(insert iterator):這些迭代器被綁定到一個容器上,可用來向容器插入元素。
    • 流迭代器(stream iterator):這些迭代器被綁定到輸入或輸出流上,可用來遍歷所
      關聯(lián)的IO流。
    • 反向迭代器(reverse iterator):這些迭代器向后而不是向前移動。除了forward_list之外的標準庫容器都有反向迭代器。
    • 移動迭代器(move iterator):這些專用的迭代器不是拷貝其中的元素,而是移動它們。我們將在第13章介紹移動迭代器。

    插入迭代器

    插入器是一種迭代器適配器,它接受一個容器,生成一個迭代器,能實現向給定容器添加元素。當我們通過一個插入迭代器進行賦值時,該迭代器調用容器操作來向給定容器的指定位置插入一個元素。

    下表列出了這種迭代器支持的操作。

    操作說明
    it=t在it指定的當前位置插入值t。
    假定c是it綁定的容器,依賴于插入迭代器的不同種類,
    此賦值會分別調用c.push_back (t) 、c.push_front(t)或c.insert (t,p),
    其中p為傳遞給inserter的迭代器位置
    *it, ++it, it++這些操作雖然存在,但不會對it做任何事情。每個操作都返回it

    插入器有三種類型,差異在于元素插入的位置:

    • back _inserter創(chuàng)建一個使用push_back的迭代器。
    • front_inserter創(chuàng)建一個使用push_front的迭代器。
    • inserter創(chuàng)建一個使用insert的迭代器。此函數接受第二個參數,這個參數必須是一個指向給定容器的迭代器。元素將被插入到給定迭代器所表示的元素之前。

    Note:只有在容器支持push_front的情況下,我們才可以使用front_inserter。類似的,只有在容器支持 push_back的情況下,我們才能使用back_inserter。

    理解插入器的工作過程是很重要的:當調用inserter(c, iter)時,我們得到一個迭代器,接下來使用它時,會將元素插入到iter原來所指向的元素之前的位置。即,如果it是由inserter生成的迭代器,則下面這樣的賦值語句

    *it = val;

    其效果與下面代碼一樣

    it = c.insert(it, val) ; // it指向新加入的元素 ++it; //遞增it使它指向原來的元素

    front_inserter生成的迭代器的行為與inserter生成的迭代器完全不一樣。當我們使用front_inserter時,元素總是插入到容器第一個元素之前。即使我們傳遞給inserter的位置原來指向第一個元素,只要我們在此元素之前插入一個新元素,此元素就不再是容器的首元素了:

    list<int> lst = { 1, 2, 3, 4}; list<int> lst2,lst3; //空list//拷貝完成之后,lst2包含4 3 2 1 copy(lst.cbegin(), lst.cend(), front_inserter(lst2));//拷貝完成之后,lst3包含1 2 3 4 copy(lst.cbegin(), lst.cend(), inserter(lst3, lst3.begin()));

    當調用front_inserter?時,我們得到一個插入迭代器,接下來會調用push_front。當每個元素被插入到容器c中時,它變?yōu)閏的新的首元素。因此,front_inserter生成的迭代器會將插入的元素序列的順序顛倒過來,而inserter和back_inserter則不會。

    iostream迭代器

    雖然iostream類型不是容器,但標準庫定義了可以用于這些IO類型對象的迭代器。

    • istream_iterator讀取輸入流,
    • ostream_iterator向一個輸出流寫數據。

    這些迭代器將它們對應的流當作一個特定類型的元素序列來處理。通過使用流迭代器,我們可以用泛型算法從流對象讀取數據以及向其寫入數據。

    istream_iterator操作

    操作說明
    istream_iterator<T> in (is);in從輸入流is讀取類型為T的值
    istream_iterator<T> end;讀取類型為T的值的istream_iterator迭代器,表示尾后位置
    inl == in2in1和in2必須讀取相同類型。如果它們都是尾后迭代器,或綁定到相同的輸入,則兩者相等
    inl != in2同上一條
    *in返回從流中讀取的值
    in->mem與(*in).mem的含義相同
    ++in, in++使用元素類型所定義的>>運算符從輸入流中讀取下一個值。與以往一樣,前置版本返回一個指向遞增后迭代器的引用,后置版本返回舊值

    當創(chuàng)建一個流迭代器時,必須指定迭代器將要讀寫的對象類型。一個istream_iterator使用>>來讀取流。因此, istream_iterator要讀取的類型必須定義了輸入運算符。當創(chuàng)建一個istream_iterator時,我們可以將它綁定到一個流。當然,我們還可以默認初始化迭代器,這樣就創(chuàng)建了一個可以當作尾后值使用的迭代器。

    istream_iterator<int> int_it(cin);//從cin讀取int istream_iterator<int> int_eof;//尾后迭代器 ifstream in("afile"); istream_iterator<string> str_it (in);//從"afile"讀取字符串

    下面是一個用istream_iterator從標準輸入讀取數據,存入一個vector的例子:

    istream_iterator<int> in_iter(cin);//從cin讀取int istream_iterator<int> eof;// istream尾后迭代器while (in_iter != eof) //當有數據可供讀取時//后置遞增運算讀取流,返回迭代器的舊值//解引用迭代器,獲得從流讀取的前一個值vec.push_back (*in_iter++);

    此循環(huán)從cin讀取int 值,保存在vec中。在每個循環(huán)步中,循環(huán)體代碼檢查in_iter是否等于eof。eof被定義為空的istream_iterator,從而可以當作尾后迭代器來使用。對于一個綁定到流的迭代器,一旦其關聯(lián)的流遇到文件尾或遇到IO錯誤,迭代器的值就與尾后迭代器相等。

    此程序最困難的部分是傳遞給push_back的參數,其中用到了解引用運算符和后置遞增運算符。該表達式的計算過程與我們之前寫過的其他結合解引用和后置遞增運算的表達式一樣。后置遞增運算會從流中讀取下一個值,向前推進,但返回的是迭代器的舊值。迭代器的舊值包含了從流中讀取的前一個值,對迭代器進行解引用就能獲得此值。

    我們可以將程序重寫為如下形式,這體現了istream_iterator更有用的地方:

    istream_iterator<int> in_iter(cin), eof; // 從cin 讀取 int vector<int> vec(in_iter, eof); //從迭代器范圍構造vec

    本例中我們用一對表示元素范圍的迭代器來構造vec。這兩個迭代器是istream_iterator,這意味著元素范圍是通過從關聯(lián)的流中讀取數據獲得的。這個構造函數從cin中讀取數據,直至遇到文件尾或者遇到一個不是int的數據為止。從流中讀取的數據被用來構造vec。

    使用算法操作流迭代器

    由于算法使用迭代器操作來處理數據,而流迭代器又至少支持某些迭代器操作,因此我們至少可以用某些算法來操作流迭代器。稍后會看到如何分辨哪些算法可以用于流迭代器。下面是一個例子,我們可以用一對istream_iterator來調用accumulate:

    istream_iterator<int> in(cin), eof; cout << accumulate(in, eof, 0) << endl;

    此調用會計算出從標準輸入讀取的值的和。如果輸入為:

    23 109 45 89 6 34 12 90 34 23 56 23 8 89 23

    則輸出為664。

    istream_iterator允許使用懶惰求值

    當我們將一個istream_iterator綁定到一個流時,標準庫并不保證迭代器立即從流讀取數據。具體實現可以推遲從流中讀取數據,直到我們使用迭代器時才真正讀取。標準庫中的實現所保證的是,在我們第一次解引用迭代器之前,從流中讀取數據的操作已經完成了。

    對于大多數程序來說,立即讀取還是推遲讀取沒什么差別。但是,如果我們創(chuàng)建了一個istream iterator,沒有使用就銷毀了,或者我們正在從兩個不同的對象同步讀取同一個流,那么何時讀取可能就很重要了。

    (MyNote:懶加載)

    ostream_iterator操作

    我們可以對任何具有輸出運算符(<<運算符)的類型定義ostream_iterator。當創(chuàng)建一個ostream_iterator時,我們可以提供(可選的)第二參數,它是一個字符串,在輸出每個元素后都會打印此字符串。此字符串必須是一個C風格字符串(即,一個字符串字面常量或者一個指向以空字符結尾的字符數組的指針)。必須將ostream_iterator綁定到一個指定的流,不允許空的或表示尾后位置的ostream_iterator。

    操作說明
    ostream_iterator<T> out (os);out將類型為T的值寫到輸出流os 中
    ostream_iterator<T> out (os, d);out將類型為T的值寫到輸出流os 中,每個值后面都輸出一個d。d指向一個空字符結尾的字符數組
    out = val用<<運算符將val寫入到out所綁定的ostream 中。val的類型必須與out可寫的類型兼容
    *out, ++out, out++這些運算符是存在的,但不對out做任何事情。每個運算符都返回out

    我們可以用ostream_iterator來輸出值的序列:

    ostream_iterator<int> out_iter (cout, " ");for (auto e : vec)*out_iter++ = e; //賦值語句實際上將元素寫到cout cout << endl;

    此程序將vec中的每個元素寫到cout,每個元素后加一個空格。每次向out_iter賦值時,寫操作就會被提交。

    值得注意的是,當我們向out_iter賦值時,可以忽略解引用和遞增運算。即,循環(huán)可以重寫成下面的樣子:

    for (auto e : vec)out_iter = e; //賦值語句將元素寫到cout cout << endl;

    運算符*和++實際上對ostream_iterator對象不做任何事情,因此忽略它們對我們的程序沒有任何影響。但是,推薦第一種形式。在這種寫法中,流迭代器的使用與其他迭代器的使用保持一致。如果想將此循環(huán)改為操作其他迭代器類型,修改起來非常容易。而且,對于讀者來說,此循環(huán)的行為也更為清晰。

    可以通過調用copy來打印vec中的元素,這比編寫循環(huán)更為簡單:

    copy(vec.begin(), vec.end(), out_iter); cout << endl;

    使用流迭代器處理類類型

    我們可以為任何定義了輸入運算符(>>)的類型創(chuàng)建istream_iterator對象。類似的,只要類型有輸出運算符(<<),我們就可以為其定義ostream_iterator。

    由于Sales_item既有輸入運算符也有輸出運算符,因此可以使用IO迭代器重寫第一章中的書店程序:

    istream_iterator<Sales_item> item_iter(cin), eof; ostream_iterator<Sales_item> out_iter(cout, "\n");//寫入cout,每個結果后面都跟一個換行符//將第一筆交易記錄存在sum中,并讀取下一條記錄 //此處,我們對item_iter執(zhí)行后置遞增操作,對結果進行解引用操作。 //這個表達式讀取下一條交易記錄,并用之前保存在item_iter中的值來初始化sum。 Sales_item sum = *item_iter++;while (item_iter != eof) {//while循環(huán)會反復執(zhí)行,直至在cin上遇到文件尾為止//如果當前交易記錄(存在item_iter 中)有著相同的ISBN號if (item_iter->isbn() == sum.isbn())sum += *item_iter++; //將其加到sum上并讀取下一條記錄else {out_iter = sum; //輸出sum當前值sum = *item_iter++; //讀取下一條記錄}// sum = *item_iter++;重復代碼,可優(yōu)化 } out_iter = sum; //記得打印最后一組記錄的和

    此程序使用item_iter從cin讀取Sales_item交易記錄,并將和寫入cout,每個結果后面都跟一個換行符。

    泛型算法結構

    任何算法的最基本的特性是它要求其迭代器提供哪些操作。某些算法,如 find,只要求通過迭代器訪問元素、遞增迭代器以及比較兩個迭代器是否相等這些能力。其他一些算法,如 sort,還要求讀、寫和隨機訪問元素的能力。算法所要求的迭代器操作可以分為5個迭代器類別(iterator category),如下表所示。

    每個算法都會對它的每個迭代器參數指明須提供哪類迭代器。

    操作說明
    輸入迭代器只讀,不寫;單遍掃描,只能遞增
    輸出迭代器只寫,不讀;單遍掃描,只能遞增
    前向迭代器可讀寫;多遍掃描,只能遞增
    雙向迭代器可讀寫;多遍掃描,可遞增遞減
    隨機訪問迭代器可讀寫,多遍掃描,支持全部迭代器運算

    第二種算法分類的方式是按照是否讀、寫或是重排序列中的元素來分類。附錄A按這種分類方法列出了所有算法。

    5類迭代器

    類似容器,迭代器也定義了一組公共操作。一些操作所有迭代器都支持,另外一些只有特定類別的迭代器才支持。例如,ostream_iterator只支持遞增、解引用和賦值。vector、string和 deque 的迭代器除了這些操作外,還支持遞減、關系和算術運算。

    迭代器是按它們所提供的操作來分類的,而這種分類形成了一種層次。除了輸出迭代器之外,一個高層類別的迭代器支持低層類別迭代器的所有操作。

    C++標準指明了泛型和數值算法的每個迭代器參數的最小類別。例如,

    • find算法在一個序列上進行一遍掃描,對元素進行只讀操作,因此至少需要輸入迭代器。

    • replace函數需要一對迭代器,至少是前向迭代器。

    • 類似的,replace_copy的前兩個迭代器參數也要求至少是前向迭代器。其第三個迭代器表示目的位置,必須至少是輸出迭代器。

    其他的例子類似。對每個迭代器參數來說,其能力必須與規(guī)定的最小類別至少相當。向算法傳遞一個能力更差的迭代器會產生錯誤。

    WARNING:對于向一個算法傳遞錯誤類別的迭代器的問題,很多編譯器不會給出任何警告或提示。

    迭代器類別

    輸入迭代器(input iterator):可以讀取序列中的元素。一個輸入迭代器必須支持

    • 用于比較兩個迭代器的相等和不相等運算符(==、!=)
    • 用于推進迭代器的前置和后置遞增運算(++)
    • 用于讀取元素的解引用運算符(*);解引用只會出現在賦值運算符的右側
    • 箭頭運算符(->),等價于(*it) .member,即,解引用迭代器,并提取對象的成員*

    輸入迭代器只用于順序訪問。對于一個輸入迭代器,*it++保證是有效的,但遞增它可能導致所有其他指向流的迭代器失效。其結果就是,不能保證輸入迭代器的狀態(tài)可以保存下來并用來訪問元素。因此,輸入迭代器只能用于單遍掃描算法。算法find和 accumulate要求輸入迭代器;而istream_iterator是一種輸入迭代器。

    輸出迭代器(output iterator):可以看作輸入迭代器功能上的補集——只寫而不讀元素。輸出迭代器必須支持

    • 用于推進迭代器的前置和后置遞增運算(++)

    • 解引用運算符(*),只出現在賦值運算符的左側(向一個已經解引用的輸出迭代器賦值,就是將值寫入它所指向的元素)

    我們只能向一個輸出迭代器賦值一次。類似輸入迭代器,輸出迭代器只能用于單遍掃描算法。用作目的位置的迭代器通常都是輸出迭代器。例如,copy函數的第三個參數就是輸出迭代器。ostream_iterator類型也是輸出迭代器。

    前向迭代器(forward iterator):可以讀寫元素。這類迭代器只能在序列中沿一個方向移動。前向迭代器支持所有輸入和輸出迭代器的操作,而且可以多次讀寫同一個元素。因此,我們可以保存前向迭代器的狀態(tài),使用前向迭代器的算法可以對序列進行多遍掃描。算法replace要求前向迭代器,forward_list 上的迭代器是前向迭代器。

    雙向迭代器(bidirectional iterator):可以正向/反向讀寫序列中的元素。除了支持所有前向迭代器的操作之外,雙向迭代器還支持前置和后置遞減運算符(–)。算法reverse要求雙向迭代器,除了forward_list之外,其他標準庫都提供符合雙向迭代器要求的迭代器。

    隨機訪問迭代器(random-access iterator):提供在常量時間內訪問序列中任意元素的能力。此類迭代器支持雙向迭代器的所有功能,此外還支持如下操作:

    • 用于比較兩個迭代器相對位置的關系運算符(<、<=、>和>=)

    • 迭代器和一個整數值的加減運算(+、+=、-和-=),計算結果是迭代器在序列中前進(或后退)給定整數個元素后的位置

    • 用于兩個迭代器上的減法運算符(-),得到兩個迭代器的距離

    • 下標運算符(iter[n]),與* (iter[n])等價

    算法sort要求隨機訪問迭代器。array、deque、string和 vector的迭代器都是隨機訪問迭代器,用于訪問內置數組元素的指針也是。

    算法形參模式

    在任何其他算法分類之上,還有一組參數規(guī)范。理解這些參數規(guī)范對學習新算法很有幫助——通過理解參數的含義,你可以將注意力集中在算法所做的操作上。大多數算法具有如下4種形式之一:

    alg(beg, end, other args); alg(beg, end, dest, other args); alg(beg, end, beg2, other args); alg(beg, end, beg2, end2, other args);

    其中 alg是算法的名字,beg和 end表示算法所操作的輸入范圍。幾乎所有算法都接受一個輸入范圍,是否有其他參數依賴于要執(zhí)行的操作。這里列出了常見的一種——dest、beg2和 end2,都是迭代器參數。顧名思義,如果用到了這些迭代器參數,它們分別承擔指定目的位置和第二個范圍的角色。除了這些迭代器參數,一些算法還接受額外的、非迭代器的特定參數。

    接受單個目標迭代器的算法

    dest參數是一個表示算法可以寫入的目的位置的迭代器。算法假定(assume):按其需要寫入數據,不管寫入多少個元素都是安全的。

    WARNING:向輸出迭代器寫入數據的算法都假定目標空間足夠容納寫入的數據。

    如果dest是一個直接指向容器的迭代器,那么算法將輸出數據寫到容器中已存在的元素內。更常見的情況是,dest被綁定到一個插入迭代器或是一個ostream_iterator。插入迭代器會將新元素添加到容器中,因而保證空間是足夠的。ostream_iterator 會將數據寫入到一個輸出流,同樣不管要寫入多少個元素都沒有問題。

    接受第二個輸入序列的算法

    接受單獨的beg2或是接受beg2和end2的算法用這些迭代器表示第二個輸入范圍。這些算法通常使用第二個范圍中的元素與第一個輸入范圍結合來進行一些運算。

    如果一個算法接受 beg2和end2,這兩個迭代器表示第二個范圍。這類算法接受兩個完整指定的范圍:[beg, end)表示的范圍和[beg2, end2)表示的第二個范圍。

    只接受單獨的 beg2(不接受end2)的算法將beg2作為第二個輸入范圍中的首元素。此范圍的結束位置未指定,這些算法假定從beg2開始的范圍與 beg和end所表示的范圍至少一樣大。
    接受單獨beg2的算法假定從 beg2開始的序列與 beg和end所表示的范圍至少一樣大。

    WARNING:接受單獨beg2的算法假定從 beg2開始的序列與beg和end所表示的范圍至少一樣大。

    算法命名規(guī)范

    除了參數規(guī)范,算法還遵循一套命名和重載規(guī)范。這些規(guī)范處理諸如:如何提供一個操作代替默認的<或–運算符以及算法是將輸出數據寫入輸入序列還是一個分離的目的位置等問題。

    一些算法使用重載形式傳遞一個謂詞

    接受謂詞參數來代替<或–運算符的算法,以及那些不接受額外參數的算法,通常都是重載的函數。函數的一個版本用元素類型的運算符來比較元素;另一個版本接受一個額外謂詞參數,來代替<或–:

    unique(beg, end); //使用==運算符比較元素 unique(beg, end, comp); //使用comp比較元素

    兩個調用都重新整理給定序列,將相鄰的重復元素刪除。

    • 第一個調用使用元素類型的==運算符來檢查重復元素;
    • 第二個則調用comp來確定兩個元素是否相等。

    由于兩個版本的函數在參數個數上不相等,因此具體應該調用哪個版本不會產生歧義。

    _if版本的算法

    接受一個元素值的算法通常有另一個不同名的(不是重載的)版本,該版本接受一個謂詞代替元素值。接受謂詞參數的算法都有附加的_if前綴:

    find(beg, end, val) ; //查找輸入范圍中val第一次出現的位置 find_if(beg, end, pred) ; //查找第一個令pred為真的元素

    這兩個算法都在輸入范圍中查找特定元素第一次出現的位置。算法find查找一個指定值;算法find_if查找使得pred返回非零值的元素。

    這兩個算法提供了命名上差異的版本,而非重載版本,因為兩個版本的算法都接受相同數目的參數。因此可能產生重載歧義,雖然很罕見,但為了避免任何可能的歧義,標準庫選擇提供不同名字的版本而不是重載。

    區(qū)分拷貝元素的版本和不拷貝的版本

    默認情況下,重排元素的算法將重排后的元素寫回給定的輸入序列中。這些算法還提供另一個版本,將元素寫到一個指定的輸出目的位置。如我們所見,寫到額外目的空間的

    算法都在名字后面附加一個_copy:

    reverse(beg, end); //反轉輸入范圍中元素的順序 reverse_copy(beg, end, dest);//將元素按逆序拷貝到dest

    一些算法同時提供_copy和_if版本。這些版本接受一個目的位置迭代器和一個謂詞:

    //從v1中刪除奇數元素 remove_if(v1.begin(), v1.end(),[](int i) { return i % 2; }); //將偶數元素從v1拷貝到v2; v1不變 remove_copy_if(v1.begin(), v1.end(), back_inserter(v2) ,[](int i){ return i % 2; });

    兩個算法都調用了lambda來確定元素是否為奇數。

    • 在第一個調用中,我們從輸入序列中將奇數元素刪除。
    • 在第二個調用中,我們將偶數元素從輸入范圍拷貝到v2中。

    特定容器算法(鏈表)

    與其他容器不同,鏈表類型list和 forward_list定義了幾個成員函數形式的算法,如下表所示。特別是,它們定義了獨有的sort、merge、remove、reverse和unique。通用版本的sort要求隨機訪問迭代器,因此不能用于list和forward_list,因為這兩個類型分別提供雙向迭代器和前向迭代器

    鏈表類型定義的其他算法的通用版本可以用于鏈表,但代價太高。這些算法需要交換輸入序列中的元素。一個鏈表可以通過改變元素間的鏈接而不是真的交換它們的值來快速“交換”元素。因此,這些鏈表版本的算法的性能比對應的通用版本好得多。

    Best Practices:對于list和forward_list,應該優(yōu)先使用成員函數版本的算法而不是通用算法。

    注:表內操作都返回void

    操作說明
    lst.merge(lst2)將來自lst2的元素合并入lst。lst和lst2都必須是有序的。
    lst.merge(lst2, comp)元素將從lst2中刪除。在合并之后,lst2變?yōu)榭铡5谝粋€版本使用<運算符;第二個版本使用給定的比較操作
    lst.remove(val)調用erase刪除掉與給定值相等(==)或令一元謂詞為真的每個元素
    lst.remove_if(pred)同上一條
    lst.reverse()反轉lst中元素的順序
    lst.sort()使用<或給定比較操作排序元素
    lst.sort(comp)同上一條
    lst.unique()調用erase刪除同一個值的連續(xù)拷貝。第一個版本使用==;第二個版本使用給定的二元謂詞
    lst.unique(pred)同上一條

    splice成員

    鏈表類型還定義了splice算法,其描述見下表。此算法是鏈表數據結構所特有的,因此不需要通用版本。

    list和forward_list的splice成員函數的參數:

    lst.splice(args)或flst.splice_after(args)

    list和forward_list的splice成員函數的參數args說明
    (p, lst2)p是一個指向lst中元素的迭代器,或一個指向flst首前位置的迭代器。函數將lst2的所有元素移動到lst中 p之前的位置或是flst中p之后的位置。將元素從lst2中刪除。lst2的類型必須與lst或flst相同,且不能是同一個鏈表
    (p, lst2, p2)p2是一個指向lst2中位置的有效的迭代器。將p2指向的元素移動到lst中,或將p2之后的元素移動到flst中。lst2可以是與lst或flst相同的鏈表
    (p, lst2, b, e)b和e必須表示 lst2中的合法范圍。將給定范圍中的元素從lst2移動到lst或flst。lst2與lst(或flst)可以是相同的鏈表,但p不能指向給定范圍中元素

    splice
    英 [spla?s] 美 [spla?s]
    v. 絞接,捻接(兩段繩子);膠接,粘接(膠片、磁帶等)
    n. 膠接處;粘接處;絞接處

    鏈表特有的操作會改變容器

    多數鏈表特有的算法都與其通用版本很相似,但不完全相同。鏈表特有版本與通用版本間的一個至關重要的區(qū)別是鏈表版本會改變底層的容器。例如,

    • remove 的鏈表版本會刪除指定的元素。
    • unique 的鏈表版本會刪除第二個和后繼的重復元素。

    類似的,merge和 splice會銷毀其參數。例如,

    • 通用版本的merge將合并的序列寫到一個給定的目的迭代器;兩個輸入序列是不變的。
    • 而鏈表版本的merge函數會銷毀給定的鏈表——元素從參數指定的鏈表中刪除,被合并到調用merge 的鏈表對象中。在merge之后,來自兩個鏈表中的元素仍然存在,但它們都已在同一個鏈表中。

    總結

    以上是生活随笔為你收集整理的《C++ Primer 5th》笔记(10 / 19):泛型算法的全部內容,希望文章能夠幫你解決所遇到的問題。

    如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。