رفتن به محتوا

نماها، ارزیابی تنبل و ترکیب

نماهای ‎C++‎20 را فرا بگیرید — آداپتورهای بازه سبک و تنبل که با عملگر لوله ترکیب می‌شوند. filter، transform، take، drop، split، join، iota و نحوه ساخت خطوط لوله داده قدرتمند بدون تخصیص‌های میانی را بیاموزید.

نماها چیستند؟

یک نما (View) یک بازه سبک است که:

1. غیرمالک است — مالک عناصری که به آن‌ها ارجاع می‌دهد نیست
2. کپی و انتقال O(1) — نماها ارسالشان ارزان است
3. ارزیابی تنبل — عناصر بر حسب تقاضا در هنگام پیمایش محاسبه می‌شوند، نه از قبل

نماها بلوک‌های سازنده خطوط لوله بازه هستند. به جای ایجاد کانتینرهای میانی در هر مرحله، نماها در یک گذر واحد روی داده ترکیب می‌شوند:

// Traditional: creates 2 intermediate vectors
auto evens = filter(data, is_even);     // allocation
auto squared = transform(evens, sq);    // allocation

// Views: zero intermediate allocations
auto result = data | views::filter(is_even) | views::transform(sq);
// Nothing computed yet — result is just a view descriptor
for (int x : result) { ... }  // computed element-by-element

نماها در std::views:: (نام مستعار فضای نام برای std::ranges::views::) قرار دارند.

filter و transform — نماهای بنیادی

views::filter عناصر مطابق با یک محمول را نگه می‌دارد. views::transform یک تابع را روی هر عنصر اعمال می‌کند. این دو با هم اکثریت حلقه‌های صریح را جایگزین می‌کنند.

filter_transform.cpp
#include <ranges>
#include <vector>
#include <string>
#include <iostream>

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

    // Filter even numbers, then square them
    auto even_squares = numbers
        | std::views::filter([](int x) { return x % 2 == 0; })
        | std::views::transform([](int x) { return x * x; });

    // Nothing has been computed yet — even_squares is a view
    for (int x : even_squares) {
        std::cout << x << ' ';  // 4 16 36 64 100
    }
    std::cout << '\n';

    // Views work with any range — including strings
    std::string text = "Hello, World!";
    auto uppercase = text
        | std::views::filter([](char c) { return std::isalpha(c); })
        | std::views::transform([](char c) { return std::toupper(c); });

    for (char c : uppercase) {
        std::cout << c;  // HELLOWORLD
    }
    std::cout << '\n';

    // Collect view results into a vector (C++23: std::ranges::to)
    std::vector<int> collected;
    for (int x : even_squares) {
        collected.push_back(x);
    }
    // C++23: auto collected = even_squares | std::ranges::to<std::vector>();
}

take، drop و نماهای برش

views::take(n) اولین n عنصر را تولید می‌کند. views::drop(n) اولین n عنصر را رد می‌کند. با هم برش کارآمد بدون کپی را ممکن می‌سازند.

take_drop.cpp
#include <ranges>
#include <vector>
#include <iostream>

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

    // take — first N elements
    for (int x : data | std::views::take(3)) {
        std::cout << x << ' ';  // 1 2 3
    }
    std::cout << '\n';

    // drop — skip first N elements
    for (int x : data | std::views::drop(7)) {
        std::cout << x << ' ';  // 8 9 10
    }
    std::cout << '\n';

    // Combine: elements 3-7 (0-indexed: drop 3, take 4)
    for (int x : data | std::views::drop(3) | std::views::take(4)) {
        std::cout << x << ' ';  // 4 5 6 7
    }
    std::cout << '\n';

    // take_while / drop_while — predicate-based
    std::vector<int> sorted_data = {1, 3, 5, 7, 2, 4, 6};

    auto ascending_prefix = sorted_data | std::views::take_while(
        [prev = 0](int x) mutable {
            bool ok = x > prev;
            prev = x;
            return ok;
        });
    for (int x : ascending_prefix) {
        std::cout << x << ' ';  // 1 3 5 7
    }
    std::cout << '\n';

    // Pagination example: page 2, page_size = 3
    int page = 2, page_size = 3;
    auto page_view = data
        | std::views::drop((page - 1) * page_size)
        | std::views::take(page_size);
    for (int x : page_view) {
        std::cout << x << ' ';  // 4 5 6
    }
    std::cout << '\n';
}

split، join و نماهای پردازش رشته

views::split یک بازه را با جداکننده تقسیم می‌کند. views::join بازه‌های تودرتو را مسطح می‌کند. این‌ها برای پردازش رشته و داده قدرتمند هستند.

split_join.cpp
#include <ranges>
#include <string>
#include <string_view>
#include <vector>
#include <iostream>

int main() {
    // split — divide a string by delimiter
    std::string csv = "Alice,30,Engineer,95000";

    for (auto field : csv | std::views::split(',')) {
        // Each 'field' is a subrange — convert to string_view
        std::string_view sv(field.begin(), field.end());
        std::cout << '[' << sv << "] ";
    }
    std::cout << '\n';  // [Alice] [30] [Engineer] [95000]

    // join — flatten nested ranges
    std::vector<std::vector<int>> matrix = {
        {1, 2, 3},
        {4, 5, 6},
        {7, 8, 9}
    };
    for (int x : matrix | std::views::join) {
        std::cout << x << ' ';  // 1 2 3 4 5 6 7 8 9
    }
    std::cout << '\n';

    // Combine: split lines, then process each line
    std::string multiline = "hello world\nfoo bar\nbaz qux";
    int line_num = 1;
    for (auto line : multiline | std::views::split('\n')) {
        std::string_view line_sv(line.begin(), line.end());
        std::cout << line_num++ << ": " << line_sv << '\n';
    }

    // Enumerate (C++23)
    // for (auto [i, val] : data | std::views::enumerate) { ... }
}

views::iota — تولید بازه‌ها

views::iota دنباله‌ای از مقادیر افزایشی تولید می‌کند. می‌تواند محدود (iota(0, 10) برای ۰ تا ۹) یا نامحدود (iota(0) برای ۰، ۱، ۲، ...) باشد. با ترکیب take، بسیاری از حلقه‌های شمارشی را جایگزین می‌کند.

views_iota.cpp
#include <ranges>
#include <iostream>
#include <vector>
#include <numeric>

int main() {
    // Bounded iota — replaces for(int i = 0; i < 10; ++i)
    for (int x : std::views::iota(0, 10)) {
        std::cout << x << ' ';  // 0 1 2 3 4 5 6 7 8 9
    }
    std::cout << '\n';

    // Unbounded iota + take — first 5 squares
    auto first_5_squares = std::views::iota(1)
        | std::views::transform([](int x) { return x * x; })
        | std::views::take(5);
    for (int x : first_5_squares) {
        std::cout << x << ' ';  // 1 4 9 16 25
    }
    std::cout << '\n';

    // Generate Fibonacci-like sequence with views
    // (iota as index generator)
    auto indices = std::views::iota(0, 20);
    auto even_indices = indices | std::views::filter([](int i) { return i % 2 == 0; });
    for (int i : even_indices | std::views::take(5)) {
        std::cout << i << ' ';  // 0 2 4 6 8
    }
    std::cout << '\n';

    // Use iota to create a vector of sequential values
    auto range_vec = std::views::iota(1, 11);  // 1 to 10
    int sum = 0;
    for (int x : range_vec) sum += x;
    std::cout << "Sum 1..10: " << sum << '\n';  // 55
}

ساخت خطوط لوله داده کامل

قدرت واقعی نماها وقتی آشکار می‌شود که چندین آداپتور را در یک خط لوله واحد ترکیب کنید. کل خط لوله به صورت تنبل در یک گذر واحد ارزیابی می‌شود.

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

struct LogEntry {
    std::string level;   // "INFO", "WARN", "ERROR"
    std::string message;
    int timestamp;
};

int main() {
    std::vector<LogEntry> logs = {
        {"INFO",  "Server started",      1000},
        {"ERROR", "Connection refused",  1001},
        {"INFO",  "Request received",    1002},
        {"WARN",  "Slow query detected", 1003},
        {"ERROR", "Timeout expired",     1004},
        {"INFO",  "Request completed",   1005},
        {"ERROR", "Disk full",           1006},
        {"WARN",  "Memory usage high",   1007},
    };

    // Pipeline: get recent error messages, formatted
    auto recent_errors = logs
        | std::views::filter([](const LogEntry& e) {
              return e.level == "ERROR";
          })
        | std::views::drop(1)            // skip first error
        | std::views::transform([](const LogEntry& e) {
              return "[" + std::to_string(e.timestamp) + "] " + e.message;
          })
        | std::views::take(5);           // at most 5 results

    std::cout << "Recent errors (after first):\n";
    for (const auto& msg : recent_errors) {
        std::cout << "  " << msg << '\n';
    }
    // [1004] Timeout expired
    // [1006] Disk full

    // Reverse view — iterate backward without copying
    auto reversed = logs | std::views::reverse | std::views::take(3);
    std::cout << "\nLast 3 entries (reversed):\n";
    for (const auto& e : reversed) {
        std::cout << "  [" << e.level << "] " << e.message << '\n';
    }
}
دام

نماها غیرمالک هستند — به بازه زیربنایی ارجاع می‌دهند. اگر بازه منبع نابود شود، نما آویزان می‌شود:

auto make_view() {
    std::vector<int> local = {1, 2, 3};
    return local | std::views::filter([](int x) { return x > 1; });
    // BUG: local is destroyed, view dangles!
}

همچنین مراقب نماها روی موقت‌هایی که در وسط خط لوله ایجاد می‌شوند باشید:

// WRONG: the string is a temporary that gets destroyed
auto words = std::string("hello world")
    | std::views::split(' ');  // dangling!

همیشه اطمینان حاصل کنید که داده منبع بیشتر از تمام نماهایی که به آن ارجاع می‌دهند عمر کند. داده را قبل از ایجاد نماها در متغیری با طول عمر کافی ذخیره کنید.

بهترین شیوه

نماها تنبل هستند — هیچ کاری تا زمان پیمایش انجام نمی‌شود. این بدان معناست:

1. ساخت یک خط لوله اساساً رایگان است (فقط اشاره‌گرهای تابع و پارامترها را ذخیره می‌کند)
2. می‌توانید نماها را روی بازه‌های بسیار بزرگ یا حتی نامتناهی ایجاد کنید
3. هر عنصر قبل از پردازش عنصر بعدی از کل خط لوله عبور می‌کند
4. نماهای میان‌بُر مانند take(n) کل خط لوله را بعد از n عنصر متوقف می‌کنند

برای تبدیل نتایج نما به یک کانتینر، پیمایش و جمع‌آوری کنید:

std::vector<int> results;
for (int x : some_view) results.push_back(x);

// ‎C++‎23 adds std::ranges::to for cleaner materialization:
auto results = some_view | std::ranges::to<std::vector>();

ترجیح دهید داده‌ها را تا جایی که ممکن است به صورت نما نگه دارید — فقط وقتی به دسترسی تصادفی، ماندگاری یا ارسال به APIهایی که کانتینر نیاز دارند نیاز دارید آن‌ها را ماده‌سازی (materialize) کنید.

نکات کلیدی
  • نماها آداپتورهای بازه غیرمالک، قابل کپی O(1) و با ارزیابی تنبل هستند
  • views::filter و views::transform بلوک‌های سازنده بنیادی خطوط لوله داده هستند
  • views::take(n) و views::drop(n) برش بدون کپی فراهم می‌کنند
  • views::iota دنباله‌ها — محدود یا نامحدود — تولید می‌کند و جایگزین حلقه‌های شمارشی می‌شود
  • عملگر لوله | نماها را در خطوط لوله خوانای چپ‌به‌راست با صفر تخصیص میانی ترکیب می‌کند
  • نماها نباید بیشتر از داده منبع خود عمر کنند — همیشه اطمینان حاصل کنید بازه زیربنایی طول عمر کافی دارد

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

(20 امتیاز)

1. نمای ‎C++‎20 چه زمانی عناصر خود را محاسبه می‌کند؟

2. نتیجه `std::views::iota(1) | std::views::take(3)` چیست؟

3. چرا ایجاد نما روی یک متغیر محلی و بازگرداندن آن از تابع خطرناک است؟