Chuyển đến nội dung

C++20 Ranges: A New Paradigm

Discover C++20 ranges — a modern redesign of the STL algorithm interface. Learn range concepts, range-based algorithms, projections, and the pipe operator for composable data transformations.

Tại sao lại có dãy?

Dãy C++20 là bản nâng cấp cơ bản cho mô hình thuật toán của STL. STL truyền thống yêu cầu chuyển cặp vòng lặp cho mọi thuật toán, điều này dài dòng và dễ xảy ra lỗi (bạn có thể vô tình chuyển các vòng lặp từ các vùng chứa khác nhau). Các phạm vi giải quyết vấn đề này bằng cách cho phép bạn chuyển chính vùng chứa — hoặc bất kỳ đối tượng nào xác địnhbegin()vàend().

Những cải tiến chính so với các thuật toán truyền thống:

1. Chuyển vùng chứa trực tiếp —std::ranges::sort(vec)thay vìstd::sort(vec.begin(), vec.end())
2. Phép chiếu — biến đổi các phần tử trước khi so sánh mà không sửa đổi chúng
3. Khái niệm — thuật toán có yêu cầu rõ ràng về thời gian biên dịch
4. Khả năng kết hợp — vận hành chuỗi với toán tử đường ống|
5. Đánh giá lười biếng — lượt xem tính toán các phần tử theo yêu cầu, tránh các bản sao trung gian

Khái niệm phạm vi

C++20 xác định hệ thống phân cấp các khái niệm phạm vi trong:

- std::ranges::range — cóbegin()vàend()
- std::ranges::sized_range — cósize()trong O(1)
- std::ranges::input_range — có thể đọc một lần (như các trình vòng lặp đầu vào)
- std::ranges::forward_range — truyền tải về phía trước nhiều đường chuyền
- std::ranges::bidirectional_range — tiến + lùi
- std::ranges::random_access_range — nhảy hai chiều + O(1)
- std::ranges::contiguous_range — truy cập ngẫu nhiên + bộ nhớ liền kề
- std::ranges::common_range —begin()vàend()trả về cùng loại
- std::ranges::viewable_range — có thể được chuyển đổi thành dạng xem

Các khái niệm này được sử dụng để hạn chế các thuật toán dựa trên phạm vi, cung cấp cho bạn thông báo lỗi rõ ràng khi bạn vượt qua một phạm vi không tương thích.

Thuật toán dựa trên phạm vi

Các thuật toán dựa trên phạm vi tồn tại trongstd::ranges::và chấp nhận toàn bộ vùng chứa. Chúng cũng trả về các loại kết quả phong phú hơn thay vì các trình vòng lặp đơn giản.

range_algorithms.cpp
#include <algorithm>
#include <ranges>
#include <vector>
#include <string>
#include <iostream>

struct Employee {
    std::string name;
    int age;
    double salary;
};

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

    // Pass the container directly — no begin/end needed
    std::ranges::sort(nums);
    // nums: {1, 1, 2, 3, 4, 5, 5, 6, 9}

    // find returns a borrowed iterator (safe with temporary ranges)
    auto it = std::ranges::find(nums, 4);
    if (it != nums.end()) {
        std::cout << "Found: " << *it << '\n';
    }

    // count_if with ranges
    auto even_count = std::ranges::count_if(nums, [](int x) { return x % 2 == 0; });
    std::cout << "Even count: " << even_count << '\n';  // 3

    // min/max on entire range
    auto [min_it, max_it] = std::ranges::minmax_element(nums);
    std::cout << "Min: " << *min_it << ", Max: " << *max_it << '\n';

    // Sorting employees by salary (descending)
    std::vector<Employee> team = {
        {"Alice", 30, 95000},
        {"Bob", 25, 72000},
        {"Charlie", 35, 110000},
    };
    std::ranges::sort(team, std::ranges::greater{}, &Employee::salary);
    // Charlie (110k), Alice (95k), Bob (72k)

    for (const auto& e : team) {
        std::cout << e.name << ": $" << e.salary << '\n';
    }
}

Phép chiếu - Chuyển đổi trước khi so sánh

Phép chiếu là một tính năng độc đáo của thuật toán dựa trên phạm vi. Chúng cho phép bạn chỉ định cách "xem" từng phần tử để so sánh hoặc xử lý mà không cần sửa đổi chính phần tử đó.

projections.cpp
#include <algorithm>
#include <ranges>
#include <vector>
#include <string>
#include <iostream>
#include <cctype>

int main() {
    // Sort strings case-insensitively using a projection
    std::vector<std::string> words = {"Banana", "apple", "Cherry", "date"};

    std::ranges::sort(words, {}, [](const std::string& s) {
        std::string lower = s;
        std::transform(lower.begin(), lower.end(), lower.begin(),
                       [](char c) { return std::tolower(c); });
        return lower;
    });
    // words: {"apple", "Banana", "Cherry", "date"}
    // Original strings preserved, sorted by lowercase version

    for (const auto& w : words) std::cout << w << ' ';
    std::cout << '\n';

    // Using member pointer as projection
    struct Point { int x, y; };
    std::vector<Point> points = {{3, 1}, {1, 5}, {2, 3}};

    // Sort by x coordinate
    std::ranges::sort(points, {}, &Point::x);
    // points: {{1,5}, {2,3}, {3,1}}

    // Find max by y coordinate
    auto max_y = std::ranges::max_element(points, {}, &Point::y);
    std::cout << "Max y: (" << max_y->x << ", " << max_y->y << ")\n";  // (1, 5)

    // Binary search with projection
    bool found = std::ranges::binary_search(points, 2, {}, &Point::x);
    std::cout << std::boolalpha << "Has x=2: " << found << '\n';  // true
}

Toán tử ống cho thành phần

Bộ điều khiển đường ống|là đặc điểm nổi bật của dòng sản phẩm C++20. Nó cho phép bạn xâu chuỗi các bộ điều hợp phạm vi (chế độ xem) thành một đường dẫn đọc từ trái sang phải, giống như một đường dẫn shell Unix:

auto result = numbers
 | std::views::filter([](int x) { return x % 2 == 0; })
 | std::views::transform([](int x) { return x * x; })
 | std::views::take(5);

This reads naturally: "take numbers, keep the even ones, square them, take the first 5." The pipeline is lazy — no computation happens until you iterate over kết quả. Điều này có nghĩa là không có vectơ trung gian nào được phân bổ.

Chúng ta sẽ tìm hiểu sâu hơn về các quan điểm trong bài học tiếp theo.

Lỗi thường gặp

Các thuật toán phạm vi bảo vệ khỏi các vòng lặp treo lủng lẳng từ các phạm vi tạm thời. Nếu bạn chuyển một vùng chứa tạm thời, trình vòng lặp được trả về sẽ treo lủng lẳng vì vùng chứa tạm thời bị hủy ở dấu chấm phẩy:

Loại trọng điểm ``cpp
auto it = std::ranges::find(get_vector(), 42);
// Returns std::ranges::dangling instead of an iterator!
// Attempting to dereference it won't compile.
``

This is a compile-time safety feature — traditional std::find would silently return a dangling iterator, leading to undefined behavior at runtime. The std::ranges::dangling khiến đây là lỗi biên dịch.

Thực hành tốt

Ưu tiên các thuật toánstd::ranges::trong mã mới - chúng an toàn hơn (không có các vòng lặp không khớp, bảo vệ lơ lửng), dễ đọc hơn (chuyển trực tiếp các vùng chứa) và mạnh mẽ hơn (các phép chiếu, khái niệm).

Sử dụng thuật toánstd::truyền thống khi:
- Cơ sở mã của bạn nhắm mục tiêu các trình biên dịch trước C++20
- Bạn cần một mức quá tải cụ thể mà phạm vi không cung cấp
- Bạn làm việc với các cặp trình vòng lặp từ các API cũ

Lưu ý: Các thuật toánstd::ranges::không là các thuật toán thay thế tùy ý. Một số trả về các loại khác nhau (ví dụ:std::ranges::sorttrả về trình lặp cuối cùng của dải ô được sắp xếp). Kiểm tra tài liệu khi di chuyển.

Điểm chính
  • Phạm vi C++20 cho phép bạn chuyển trực tiếp các vùng chứa tới các thuật toán - không còn bản soạn sẵnbegin()/end()nữa
  • Phép chiếu biến đổi các phần tử để so sánh mà không sửa đổi dữ liệu gốc
  • Các khái niệm phạm vi (input_range,random_access_range, v.v.) cung cấp các ràng buộc về thời gian biên dịch rõ ràng
  • Toán tử đường ống|cho phép các đường ống xử lý dữ liệu có thể tổng hợp từ trái sang phải
  • std::ranges::danglingngăn chặn các lỗi lặp treo lơ lửng trong thời gian biên dịch
  • Ưu tiên các thuật toánstd::ranges::trong mã C++20 mới để đảm bảo an toàn, dễ đọc và hiệu quả

Câu đố — Kiểm tra kiến thức

(20 XP)

1. Phép chiếu trong bối cảnh thuật toán phạm vi C++20 là gì?

2. What does`std::ranges::find`return when called with a temporary container?

3. What does`std::ranges::sort(vec, std::ranges::greater{}, &Employee::salary)`do?