C++ `std::ranges`:C++20 声明式算法与视图组合的高效应用

好的,没问题!让我们开始这场C++20 std::ranges 的狂欢派对!

C++20 std::ranges:声明式算法与视图组合的高效应用

大家好!欢迎来到今天的“C++20 std::ranges:让你的代码更优雅,性能更爆炸”讲座。我是你们的老朋友,一个在代码海洋里摸爬滚打多年的老水手。今天,我们要聊聊C++20中最激动人心的新特性之一:std::ranges

开场白:告别繁琐,拥抱声明式编程

在C++11之前,我们使用STL算法时,总是被迭代器搞得晕头转向。要指定开始、结束位置,还要小心翼翼地避免越界。代码写起来就像在用脚后跟挠痒痒,费劲还不舒服。

C++20的std::ranges就像一把锋利的手术刀,直接切入问题的核心,让我们能够用更声明式的方式编写代码。这意味着我们可以专注于“做什么”,而不是“怎么做”。

ranges 的基本概念:视图、算法和管道

std::ranges 的核心在于三个概念:

  • 视图 (Views): 视图是数据的“窗口”。它们不拥有数据,只是以某种方式转换或过滤底层的数据源。视图是惰性求值的,这意味着它们只在需要时才计算结果。
  • 算法 (Algorithms): 算法是对数据进行操作的函数。std::ranges 提供了许多与 STL 算法对应的 range-based 版本,可以直接作用于 range 对象。
  • 管道 (Pipelines): 管道是将多个视图和算法连接起来,形成一个数据处理流程。这就像乐高积木一样,我们可以将不同的模块组合起来,构建复杂的逻辑。

ranges 的优势:代码更简洁,更易读,更高效

使用 std::ranges 的好处是显而易见的:

  • 代码更简洁: 摆脱了手动迭代器的束缚,代码更加紧凑。
  • 代码更易读: 声明式编程风格让代码的意图更加清晰。
  • 代码更高效: 视图的惰性求值避免了不必要的计算,提高了性能。

ranges 的基本用法:从入门到精通

让我们通过一些示例来了解 std::ranges 的基本用法。

1. 视图 (Views):

视图是 std::ranges 的基石。它们提供了各种数据转换和过滤的方式。

  • std::views::all: 创建一个包含整个 range 的视图。

    #include <iostream>
    #include <vector>
    #include <ranges>
    
    int main() {
        std::vector<int> numbers = {1, 2, 3, 4, 5};
        auto all_view = std::views::all(numbers);
    
        for (int number : all_view) {
            std::cout << number << " ";
        }
        std::cout << std::endl; // 输出:1 2 3 4 5
    
        return 0;
    }
  • std::views::take: 从 range 中获取前 N 个元素。

    #include <iostream>
    #include <vector>
    #include <ranges>
    
    int main() {
        std::vector<int> numbers = {1, 2, 3, 4, 5};
        auto take_view = std::views::take(numbers, 3); // 获取前3个元素
    
        for (int number : take_view) {
            std::cout << number << " ";
        }
        std::cout << std::endl; // 输出:1 2 3
    
        return 0;
    }
  • std::views::drop: 从 range 中删除前 N 个元素。

    #include <iostream>
    #include <vector>
    #include <ranges>
    
    int main() {
        std::vector<int> numbers = {1, 2, 3, 4, 5};
        auto drop_view = std::views::drop(numbers, 2); // 删除前2个元素
    
        for (int number : drop_view) {
            std::cout << number << " ";
        }
        std::cout << std::endl; // 输出:3 4 5
    
        return 0;
    }
  • std::views::filter: 根据条件过滤 range 中的元素。

    #include <iostream>
    #include <vector>
    #include <ranges>
    
    int main() {
        std::vector<int> numbers = {1, 2, 3, 4, 5, 6};
        auto even_view = std::views::filter(numbers, [](int n) { return n % 2 == 0; }); // 过滤偶数
    
        for (int number : even_view) {
            std::cout << number << " ";
        }
        std::cout << std::endl; // 输出:2 4 6
    
        return 0;
    }
  • std::views::transform: 将 range 中的每个元素转换为另一个值。

    #include <iostream>
    #include <vector>
    #include <ranges>
    
    int main() {
        std::vector<int> numbers = {1, 2, 3, 4, 5};
        auto square_view = std::views::transform(numbers, [](int n) { return n * n; }); // 计算平方
    
        for (int number : square_view) {
            std::cout << number << " ";
        }
        std::cout << std::endl; // 输出:1 4 9 16 25
    
        return 0;
    }
  • std::views::reverse: 反转range中的元素。

    #include <iostream>
    #include <vector>
    #include <ranges>
    
    int main() {
        std::vector<int> numbers = {1, 2, 3, 4, 5};
        auto reversed_view = std::views::reverse(numbers);
    
        for (int number : reversed_view) {
            std::cout << number << " ";
        }
        std::cout << std::endl; // 输出:5 4 3 2 1
    
        return 0;
    }
  • std::views::iota: 创建一个递增的整数序列。

    #include <iostream>
    #include <ranges>
    
    int main() {
        auto iota_view = std::views::iota(1, 6); // 生成 1, 2, 3, 4, 5
    
        for (int number : iota_view) {
            std::cout << number << " ";
        }
        std::cout << std::endl; // 输出:1 2 3 4 5
    
        return 0;
    }

2. 算法 (Algorithms):

std::ranges 提供了许多与 STL 算法对应的 range-based 版本。 这些算法可以直接作用于 range 对象,而无需手动指定迭代器。

  • std::ranges::for_each: 对 range 中的每个元素执行操作。

    #include <iostream>
    #include <vector>
    #include <ranges>
    #include <algorithm>
    
    int main() {
        std::vector<int> numbers = {1, 2, 3, 4, 5};
        std::ranges::for_each(numbers, [](int n) { std::cout << n << " "; });
        std::cout << std::endl; // 输出:1 2 3 4 5
    
        return 0;
    }
  • std::ranges::count: 计算 range 中满足条件的元素个数。

    #include <iostream>
    #include <vector>
    #include <ranges>
    #include <algorithm>
    
    int main() {
        std::vector<int> numbers = {1, 2, 3, 2, 4, 2, 5};
        int count = std::ranges::count(numbers, 2); // 计算 2 的个数
        std::cout << "Count of 2: " << count << std::endl; // 输出:Count of 2: 3
    
        return 0;
    }
  • std::ranges::sort: 对 range 中的元素进行排序。

    #include <iostream>
    #include <vector>
    #include <ranges>
    #include <algorithm>
    
    int main() {
        std::vector<int> numbers = {5, 2, 1, 4, 3};
        std::ranges::sort(numbers); // 排序
    
        for (int number : numbers) {
            std::cout << number << " ";
        }
        std::cout << std::endl; // 输出:1 2 3 4 5
    
        return 0;
    }
  • std::ranges::copy: 将 range 中的元素复制到另一个 range。

    #include <iostream>
    #include <vector>
    #include <ranges>
    #include <algorithm>
    
    int main() {
        std::vector<int> source = {1, 2, 3, 4, 5};
        std::vector<int> destination(source.size());
        std::ranges::copy(source, destination.begin()); // 复制
    
        for (int number : destination) {
            std::cout << number << " ";
        }
        std::cout << std::endl; // 输出:1 2 3 4 5
    
        return 0;
    }
  • std::ranges::find: 在range中查找指定的元素。

    #include <iostream>
    #include <vector>
    #include <ranges>
    #include <algorithm>
    
    int main() {
        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; // 输出:Found: 3
        } else {
            std::cout << "Not found" << std::endl;
        }
        return 0;
    }

3. 管道 (Pipelines):

管道是将多个视图和算法连接起来,形成一个数据处理流程。 使用管道操作符 | 可以方便地将不同的视图和算法组合在一起。

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

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

    // 找到偶数,然后将它们平方,最后取出前 3 个
    auto pipeline = numbers
                    | std::views::filter([](int n) { return n % 2 == 0; })
                    | std::views::transform([](int n) { return n * n; })
                    | std::views::take(3);

    for (int number : pipeline) {
        std::cout << number << " ";
    }
    std::cout << std::endl; // 输出:4 16 36

    return 0;
}

在这个例子中,我们首先使用 std::views::filter 过滤出偶数,然后使用 std::views::transform 计算它们的平方,最后使用 std::views::take 取出前 3 个元素。整个过程通过管道操作符 | 连接起来,代码非常简洁易懂。

高级技巧:自定义视图

除了标准库提供的视图外,我们还可以自定义视图,以满足特定的需求。 自定义视图需要实现 rangeviewable_range 概念。

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

// 自定义视图:将每个元素乘以一个系数
template <typename Range, typename T>
class MultiplyView : public std::ranges::view_interface<MultiplyView<Range, T>> {
private:
    Range base_;
    T factor_;

public:
    MultiplyView(Range base, T factor) : base_(std::move(base)), factor_(factor) {}

    auto begin() {
        return std::ranges::begin(base_);
    }

    auto end() {
        return std::ranges::end(base_);
    }

    auto begin() const requires std::ranges::range<const Range> {
        return std::ranges::begin(base_);
    }

    auto end() const requires std::ranges::range<const Range> {
        return std::ranges::end(base_);
    }

    friend auto tag_invoke(std::ranges::tag::data, const MultiplyView& self) {
        return self.factor_;
    }

    auto operator[](size_t n) const
    requires std::ranges::random_access_range<Range> && std::ranges::sized_range<Range> {
        return base_[n] * factor_;
    }

    template <std::ranges::input_range R>
    friend MultiplyView multiply(R&& r, T factor) {
        return MultiplyView(std::forward<R>(r), factor);
    }

    //迭代器
    class iterator{
    private:
        using BaseIter = std::ranges::iterator_t<Range>;
        BaseIter current;
        T factor;

    public:
        using iterator_category = std::ranges::iterator_concept<BaseIter>;
        using value_type = decltype(*current * factor);
        using difference_type = std::ranges::difference_type_t<Range>;
        using pointer = value_type*;
        using reference = value_type;

        iterator(BaseIter cur, T factor) : current(cur), factor(factor) {}

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

        iterator operator++(int) {
            iterator temp = *this;
            ++(*this);
            return temp;
        }

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

        bool operator!=(const iterator& other) const {
            return !(*this == other);
        }

        value_type operator*() const {
            return *current * factor;
        }
    };

    iterator begin() {
        return iterator(std::ranges::begin(base_), factor_);
    }

    iterator end() {
        return iterator(std::ranges::end(base_), factor_);
    }

};

template <std::ranges::input_range R, typename T>
MultiplyView(R&& r, T factor) -> MultiplyView<std::views::all_t<R>, T>;

namespace std::ranges {
    template <class R, class T>
    inline constexpr bool enable_borrowed_range<MultiplyView<R, T>> = enable_borrowed_range<R>;
}

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

    // 使用自定义视图将每个元素乘以 2
    auto multiplied_view = MultiplyView(numbers, 2);

    for (int number : multiplied_view) {
        std::cout << number << " ";
    }
    std::cout << std::endl; // 输出:2 4 6 8 10

    // 也可以将自定义视图与其他视图组合使用
    auto pipeline = numbers
                    | MultiplyView(2)
                    | std::views::filter([](int n) { return n % 4 == 0; });

    for (int number : pipeline) {
        std::cout << number << " ";
    }
    std::cout << std::endl; // 输出:4 8

    return 0;
}

注意事项和最佳实践

  • 避免过度使用管道: 虽然管道很方便,但过度使用可能会导致代码难以调试和维护。
  • 选择合适的视图: 不同的视图有不同的性能特点,选择合适的视图可以提高代码的效率。
  • 理解惰性求值: 视图是惰性求值的,这意味着它们只在需要时才计算结果。这可以避免不必要的计算,但也要注意避免副作用。
  • 注意所有权问题: 视图不拥有数据,因此在使用视图时要确保底层数据源的生命周期足够长。

ranges 与性能

std::ranges 的惰性求值特性可以显著提高性能,尤其是在处理大型数据集时。通过避免不必要的计算,ranges 可以减少内存占用和 CPU 时间。

总结

std::ranges 是 C++20 中一个强大的工具,可以帮助我们编写更简洁、更易读、更高效的代码。 掌握 std::ranges 的基本概念和用法,可以让我们在 C++ 编程中更加游刃有余。

最后的忠告

std::ranges 就像一把双刃剑,用得好可以事半功倍,用不好可能会适得其反。 因此,在使用 std::ranges 时,一定要深入理解其原理,并根据实际情况选择合适的用法。

好了,今天的讲座就到这里。 感谢大家的参与! 希望大家能够喜欢 std::ranges,并将其应用到自己的项目中。 记住,代码的优雅和性能同样重要!祝大家编码愉快!

发表回复

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