رفتن به محتوا

بازه‌های ‎C++‎20: پارادایمی جدید

بازه‌های ‎C++‎20 را کشف کنید — بازطراحی مدرن رابط الگوریتم STL. مفاهیم بازه، الگوریتم‌های مبتنی بر بازه، تصویرسازی‌ها و عملگر لوله برای تبدیل‌های داده ترکیب‌پذیر را بیاموزید.

چرا بازه‌ها؟

بازه‌های ‎C++‎20 (Ranges) یک ارتقای بنیادین به مدل الگوریتم STL هستند. STL سنتی نیاز به ارسال جفت تکرارگر به هر الگوریتم دارد، که طولانی و مستعد خطاست (ممکن است به طور تصادفی تکرارگرهایی از کانتینرهای مختلف ارسال کنید). بازه‌ها این مشکل را با اجازه ارسال خود کانتینر — یا هر شیئی که begin() و end() تعریف کند — حل می‌کنند.

بهبودهای کلیدی نسبت به الگوریتم‌های سنتی:

1. ارسال مستقیم کانتینرها — std::ranges::sort(vec) به جای std::sort(vec.begin(), vec.end())
2. تصویرسازی‌ها (Projections) — تبدیل عناصر قبل از مقایسه، بدون تغییر آن‌ها
3. مفاهیم (Concepts) — الگوریتم‌ها الزامات زمان کامپایل واضحی دارند
4. ترکیب‌پذیری — زنجیره کردن عملیات با عملگر لوله |
5. ارزیابی تنبل — نماها عناصر را بر حسب تقاضا محاسبه می‌کنند و از کپی‌های میانی جلوگیری می‌کنند

مفاهیم بازه

‎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 — قابل تبدیل به نما

این مفاهیم برای محدود کردن الگوریتم‌های مبتنی بر بازه استفاده می‌شوند و پیام‌های خطای واضحی هنگام ارسال بازه ناسازگار به شما می‌دهند.

الگوریتم‌های مبتنی بر بازه

الگوریتم‌های مبتنی بر بازه در std::ranges:: قرار دارند و کانتینرهای کامل را می‌پذیرند. همچنین نوع‌های نتیجه غنی‌تری به جای تکرارگرهای ساده برمی‌گردانند.

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) ویژگی منحصربه‌فرد الگوریتم‌های مبتنی بر بازه هستند. به شما اجازه می‌دهند نحوه «دیدن» هر عنصر برای مقایسه یا پردازش را مشخص کنید، بدون تغییر خود عنصر.

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
}

عملگر لوله برای ترکیب

عملگر لوله | نشانه بارز بازه‌های ‎C++‎20 است. به شما اجازه می‌دهد آداپتورهای بازه (نماها) را در یک خط لوله زنجیره‌ای کنید که از چپ به راست خوانده می‌شود، مانند لوله پوسته یونیکس:

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

این به طور طبیعی خوانده می‌شود: «اعداد را بگیر، زوج‌ها را نگه دار، مربع کن، ۵ تای اول را بگیر.» خط لوله تنبل است — هیچ محاسبه‌ای تا زمانی که روی result پیمایش نکنید انجام نمی‌شود. این بدان معناست که هیچ vectorمیانی تخصیص داده نمی‌شود.

نماها را در درس بعدی به طور عمیق بررسی خواهیم کرد.

دام

الگوریتم‌های بازه از تکرارگرهای آویزان از بازه‌های موقت محافظت می‌کنند. اگر یک کانتینر موقت ارسال کنید، تکرارگر بازگشتی آویزان خواهد بود زیرا موقت در نقطه‌ویرگول نابود می‌شود:

auto it = std::ranges::find(get_vector(), 42);  
// Returns std::ranges::dangling instead of an iterator!
// Attempting to dereference it won't compile.

این یک ویژگی ایمنی زمان کامپایل است — std::find سنتی بی‌صدا یک تکرارگر آویزان برمی‌گرداند که در زمان اجرا به رفتار نامشخص منجر می‌شود. نوع نگهبان std::ranges::dangling این را به یک خطای کامپایل تبدیل می‌کند.

بهترین شیوه

در کد جدید الگوریتم‌های std::ranges:: را ترجیح دهید — آن‌ها امن‌تر (بدون تکرارگرهای ناهمخوان، حفاظت از آویزان)، خواناتر (ارسال مستقیم کانتینرها) و قدرتمندتر (تصویرسازی‌ها، مفاهیم) هستند.

از الگوریتم‌های سنتی std:: استفاده کنید وقتی:
- کدتان کامپایلرهای قبل از ‎C++‎20 را هدف قرار می‌دهد
- به یک بارگذاری خاص نیاز دارید که ranges ارائه نمی‌دهد
- با جفت تکرارگرهای APIهای قدیمی کار می‌کنید

توجه: الگوریتم‌های std::ranges:: جایگزین‌های مستقیم نیستند. برخی نوع‌های متفاوتی برمی‌گردانند (مثلاً std::ranges::sort تکرارگر انتهای بازه مرتب‌شده را برمی‌گرداند). هنگام مهاجرت مستندات را بررسی کنید.

نکات کلیدی
  • بازه‌های ‎C++‎20 به شما اجازه می‌دهند کانتینرها را مستقیماً به الگوریتم‌ها ارسال کنید — دیگر خبری از کد اضافی begin()/end() نیست
  • تصویرسازی‌ها عناصر را برای مقایسه تبدیل می‌کنند بدون تغییر داده اصلی
  • مفاهیم بازه (input_range، random_access_range و غیره) محدودیت‌های واضح زمان کامپایل فراهم می‌کنند
  • عملگر لوله | خطوط لوله پردازش داده ترکیب‌پذیر از چپ به راست را ممکن می‌سازد
  • std::ranges::dangling باگ‌های تکرارگر آویزان را در زمان کامپایل جلوگیری می‌کند
  • در کد جدید ‎C++‎20 الگوریتم‌های std::ranges:: را برای ایمنی، خوانایی و قدرت ترجیح دهید

آزمون — دانش خود را بسنجید

(20 امتیاز)

1. تصویرسازی در زمینه الگوریتم‌های بازه ‎C++‎20 چیست؟

2. وقتی `std::ranges::find` با کانتینر موقت فراخوانی شود چه برمی‌گرداند؟

3. `std::ranges::sort(vec, std::ranges::greater{}, &Employee::salary)` چه می‌کند؟