沒想到C++中的std::remove_if()函數歷史還挺悠久

前言

看到 remove 這個單詞的第一反應是什麼意思?我的第一感覺是刪除、去掉的意思,就像一個程序員看到 string 就會說是字符串,而不會說它是線、或者細繩的意思,可是C++里居然有個函數叫 std::remove(),調用完這個函數什麼也沒刪除,這我就奇怪了,打開有道詞典查詢一下:

不查不要緊,一查嚇一跳,以下是詞典給出的三個釋義:

  • vt. 移動,遷移;開除;調動
  • vi. 移動,遷移;搬家
  • n. 移動;距離;搬家

及物動詞、不及物動詞、名詞給出的含義都是移動,只有一個開除的意思和刪除有點像,難道我穿越了?我之前一直以爲它是刪除的意思啊,很多函數還是用它命名的呢!

趕緊翻翻其他的字典,給高中的英語老師打個電話問問,最終還是在一些釋義中找到了刪除的意思,還有一些用作刪除的例句,有趣的是在有道詞典上,所有的單詞解釋都和移動有關,所有的例句都是和刪除有關。

remove_if的歷史

爲什麼要查單詞的 remove 的意思,當然是被它坑過了,本來想從 std::vector<T> 中刪除指定的元素,考慮到迭代器失效的問題,放棄了循環遍歷的複雜處理,選擇直接使用算法函數 std::remove_if()來進行刪除,之前對於 std::remove()std::remove_if() 有過簡單的瞭解,不過記憶還是出現了偏差。

一直記得 std::remove() 函數調用之後需要再使用 erase() 函數處理下,忘記了 std::remove_if() 函數也要做相同的處理,於是在出現問題的時候一度懷疑這個函數的功能發生了變更,開始找這個函數歷史迭代的版本,這裏推薦一個網站 C++標準函數查詢 - std::remove_if(),用來查詢函數的定義、所在頭文件和使用方法非常方便。

文檔中有這樣兩句:

  1. Removes all elements that are equal to value, using operator== to compare them.
  2. Removes all elements for which predicate p returns true.

解釋函數作用時用到的單詞都是 remove ,你說神不神奇,這裏應該都是取的移動的意思。

這兩句話對應的函數聲明應該是:

template< class ForwardIt, class T >
ForwardIt remove( ForwardIt first, ForwardIt last, const T& value );        // (until C++20)

template< class ForwardIt, class UnaryPredicate >
ForwardIt remove_if( ForwardIt first, ForwardIt last, UnaryPredicate p );   // (until C++20)

這兩個函數後面都有相同的說明—— (until C++20) ,意思大概就是說這兩個函數一直到 C++20 版本都存在,在我的印象中 std::remove_if() 函數比較新,最起碼得比 std::remove() 函數年輕幾歲,可是他們到底是哪個版本添加到c++標準的的呢?中途的功能有沒有發生變更,繼續回憶!

第一次看到這兩個函數應該是在看《Effective STL》這本書的時候,大概是5年前了,正好這個本書就放在手邊,趕緊翻目錄查一下,打開對應章節發現其中確實提到了刪除 std::vector<T> 中的元素時,在調用了這兩個函數之後都需要再調用 erase() 函數對待刪除的元素進行擦除。

看看書的出版時間是2013年,難道是 C++11 的標準加上的,不對,看一下翻譯者寫得序,落款時間2003年,不能是 C++03 的標準吧?不過這是一本翻譯書籍,再看看原作者 Scott Meyers 寫的前言,落款時間2001年,好吧,看來這兩個函數肯定在 C++98的版本中就已經存在了,我有點驚呆了,這確實顛覆了我的記憶和認知。

造成這種認知錯誤主要有兩方面原因,第一方面就是受到了開發環境的限制,從一開始學習的時候Turob C 2.0VC++ 6.0VS2005VS2008VS2010就很少接觸 C++11 的知識,Dev-C++Code::Blocks 也是在特定的情況下使用,沒有過多的研究,結果在剛開始工作的時候開發工具居然是VS2003,這個版本我之前都沒聽說過,還好一步步升級到了08、13、17版本。

第二方面就是這兩個函數常常與 Lambda 表達式,auto 關鍵字一起用,這都是 C++11 裏纔有的,讓人感覺好像這個 std::remove_if() 函數也是 C++11 版本中的內容,造成了錯覺。總來說還是用的少,不熟悉,以後多看多練就好了。

remove_if的實現

要想更深入的學習 std::remove_if() 函數, 那這個函數實現的細節有必要了解一下,這有助於我們理解函數的使用方法,下面給出兩個版本可能的實現方式,也許下面的實現與你查到的不一樣,但是思想是相通的,有些實現細節中使用了 std::find_if() 函數,這裏沒有列舉這個版本,下面這兩個版本的代碼更容易讓人明白,它究竟做了哪些事情。

// C++98 版本
template <class ForwardIterator, class UnaryPredicate>
    ForwardIterator remove_if (ForwardIterator first, ForwardIterator last,
                             UnaryPredicate pred)
{
    ForwardIterator result = first;
    while (first!=last) {
        if (!pred(*first)) {
            *result = *first;
            ++result;
        }
        ++first;
    }
    return result;
}
// C++11     版本
template <class ForwardIterator, class UnaryPredicate>
    ForwardIterator remove_if (ForwardIterator first, ForwardIterator last,
                             UnaryPredicate pred)
{
    ForwardIterator result = first;
    while (first!=last) {
        if (!pred(*first)) {
            *result = std::move(*first);
            ++result;
        }
        ++first;
    }
    return result;
}

對比兩段代碼有沒有發現區別——只改了半行代碼,將賦值語句中的 *firstC++11 版本中替換成了 std::move(*first),這隻能發生在 C++11 之後,因爲 std::move() 函數是 C++11 才加入的。

這代碼乍一看挺唬人的,其實仔細分析一下還挺簡單的,只是這些符號看起來有些生疏,其實可以把 ForwardIterator 看成一個指針類型,UnaryPredicate 是一個函數類型,我們改寫一下:

int* remove_if (int* first, int* last, func_type func)
{
    int* result = first;
    for (;first!=last;++first)
    {
        if (!func(*first))
        {
            *result = *first;
            ++result;
        }
    }
    return result;
}

這代碼是不是就比較接地氣了,想想一下,一個是包含10個元素的數組,讓你刪除其中的偶數怎麼做?其實就是遍歷一遍數組,從開始位置到結束位置逐個判斷,如果不是偶數就不進行操作,如果是偶數就把當前的偶數向前移動到結果指針上就好了,結果指針向後移動準備接受下一個奇數,這個判斷是不是偶數的函數就是上面代碼中的 func()

最後結果指針 result 停在有效元素後面一個位置上,這個位置到結尾指針 last 的位置上的元素都應該被刪除,這就是爲什麼常常將 std::remove_if() 函數的返回值作爲 erase() 函數的第一個參數,而將 last 指針作爲 erase() 函數的第二個參數,實際作用就是將這些位置上的元素擦除,從頭擦到尾,達到真正刪除的目的。

具體使用

說了這麼多,接下來看看具體怎麼用,我們將 std::remove_if() 函數和 erase() 函數分開使用,主要看一下調用 std::remove_if() 函數之後的 vector 中元素的值是怎麼變的。

#include <iostream>
#include <vector>
#include <algorithm>

bool isEven(int n) // 是否是偶數
{
    return n % 2 == 0;
}

int main()
{
    std::vector<int> vecTest;
    for (int i = 0; i < 10; ++i)
        vecTest.push_back(i);

    for (int i = 0; i < vecTest.size(); ++i)
        std::cout << vecTest[i] << " ";
    std::cout << std::endl;

    // 移動元素
    std::vector<int>::iterator itor = std::remove_if(vecTest.begin(), vecTest.end(), isEven);

    // 查看移動後的變化
    for (int i = 0; i < vecTest.size(); ++i)
        std::cout << vecTest[i] << " ";
    std::cout << std::endl;

    // 刪除元素
    vecTest.erase(itor, vecTest.end());

    for (int i = 0; i < vecTest.size(); ++i)
        std::cout << vecTest[i] << " ";

    return 0;
}

運行結果爲:

0 1 2 3 4 5 6 7 8 9
1 3 5 7 9 5 6 7 8 9
1 3 5 7 9

從結果可以看出,第二步調用 std::remove_if() 函數之後,vector 中的元素個數並沒有減少,只是將後面不需要刪除的元素移動到了 vector 的前面,從第二行結果來看,調用 std::remove_if() 函數之後返回的結果 itor 指向5,所以擦除從5所在位置到結尾的元素就達到了我們的目的。

這段代碼在 C++98C++11C++14 環境下都可以編譯運行,在這裏推薦一個在線編譯器 C++ Shell,可以測試各個版本編譯器下運行結果,界面簡潔明瞭,方便測試。

上面的代碼其實寫得有些囉嗦,如果使用 C++11 語法之後,可以簡寫爲:

#include <iostream>
#include <vector>
#include <algorithm>

int main()
{
    std::vector<int> vecTest{0, 1, 2, 3, 4, 5, 6, 7 ,8, 9};

    std::for_each(vecTest.begin(), vecTest.end(), [](int n){ std::cout << n << " "; });
    std::cout << std::endl;

    // 移動後刪除元素
    vecTest.erase(std::remove_if(vecTest.begin(), vecTest.end(),
        [](int n){ return n % 2 == 0; }), vecTest.end());

    std::for_each(vecTest.begin(), vecTest.end(), [](int n){ std::cout << n << " "; });

    return 0;
}

運行結果:

0 1 2 3 4 5 6 7 8 9
1 3 5 7 9

總結

  1. 對於模糊的知識要花時間複習,避免臨時用到的時候手忙腳亂出問題
  2. 對於一些心存疑慮的函數可以看一下具體的實現,知道實現的細節可以讓我們更加清楚程序都做了哪些事情
  3. 對於新的技術標準可以不精通,但是必須花一些時間進行了解,比如新的 C++ 標準
  4. 對於違反常識的代碼,先不要否定,即使在你的運行環境中報錯,說不定人家是新語法呢?
  5. 曾經看到一段在類的定義時初始化非靜態變量的代碼,一度認爲編譯不過,但後來發現在 C++11 中運行的很好
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章