IE盒子

搜索
查看: 102|回复: 0

C++ 20 新特性 ranges 精讲

[复制链接]

1

主题

10

帖子

15

积分

新手上路

Rank: 1

积分
15
发表于 2023-2-7 11:14:55 | 显示全部楼层 |阅读模式
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 处停止
输出:



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



注意
视图不拥有数据。因此,视图不会延长其数据的生命周期。因此,视图只能对左值操作。如果在临时范围上定义视图,则编译将失败。
#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);


}




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

}




函数组合

#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)
回复

使用道具 举报

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

快速回复 返回顶部 返回列表