C++ `std::ranges`:C++20 声明式范围操作与算法组合

好的,各位观众老爷,今天咱们来聊聊C++20里新出的一个超级好玩的东西——std::ranges,中文可以叫它“范围”或者“区间”,但我觉得叫“ranges”更酷炫,更有逼格。想象一下,你之前写C++代码,处理数组、vector等等,是不是得用迭代器开始结束,循环遍历,写得眼花缭乱?现在有了std::ranges,你可以像写Python一样,用更简洁、更声明式的方式操作数据了!而且还能像搭乐高一样,把各种算法组合起来,简直爽翻天!

一、 啥是std::ranges?为啥要用它?

简单来说,std::ranges就是C++20里对范围操作的一套新标准。它主要解决了以下几个问题:

  1. 简化代码: 之前的C++算法需要传入迭代器开始和结束位置,代码冗长。std::ranges可以直接操作整个范围,代码简洁多了。
  2. 更安全: 避免了迭代器失效的问题。因为你直接操作范围,而不是手动管理迭代器。
  3. 组合性: 可以像搭积木一样,把多个算法组合起来,形成复杂的数据处理流程。这可比手写循环高效多了。
  4. 延迟计算: 很多std::ranges的操作都是延迟计算的,只有在真正需要结果的时候才会执行,提高了效率。

二、 核心概念:View、Action和Range

std::ranges的核心概念有三个:

  • Range(范围): 就是一个可以遍历的序列。比如std::vectorstd::arraystd::list,甚至你自己定义的类,只要满足一定的条件,都可以是Range。
  • View(视图): 视图是一个轻量级的Range,它不会拥有数据,而是对现有Range进行转换和过滤。你可以把View想象成一个眼镜,戴上不同的眼镜,看到的东西就不同了,但眼镜本身并不会改变原来的东西。View是延迟计算的,只有在真正需要结果的时候才会执行。
  • Action(动作): Action是对Range进行操作的函数或函数对象。比如排序、查找、转换等等。

这三个概念的关系是:Range是数据源,View是对Range的转换和过滤,Action是对Range进行操作。通过组合View和Action,我们可以构建复杂的数据处理流程。

三、 常用Range适配器(Views)

Range适配器是std::ranges里最常用的东西,它们可以对Range进行各种转换和过滤。下面是一些常用的Range适配器:

适配器 功能 示例
views::filter 过滤Range中的元素,只保留满足条件的元素。 c++ std::vector<int> numbers = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10}; auto even_numbers = numbers | std::views::filter([](int n){ return n % 2 == 0; }); // even_numbers现在包含{2, 4, 6, 8, 10}
views::transform 转换Range中的元素,将每个元素映射到另一个值。 c++ std::vector<int> numbers = {1, 2, 3, 4, 5}; auto squared_numbers = numbers | std::views::transform([](int n){ return n * n; }); // squared_numbers现在包含{1, 4, 9, 16, 25}
views::take 从Range中取出前N个元素。 c++ std::vector<int> numbers = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10}; auto first_five = numbers | std::views::take(5); // first_five现在包含{1, 2, 3, 4, 5}
views::drop 从Range中丢弃前N个元素。 c++ std::vector<int> numbers = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10}; auto last_five = numbers | std::views::drop(5); // last_five现在包含{6, 7, 8, 9, 10}
views::reverse 反转Range中的元素。 c++ std::vector<int> numbers = {1, 2, 3, 4, 5}; auto reversed_numbers = numbers | std::views::reverse; // reversed_numbers现在包含{5, 4, 3, 2, 1}
views::common 将Range转换为common range。Common range是指begin()和end()返回相同类型的迭代器的Range。有些算法只能处理common range。 “`c++ std::vector numbers = {1, 2, 3, 4, 5}; auto common_numbers = numbers std::views::common; // common_numbers现在是一个common range。
views::iota 生成一个递增的整数序列。 c++ auto numbers = std::views::iota(1, 11); // numbers现在包含{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
views::elements 从包含tuple或pair的Range中提取指定索引的元素。 c++ std::vector<std::pair<int, std::string>> data = {{1, "one"}, {2, "two"}, {3, "three"}}; auto names = data | std::views::elements<1>; // names现在包含{"one", "two", "three"}
views::split 将Range分割成多个子Range,根据指定的分隔符。 c++ std::string text = "hello,world,this,is,a,test"; auto words = text | std::views::split(','); // words现在包含{"hello", "world", "this", "is", "a", "test"}
views::join 将一个包含Range的Range连接成一个Range。 c++ std::vector<std::vector<int>> matrix = {{1, 2}, {3, 4}, {5, 6}}; auto numbers = matrix | std::views::join; // numbers现在包含{1, 2, 3, 4, 5, 6}

四、 常用算法(Actions)

std::ranges也提供了一些常用的算法,可以直接操作Range。这些算法和std::algorithm里的算法类似,但是用法更简洁。

算法 功能 示例
ranges::sort 对Range进行排序。 c++ std::vector<int> numbers = {5, 2, 8, 1, 9, 4}; std::ranges::sort(numbers); // numbers现在包含{1, 2, 4, 5, 8, 9}
ranges::copy 将一个Range复制到另一个Range。 c++ std::vector<int> numbers = {1, 2, 3, 4, 5}; std::vector<int> copy(5); std::ranges::copy(numbers, copy.begin()); // copy现在包含{1, 2, 3, 4, 5}
ranges::find 在Range中查找指定元素。 c++ std::vector<int> numbers = {1, 2, 3, 4, 5}; auto it = std::ranges::find(numbers, 3); if (it != numbers.end()) { std::cout << "Found: " << *it << std::endl; }
ranges::count 统计Range中满足条件的元素个数。 c++ std::vector<int> numbers = {1, 2, 3, 4, 5, 2, 2}; auto count = std::ranges::count(numbers, 2); // count现在是3
ranges::for_each 对Range中的每个元素执行指定操作。 c++ std::vector<int> numbers = {1, 2, 3, 4, 5}; std::ranges::for_each(numbers, [](int n){ std::cout << n << " "; }); // 输出:1 2 3 4 5
ranges::transform 将一个Range转换成另一个Range,并将结果写入到指定的输出Range。 c++ std::vector<int> numbers = {1, 2, 3, 4, 5}; std::vector<int> squared_numbers(5); std::ranges::transform(numbers, squared_numbers.begin(), [](int n){ return n * n; }); // squared_numbers现在包含{1, 4, 9, 16, 25}
ranges::any_of 判断Range中是否存在满足条件的元素。 c++ std::vector<int> numbers = {1, 2, 3, 4, 5}; bool has_even = std::ranges::any_of(numbers, [](int n){ return n % 2 == 0; }); // has_even现在是true
ranges::all_of 判断Range中是否所有元素都满足条件。 c++ std::vector<int> numbers = {2, 4, 6, 8, 10}; bool all_even = std::ranges::all_of(numbers, [](int n){ return n % 2 == 0; }); // all_even现在是true
ranges::none_of 判断Range中是否没有元素满足条件。 c++ std::vector<int> numbers = {1, 3, 5, 7, 9}; bool none_even = std::ranges::none_of(numbers, [](int n){ return n % 2 == 0; }); // none_even现在是true
ranges::remove_if 从Range中移除满足条件的元素。注意:这个算法会改变原Range,并将不满足条件的元素移动到Range的前面,返回指向被移除元素起始位置的迭代器。通常需要结合erase来真正删除元素。 c++ std::vector<int> numbers = {1, 2, 3, 4, 5, 6}; auto it = std::ranges::remove_if(numbers, [](int n){ return n % 2 == 0; }); numbers.erase(it, numbers.end()); // numbers现在包含{1, 3, 5}

五、 实战演练:组合View和Action

光说不练假把式,咱们来几个例子,看看怎么把View和Action组合起来,实现复杂的数据处理。

例子1:找到一个vector中所有偶数的平方,并排序。

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

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

    // 1. 过滤出偶数
    // 2. 计算平方
    // 3. 排序
    auto result = numbers | std::views::filter([](int n){ return n % 2 == 0; })
                          | std::views::transform([](int n){ return n * n; });

    std::vector<int> sorted_result(std::ranges::begin(result), std::ranges::end(result));
    std::ranges::sort(sorted_result);

    // 输出结果
    for (int n : sorted_result) {
        std::cout << n << " ";
    }
    std::cout << std::endl; // 输出:4 16 36 64 100

    return 0;
}

这个例子里,我们用|操作符把views::filterviews::transform连接起来,形成一个View。然后,我们把这个View转换成一个std::vector,并用std::ranges::sort进行排序。

例子2:从一个字符串中提取所有的数字。

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

int main() {
    std::string text = "abc123def456ghi789";

    // 1. 过滤出数字字符
    // 2. 将字符转换为整数
    auto digits = text | std::views::filter([](char c){ return std::isdigit(c); })
                       | std::views::transform([](char c){ return c - '0'; });

    // 将结果保存到vector
    std::vector<int> result(std::ranges::begin(digits), std::ranges::end(digits));

    // 输出结果
    for (int n : result) {
        std::cout << n << " ";
    }
    std::cout << std::endl; // 输出:1 2 3 4 5 6 7 8 9

    return 0;
}

这个例子里,我们用views::filter过滤出数字字符,然后用views::transform将字符转换为整数。

例子3:分割字符串,并统计每个单词的长度。

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

int main() {
    std::string text = "hello world this is a test";

    // 1. 分割字符串成单词
    // 2. 统计每个单词的长度
    auto word_lengths = text | std::views::split(' ')
                            | std::views::transform([](auto word){
                                  // word是一个std::ranges::subrange,需要转换成string
                                  std::string s(word.begin(), word.end());
                                  return s.length();
                              });

    // 将结果保存到vector
    std::vector<size_t> result(std::ranges::begin(word_lengths), std::ranges::end(word_lengths));

    // 输出结果
    for (size_t len : result) {
        std::cout << len << " ";
    }
    std::cout << std::endl; // 输出:5 5 4 2 1 4

    return 0;
}

这个例子里,我们用views::split将字符串分割成单词,然后用views::transform统计每个单词的长度。注意,views::split返回的是一个std::ranges::subrange,我们需要把它转换成std::string才能计算长度。

六、 自定义Range

除了使用标准的Range,我们还可以自定义Range。要自定义Range,需要满足一定的条件,比如提供begin()end()函数,以及定义迭代器类型。

下面是一个简单的自定义Range的例子:

#include <iostream>
#include <ranges>

// 自定义Range:生成一个从start到end的整数序列
class IntRange {
public:
    class iterator {
    public:
        using iterator_category = std::input_iterator_tag;
        using value_type = int;
        using difference_type = std::ptrdiff_t;
        using pointer = int*;
        using reference = int&;

        iterator(int value) : value_(value) {}

        int operator*() const { return value_; }

        iterator& operator++() {
            ++value_;
            return *this;
        }

        bool operator!=(const iterator& other) const {
            return value_ != other.value_;
        }

    private:
        int value_;
    };

    IntRange(int start, int end) : start_(start), end_(end) {}

    iterator begin() const { return iterator(start_); }

    iterator end() const { return iterator(end_); }

private:
    int start_;
    int end_;
};

int main() {
    IntRange numbers(1, 11); // 生成一个从1到10的整数序列

    // 使用std::ranges::for_each遍历Range
    std::ranges::for_each(numbers, [](int n){ std::cout << n << " "; });
    std::cout << std::endl; // 输出:1 2 3 4 5 6 7 8 9 10

    return 0;
}

这个例子里,我们定义了一个IntRange类,它可以生成一个从startend的整数序列。我们还定义了一个iterator类,用于遍历这个序列。

七、 注意事项

  • 编译器支持std::ranges是C++20的特性,需要使用支持C++20的编译器。
  • 性能: 虽然std::ranges通常比手写循环更高效,但也要注意避免过度使用View,导致性能下降。
  • 调试: 调试std::ranges的代码可能会比较困难,因为很多操作都是延迟计算的。

八、 总结

std::ranges是C++20里一个非常强大的特性,它可以让你用更简洁、更声明式的方式操作数据。通过组合View和Action,你可以构建复杂的数据处理流程,提高代码的可读性和可维护性。虽然学习std::ranges需要一些时间,但绝对值得!

希望今天的讲解对你有所帮助。记住,编程的乐趣在于不断学习和尝试!下次再见!

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注