انتقل إلى المحتوى

نطاقات C++20: نموذج جديد

اكتشف نطاقات C++20 — إعادة تصميم حديثة لواجهة خوارزمية STL. تعرف على مفاهيم النطاق والخوارزميات المستندة إلى النطاق والإسقاطات ومشغل الأنابيب لتحويلات البيانات القابلة للتركيب.

لماذا النطاقات؟

تعد نطاقات C++20 بمثابة ترقية أساسية لنموذج خوارزمية STL. تتطلب STL التقليدية تمرير أزواج التكرارات إلى كل خوارزمية، وهو أمر مطول وعرضة للخطأ (يمكنك تمرير التكرارات عن طريق الخطأ من حاويات مختلفة). تعمل النطاقات على حل هذه المشكلة من خلال السماح لك بتمرير الحاوية نفسها — أو أي كائن يحددbegin()وend().

التحسينات الرئيسية على الخوارزميات التقليدية:

1. تمرير الحاويات مباشرةً —std::ranges::sort(vec)بدلاً منstd::sort(vec.begin(), vec.end())
2. الإسقاطات — تحويل العناصر قبل المقارنة، دون تعديلها
3. المفاهيم — للخوارزميات متطلبات واضحة لوقت الترجمة
4. القابلية للتركيب — عمليات السلسلة مع مشغل الأنابيب|
5. التقييم البطيء — يتم عرض العناصر الحسابية عند الطلب، مع تجنب النسخ الوسيطة

Range Concepts

يحدد C++20 التسلسل الهرمي لمفاهيم النطاق في:

- std::ranges::range — يحتوي علىbegin()وend()
- std::ranges::sized_range — يحتوي علىsize()في O(1)
- std::ranges::input_range — يمكن قراءتها مرة واحدة (مثل مكررات الإدخال)
- std::ranges::forward_range — اجتياز للأمام متعدد التمريرات
- std::ranges::bidirectional_range — للأمام + للخلف
- std::ranges::random_access_range — قفزات ثنائية الاتجاه + O(1).
- std::ranges::contiguous_range — وصول عشوائي + ذاكرة متجاورة
- std::ranges::common_range — يُرجعbegin()وend()نفس النوع
- std::ranges::viewable_range — يمكن تحويله إلى طريقة عرض

تُستخدم هذه المفاهيم لتقييد الخوارزميات المستندة إلى النطاق، مما يوفر لك رسائل خطأ واضحة عند تمرير نطاق غير متوافق.

Range-Based Algorithms

Range-based algorithms live instd::ranges::and accept entire containers. They also return richer result types instead of bare iterators.

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';
    }
}

Projections — Transform Before Comparing

Projections are a unique feature of range-based algorithms. They let you specify how to "view" each element for comparison or processing, without modifying the element itself.

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
}

The Pipe Operator for Composition

The pipe operator|is the hallmark of C++20 ranges. It lets you chain range adaptors (views) into a pipeline that reads left-to-right, like a Unix shell pipe:

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 result. This means no intermediate vectors are allocated.

We will explore views in depth in the next lesson.

خطأ شائع

Range algorithms protect against dangling iterators from temporary ranges. If you pass a temporary container, the returned iterator would dangle because the temporary is destroyed at the semicolon:

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 sentinel type makes this a compile error.

أفضل ممارسة

Preferstd::ranges::algorithms in new code — they are safer (no mismatched iterators, dangling protection), more readable (pass containers directly), and more powerful (projections, concepts).

Use traditionalstd::algorithms when:
- Your codebase targets pre-C++20 compilers
- You need a specific overload that ranges don't provide
- You work with iterator pairs from legacy APIs

Note:std::ranges::algorithms are not drop-in replacements. Some return different types (e.g.,std::ranges::sortreturns the end iterator of the sorted range). Check the documentation when migrating.

النقاط الرئيسية
  • C++20 ranges let you pass containers directly to algorithms — no morebegin()/end()boilerplate
  • Projections transform elements for comparison without modifying the original data
  • Range concepts (input_range,random_access_range, etc.) provide clear compile-time constraints
  • The pipe operator|enables composable, left-to-right data processing pipelines
  • std::ranges::danglingprevents dangling iterator bugs at compile time
  • Preferstd::ranges::algorithms in new C++20 code for safety, readability, and power

اختبار — اختبر معرفتك

(20 نقطة خبرة)

1. What is a projection in the context of C++20 range algorithms?

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

3. ماذا يفعل`std::ranges::sort(vec, std::ranges::greater{}, &Employee::salary)`؟