天天看點

C++ 20 新特性 ranges 精講C++ 20 新特性 ranges 精講

C++ 20 新特性 ranges 精講

C++20 中的

ranges

庫使得使用 STL 更加舒适和強大。

ranges

庫中的算法是惰性的,可以直接在容器上工作,并且可以很容易地組合。簡而言之,

ranges

庫的舒适性和強大性都源于它的函數思想。

在深入細節之前,這裡有一個

ranges

庫的第一個示例:結合 transform 和 filter 函數。

示例代碼:

#include <iostream>
#include <`ranges`>
#include <vector>
#include <format>

int main() {


    std::vector vec{1, 12, 32, 54, 10086, -1314};

    auto res = vec | std::views::filter([](int n) { return n % 2 == 0; })
               | std::views::transform([](int n) { return n * 2; });

    for (auto v: res) {

        std::cout << v << std::endl;
    }
}
           

應該從左到右讀取這個表達式。管道符号代表函數組合:首先,所有偶數才能通過

(std::views::filter([](int n){ return n % 2 == 0; }))

。之後,每個剩餘的數字都映射到它的兩倍

(std::views::transform([](int n){ return n * 2; }))

。這個小示例展示了

ranges

庫的兩個新功能:函數組合應用于整個容器。

Ranges

Ranges的概念:

begin

疊代器和

end

哨兵提供的

range

指定了一組可以周遊的項目。 STL 的容器是

range

,但不是

views

。 哨兵指定了

range

的結束。

Sentinel

對于 STL 的容器,

end

疊代器是哨兵。在 C++20 中,哨兵的類型可以與

begin

疊代器的類型不同。

根據 range 的不同,一個空字元

'\0'

可能結束一個字元串,一個空字元串

std::string{}

可能結束一個單詞清單,一個

std::nullptr

可能結束一個連結清單,或者數字

-1

可能結束一個非負數清單。

下面的例子使用哨兵來操作

C-string

std::vector<int>

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

struct Space {
    bool operator==(auto pos) const {
        return *pos == ' ';
    }
};

struct NegativeNumber {
    bool operator==(auto num) const {
        return *num < 0;
    }
};

struct Sum {
    void operator()(auto n) { sum += n; }

    int sum{0};
};

int main() {


    const char *codingriji = "subscribed to my wechat official account codingriji";

    std::ranges::for_each(codingriji, Space{}, [](char c) { std::cout << c; });
    std::cout << '\n';
    for (auto c: std::ranges::subrange{codingriji, Space{}}) std::cout << c;
    std::cout << '\n';

    std::ranges::subrange rainer{codingriji, Space{}};
    std::ranges::for_each(rainer, [](char c) { std::cout << c << ' '; });
    std::cout << '\n';
    for (auto c: rainer) std::cout << c << ' ';
    std::cout << '\n';


    std::cout << "\n";


    std::vector<int> myVec{5, 10, 33, -5, 10, 10086, 10010};

    for (auto v: myVec) std::cout << v << " ";
    std::cout << '\n';

    auto [tmp1, sum] = std::ranges::for_each(myVec, Sum{});
    std::cout << "Sum: " << sum.sum << '\n';

    auto [tmp2, sum2] = std::ranges::for_each(std::begin(myVec), NegativeNumber{},
                                              Sum{});
    std::cout << "Sum: " << sum2.sum << '\n';

    std::ranges::transform(std::begin(myVec), NegativeNumber{},

                           std::begin(myVec), [](auto num) { return num * num; });
    std::ranges::for_each(std::begin(myVec), NegativeNumber{},
                          [](int num) { std::cout << num << " "; });
    std::cout << '\n';
    for (auto v: std::ranges::subrange{std::begin(myVec), NegativeNumber{}}) {
        std::cout << v << " ";
    }

    std::cout << "\n\n";

}
           

定義了兩個哨兵:

Space

NegativeNumber

。兩者都定義了等于運算符。由于使用了

<compare>

頭檔案,編譯器會自動生成不等運算符。當使用

std::ranges_for_each

std::ranges::tranform

等算法時需要使用不等運算符。

我先來介紹一下哨兵

Space

。 第 31 行直接在字元串應用了哨兵

Space{}

。建立

std::ranges::subrange

可以在範圍循環中使用哨兵。你也可以定義

std::ranges::subrange

并直接在

std::ranges::for_each

算法 或範圍循環 中使用它。

第二個例子使用了

std::vector<int>

,填充了值 {5, 10, 33, -5, 10}。哨兵

NegativeNumber

檢查數字是否為負數。首先,我使用函數對象

Sum

(第 20 - 23 行) 對所有值求和。

std::ranges::for_each

傳回一對

(it, func)

it

是哨兵的後繼,

func

是應用于範圍的函數對象。

由于結構化綁定,可以直接定義變量

sum

sum2

并顯示它們的值 。

std::ranges::for_each

使用了哨兵

NegativeNumber

。是以,

sum2

是到哨兵的和。調用

std::ranges::transform

将每個元素轉換為它的平方:

[](auto num){ return num * num}

。轉換在哨兵

NegativeNumber

處停止

輸出:

C++ 20 新特性 ranges 精講C++ 20 新特性 ranges 精講

Views

視圖是一種在範圍上應用并執行某些操作的東西。視圖不擁有資料,它的複制、移動或指派時間複雜度為常數。

#include <iostream>
#include <vector>
#include <ranges>

int main() {

    std::vector<int> numbers = {1, 2, 3, 4, 5, 6};
    auto results = numbers | std::views::filter([](int n) { return n % 2 == 0; })
                   | std::views::transform([](int n) { return n * 2; });

    for (auto v: results) {

    }

}
           

在此代碼片段中,

numbers

是範圍,

std::views::filter

std::views::transform

是視圖。此外,

std::string_view

std::span

也是視圖。

由于視圖的存在,C++20 允許以函數式風格程式設計。視圖可以組合并且是懶惰的。

Standard library header (C++20) - cppreference.com

C++ 20 新特性 ranges 精講C++ 20 新特性 ranges 精講

注意

視圖不擁有資料。是以,視圖不會延長其資料的生命周期。是以,視圖隻能對左值操作。如果在臨時範圍上定義視圖,則編譯将失敗。

#include <iostream>
#include <ranges>
#include <vector>

int main() {
    const auto numbers = {1, 2, 3, 4, 5};

    auto firstThree = numbers | std::views::drop(3);
//     auto firstThree = {1, 2, 3, 4, 5} | std::views::drop(3); 錯
    for (auto v: firstThree) {
        std::cout << v << std::endl;
    }


    std::ranges::drop_view firstFour{numbers, 4};
//     std::ranges::drop_view firstFour{{1, 2, 3, 4, 5}, 4}; 錯
}

           

應用于容器

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

int main() {
    std::vector vec{1, 3, 5, 2, 3, 5, 6, 7, 8, 9,67, 5, 23, 4, 2, 6456, 4};
    std::sort(vec.begin(), vec.end()); //不友善
    std::ranges::sort(vec);
    for (auto v: vec) {
        std::cout << v << std::endl;
    }
}
           

Projection(投影)

std::ranges::sort

有兩個重載:

template< std::random_access_iterator I, std::sentinel_for<I> S,
          class Comp = ranges::less, class Proj = std::identity >
requires std::sortable<I, Comp, Proj>
constexpr I
sort( I first, S last, Comp comp = {}, Proj proj = {} );
(1)	(since C++20)
template< ranges::random_access_range R, class Comp = ranges::less,
          class Proj = std::identity >
requires std::sortable<ranges::iterator_t<R>, Comp, Proj>
constexpr ranges::borrowed_iterator_t<R>
sort( R&& r, Comp comp = {}, Proj proj = {} );
           

當你研究第二個重載時,你會注意到它接受一個可排序的範圍R,一個謂詞

Comp

和一個投影

Proj

。預設的謂詞

Comp

使用

less

,而投影

Proj

使用傳回其參數不變的身份

std :: identity

。投影是将集合映射到子集的映射。

struct Student {
    std::string name;
    int id;
};

void printStudent(const std::vector<Student> &studentCollection) {
    for (const auto &student: studentCollection) {
        std::cout << std::format(" ({} , {}) ", student.name, student.id);
    }
    std::cout << "\n\n";
}


int main() {
    std::vector<Student> studentCollection{{"jack",  10086},
                                           {"black", 10010},
                                           {"trump", 12345},
                                           {"job",   143235}};
    std::ranges::sort(studentCollection, {}, &Student::name);
    printStudent(studentCollection);

    std::ranges::sort(studentCollection, std::ranges::greater(), &Student::name);
    printStudent(studentCollection);

    std::ranges::sort(studentCollection, std::ranges::greater(), &Student::id);
    printStudent(studentCollection);

    std::ranges::sort(studentCollection, std::ranges::greater(), [](auto p) {
        return std::to_string(p.id) + p.name;
    });
    printStudent(studentCollection);


}
           
C++ 20 新特性 ranges 精講C++ 20 新特性 ranges 精講

map中key&value操作

#include <iostream>
#include <ranges>
#include <unordered_map>

int main() {
    std::unordered_map<std::string, int> m{{"jack",  10086},
                                           {"black", 10010},
                                           {"trump", 12345},
                                           {"job",   143235}};

    auto names = std::views::keys(m);
    for (const auto &name: names) {
        std::cout << name << " ";
    }
    std::cout << "\n";

    auto values = std::views::values(m);
    for (const auto &value: values) {
        std::cout << value << " ";
    }
    std::cout << "\n";

}
           
C++ 20 新特性 ranges 精講C++ 20 新特性 ranges 精講

函數組合

#include <iostream>
#include <ranges>
#include <unordered_map>

int main() {
    std::unordered_map<std::string, int> m{{"jack",    10086},
                                           {"black",   10010},
                                           {"fdasjkh", 10010},
                                           {"dfsjhb",  10010},
                                           {"trump",   12345},
                                           {"job",     143235}};

    auto firstb = [](const std::string &name) { return name[0] == 'b'; };
    for (const auto &name: std::views::keys(m)
                           | std::views::reverse
                           | std::views::take(4)
                           | std::views::filter(firstb)) {
//        auto rev1 = std::views::reverse(std::views::keys(m));
        std::cout << name << std::endl;
    }

}
           

管道符号|是函數組合的文法糖。您可以寫

R | C

,而不是

C (R)

。是以,接下來的等價的。

std::views::keys(m) | std::views::reverse

auto rev1 = std::views::reverse(std::views::keys(m));

惰性計算

std :: views :: iota

是一個範圍工廠,用于通過逐漸增加初始值來建立元素序列。這個序列可以是有限的或無限的。程式rangesIota.cpp使用10個int填充std :: vector,從0開始。

#include <iostream>
#include <numeric>
#include <ranges>
#include <vector>

bool isPrime(int i) {
    for (int j = 2; j * j <= i; ++j) {
        if (i % j == 0) return false;
    }
    return true;
}

auto odd = [](int i) {
    return
            i % 2 == 1;
};

int main() {
    std::cout << std::boolalpha;

    std::vector<int> vec;
    std::vector<int> vec2;

    for (int i: std::views::iota(0, 10)) vec.push_back(i);

    for (int i: std::views::iota(0) | std::views::take(10)) vec2.push_back(i);
    std::cout << "vec == vec2: " << (vec == vec2) << '\n';

    for (int i: vec) std::cout << i << " ";

    std::cout << "求質數" << std::endl;
    for (int i: std::views::iota(1'000'000) | std::views::filter(odd)
                | std::views::filter(isPrime)
                | std::views::take(20)) {
        std::cout << i << std::endl;
    }
}
           

第一個

iota

調用建立從0到9的所有數字,增加1.第二個iota調用建立從0開始的無限資料流,每次增加1.

std :: views :: iota(0)

是懶惰的。在請求時才會得到新值。請求了十次。是以,兩個數組是相同的。

參考Modernes C++ (modernescpp.com)

繼續閱讀