رفتن به محتوا

Container Adaptors و کانتینرهای ویژه

درباره stack، queue، priority_queue و نوع‌های واژگانی مدرن مانند span، string_view و optional بیاموزید. بفهمید چه زمانی آداپتورها کد شما را ساده‌تر می‌کنند.

آداپتورهای کانتینر چیستند؟

آداپتورهای کانتینر (Container Adaptors) پوشش‌هایی روی کانتینرهای موجود هستند که یک رابط محدود ارائه می‌دهند. آن‌ها یک الگوی دسترسی خاص را با پنهان کردن API کامل کانتینر اعمال می‌کنند:

- std::stack — LIFO (آخرین ورودی، اولین خروجی)، به طور پیش‌فرض با deque پشتیبانی می‌شود
- std::queue — FIFO (اولین ورودی، اولین خروجی)، به طور پیش‌فرض با deque پشتیبانی می‌شود
- std::priority_queue — هرم بیشینه (max-heap)، به طور پیش‌فرض با vector پشتیبانی می‌شود

آداپتورها تکرارگر ارائه نمی‌دهند — فقط می‌توانید به عنصر بالا/جلو دسترسی داشته باشید. این محدودیت عمدی است: هدف کد را واضح‌تر می‌کند و از سوءاستفاده تصادفی جلوگیری می‌کند.

Stack و Queue در عمل

پشته‌ها و صف‌ها ساختارهای داده بنیادی هستند که در تجزیه (parsing)، BFS/DFS، سیستم‌های بازگشت (undo) و زمان‌بندی وظایف استفاده می‌شوند.

stack_queue.cpp
#include <stack>
#include <queue>
#include <string>
#include <iostream>

// Check if parentheses are balanced using a stack
bool is_balanced(const std::string& expr) {
    std::stack<char> stk;
    for (char c : expr) {
        if (c == '(' || c == '[' || c == '{') {
            stk.push(c);
        } else if (c == ')' || c == ']' || c == '}') {
            if (stk.empty()) return false;
            char top = stk.top();
            stk.pop();
            if ((c == ')' && top != '(') ||
                (c == ']' && top != '[') ||
                (c == '}' && top != '{')) {
                return false;
            }
        }
    }
    return stk.empty();
}

int main() {
    std::cout << std::boolalpha;
    std::cout << is_balanced("({[]})") << '\n';   // true
    std::cout << is_balanced("({[}])") << '\n';   // false

    // Queue — BFS-style task processing
    std::queue<std::string> tasks;
    tasks.push("compile");
    tasks.push("link");
    tasks.push("run tests");

    while (!tasks.empty()) {
        std::cout << "Processing: " << tasks.front() << '\n';
        tasks.pop();   // removes from front
    }
}

صف اولویت‌دار (هرم)

std::priority_queue به طور پیش‌فرض یک هرم بیشینه (max-heap) است — top() بزرگ‌ترین عنصر را برمی‌گرداند. برای هرم کمینه (min-heap)، از std::greater به عنوان مقایسه‌کننده استفاده کنید.

priority_queue.cpp
#include <queue>
#include <vector>
#include <functional>
#include <iostream>
#include <string>

struct Task {
    int priority;
    std::string name;
};

int main() {
    // Max-heap (default) — largest first
    std::priority_queue<int> max_heap;
    max_heap.push(30);
    max_heap.push(10);
    max_heap.push(50);
    max_heap.push(20);

    while (!max_heap.empty()) {
        std::cout << max_heap.top() << ' ';  // 50 30 20 10
        max_heap.pop();
    }
    std::cout << '\n';

    // Min-heap — smallest first
    std::priority_queue<int, std::vector<int>, std::greater<int>> min_heap;
    min_heap.push(30);
    min_heap.push(10);
    min_heap.push(50);
    std::cout << "Min: " << min_heap.top() << '\n';  // 10

    // Custom comparator with lambda
    auto cmp = [](const Task& a, const Task& b) {
        return a.priority < b.priority;  // higher priority first
    };
    std::priority_queue<Task, std::vector<Task>, decltype(cmp)> task_queue(cmp);
    task_queue.push({3, "low priority"});
    task_queue.push({10, "critical"});
    task_queue.push({7, "medium"});

    while (!task_queue.empty()) {
        auto& t = task_queue.top();
        std::cout << "[" << t.priority << "] " << t.name << '\n';
        task_queue.pop();
    }
}

std::span — نمای غیرمالک داده پیوسته (‎C++‎20)

std::span یک نمای سبک و غیرمالک روی دنباله‌ای پیوسته از عناصر است. الگوی مستعد خطای ارسال جفت‌های (اشاره‌گر، اندازه) را جایگزین می‌کند.

span_example.cpp
#include <span>
#include <vector>
#include <array>
#include <iostream>
#include <numeric>

// span accepts vector, array, C-array — anything contiguous
void print_stats(std::span<const int> data) {
    if (data.empty()) return;
    std::cout << "Size: " << data.size() << '\n';
    std::cout << "First: " << data.front() << '\n';
    std::cout << "Last:  " << data.back() << '\n';

    int sum = std::accumulate(data.begin(), data.end(), 0);
    std::cout << "Sum:   " << sum << '\n';

    // Subviews
    auto first3 = data.first(3);   // first 3 elements
    auto last2  = data.last(2);    // last 2 elements
    auto mid    = data.subspan(1, 3);  // 3 elements starting at index 1
}

int main() {
    std::vector<int> vec = {1, 2, 3, 4, 5};
    std::array<int, 5> arr = {10, 20, 30, 40, 50};
    int c_arr[] = {100, 200, 300, 400, 500};

    print_stats(vec);     // works with vector
    print_stats(arr);     // works with array
    print_stats(c_arr);   // works with C-array
}

string_view و optional

std::string_view (‎C++‎17) یک ارجاع غیرمالک و فقط‌خواندنی به یک رشته است. هزینه کپی یا تخصیص std::string را برای توابعی که فقط نیاز به خواندن دارند حذف می‌کند:

void greet(std::string_view name) {  // no copy, no allocation
    std::cout << "Hello, " << name << '\n';
}
greet("world");               // works with string literals
greet(std::string{"world"});  // works with std::string

std::optional (‎C++‎17) نشان‌دهنده مقداری است که ممکن است وجود داشته باشد یا نداشته باشد — جایگزین ایمن از نظر نوع برای «مقادیر جادویی» یا اشاره‌گرهای خام برای نشان دادن عدم وجود:

std::optional<int> find_index(std::span<const int> data, int target) {
    for (int i = 0; i < data.size(); ++i)
        if (data[i] == target) return i;
    return std::nullopt;  // not found
}
auto idx = find_index(vec, 42);
if (idx) std::cout << "Found at " << *idx << '\n';
std::cout << idx.value_or(-1) << '\n';  // -1 if empty
دام

از آنجایی که std::string_view و std::span غیرمالک هستند، اگر داده زیربنایی نابود شود به راحتی آویزان می‌شوند:

std::string_view bad() {
    std::string temp = "hello";
    return temp;  // DANGLING — temp destroyed at end of scope
}

هرگز string_view یا span به یک متغیر محلی را برنگردانید. آن‌ها برای پارامترهای تابع امن هستند (داده فراخواننده بیشتر از فراخوانی عمر دارد)، اما به عنوان مقادیر بازگشتی خطرناک هستند مگر اینکه بتوانید تضمین کنید منبع بیشتر از نما عمر دارد.

همین مسئله برای std::span نیز صدق می‌کند — در داخل فقط یک اشاره‌گر و یک اندازه است، بدون معنای مالکیت.

بهترین شیوه

از آداپتورهای کانتینر استفاده کنید وقتی می‌خواهید نظمی را اعمال کنید بر نحوه دسترسی به عناصر:

- به جای deque یا vector خام، وقتی فقط دسترسی LIFO معنادار است از std::stack استفاده کنید. این هدف را واضح می‌کند و از دسترسی تصادفی اتفاقی جلوگیری می‌کند.
- برای پردازش FIFO (صف وظایف، BFS) از std::queue استفاده کنید.
- برای زمان‌بندی، الگوریتم دایکسترا یا هر سناریویی که همیشه عنصر حدی را می‌خواهید از std::priority_queue استفاده کنید.

اگر به پیمایش، پرس‌وجوی اندازه در نقاط دلخواه یا دسترسی میانی نیاز دارید، از کانتینر زیربنایی مستقیماً استفاده کنید.

نکات کلیدی
  • آداپتورهای کانتینر (stack، queue، priority_queue) رابط را محدود می‌کنند تا نظم استفاده را اعمال کنند
  • std::priority_queue به طور پیش‌فرض یک هرم بیشینه است — برای هرم کمینه از std::greater استفاده کنید
  • std::span (‎C++‎20) یک نمای غیرمالک روی داده پیوسته فراهم می‌کند و جفت‌های (اشاره‌گر، اندازه) را جایگزین می‌کند
  • std::string_view (‎C++‎17) از کپی رشته‌ها برای استفاده فقط‌خواندنی جلوگیری می‌کند — اما مراقب ارجاعات آویزان باشید
  • std::optional (‎C++‎17) روش ایمن از نظر نوع برای نمایش «شاید بدون مقدار» بدون مقادیر نگهبان جادویی است
  • نماهای غیرمالک (span، string_view) هرگز نباید بیشتر از داده‌ای که به آن ارجاع می‌دهند عمر کنند

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

(15 امتیاز)

1. کدام آداپتور کانتینر رفتار FIFO (اولین ورودی، اولین خروجی) ارائه می‌دهد؟

2. `std::priority_queue<int>` به طور پیش‌فرض از `top()` چه نوعی برمی‌گرداند؟

3. چرا بازگرداندن `std::string_view` به یک `std::string` محلی خطرناک است؟