رفتن به محتوا

تکرارگرها: چسب STL

دسته‌بندی‌های تکرارگر را بشناسید، از تکرارگرهای ورودی تا تکرارگرهای پیوسته. حساب تکرارگر، قرارداد begin/end و نحوه اتصال تکرارگرها به الگوریتم‌ها را بیاموزید.

انتزاع تکرارگر

تکرارگرها (Iterators) چسب بین کانتینرهای STL و الگوریتم‌ها هستند. به جای نوشتن N الگوریتم برای M کانتینر (N*M پیاده‌سازی)، STL یک رابط تکرارگر مشترک تعریف می‌کند. هر کانتینر تکرارگرها را فراهم می‌کند و هر الگوریتم روی بازه‌های تکرارگر کار می‌کند — که N+M پیاده‌سازی می‌دهد که با هم کار می‌کنند.

تکرارگر شیئی است که:
1. به یک عنصر در یک دنباله اشاره می‌کند
2. می‌تواند ارجاع‌زدایی شود (*it) برای دسترسی به عنصر
3. می‌تواند افزایش یابد (++it) برای رفتن به عنصر بعدی
4. می‌تواند مقایسه شود (it != end) برای تشخیص پایان دنباله

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

دسته‌بندی‌های تکرارگر

دسته‌بندی‌های تکرارگر ++C یک سلسله‌مراتب تشکیل می‌دهند (هر سطح شامل قابلیت‌های تمام سطوح پایین‌تر است):

| دسته‌بندی | عملیات | کانتینرهای نمونه |
|---|---|---|
| ورودی (Input) | خواندن، تک‌گذر رو به جلو (++، *، ==) | istream_iterator |
| خروجی (Output) | نوشتن، تک‌گذر رو به جلو (++، *=) | ostream_iterator، back_inserter |
| رو به جلو (Forward) | خواندن/نوشتن، چندگذر رو به جلو | forward_list، unordered_set |
| دوطرفه (Bidirectional) | رو به جلو + رو به عقب (--) | list، set، map |
| دسترسی تصادفی (Random Access) | دوطرفه + پرش (+n، -n، []، <) | deque |
| پیوسته (Contiguous) (‎C++‎17) | دسترسی تصادفی + عناصر در حافظه مجاور هستند | vector، array، string |

الگوریتم‌ها حداقل دسته‌بندی مورد نیاز خود را اعلام می‌کنند. std::sort به تکرارگرهای دسترسی تصادفی نیاز دارد، بنابراین با vector کار می‌کند اما با list خیر (که تابع عضو sort() مخصوص خود را دارد).

قرارداد begin/end

هر کانتینر STL متدهای begin() و end() را فراهم می‌کند — یک بازه نیمه‌باز [begin, end) که end به یک موقعیت بعد از آخرین عنصر اشاره می‌کند. توابع آزاد std::begin() و std::end() با کانتینرها، آرایه‌های C و هر نوعی که متدهای عضو .begin() و .end() دارد کار می‌کنند.

begin_end.cpp
#include <vector>
#include <list>
#include <algorithm>
#include <iostream>
#include <iterator>

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

    // Member functions
    auto first = vec.begin();   // points to 5
    auto last  = vec.end();     // points past 2

    // Free functions — preferred for generic code
    auto first2 = std::begin(vec);
    auto last2  = std::end(vec);

    // Works with C-arrays too
    int arr[] = {10, 20, 30};
    std::sort(std::begin(arr), std::end(arr));

    // const iterators — read-only access
    auto cit = vec.cbegin();     // const_iterator
    // *cit = 99;                // ERROR: cannot modify through const_iterator

    // Reverse iterators — iterate backward
    for (auto rit = vec.rbegin(); rit != vec.rend(); ++rit) {
        std::cout << *rit << ' ';  // 2, 4, 1, 3, 5 (reversed, pre-sort was applied)
    }
    std::cout << '\n';
}

ابزارهای تکرارگر: next، prev، advance، distance

سرآیند توابع کمکی فراهم می‌کند که در تمام دسته‌بندی‌های تکرارگر کار می‌کنند و به طور خودکار کارآمدترین پیاده‌سازی را انتخاب می‌کنند.

iterator_utils.cpp
#include <iterator>
#include <vector>
#include <list>
#include <iostream>

int main() {
    std::vector<int> vec = {10, 20, 30, 40, 50};

    // std::next — returns iterator n positions forward (default 1)
    auto it = std::next(vec.begin(), 2);   // points to 30
    std::cout << *it << '\n';              // 30

    // std::prev — returns iterator n positions backward
    auto it2 = std::prev(vec.end());       // points to 50
    std::cout << *it2 << '\n';             // 50

    // std::advance — modifies iterator in place (no return)
    auto it3 = vec.begin();
    std::advance(it3, 3);                  // now points to 40
    std::cout << *it3 << '\n';             // 40

    // std::distance — number of increments from first to last
    auto d = std::distance(vec.begin(), vec.end());  // 5
    std::cout << "Distance: " << d << '\n';

    // These work with non-random-access iterators too
    std::list<int> lst = {1, 2, 3, 4, 5};
    auto lit = std::next(lst.begin(), 2);  // O(n) for list, O(1) for vector
    std::cout << *lit << '\n';             // 3

    auto ld = std::distance(lst.begin(), lst.end());  // O(n) for list
    std::cout << "List distance: " << ld << '\n';     // 5
}

تکرارگرهای درج و تکرارگرهای جریان

تکرارگرهای درج (Insert Iterators) به الگوریتم‌ها اجازه می‌دهند به جای بازنویسی عناصر موجود، در کانتینرها درج کنند. تکرارگرهای جریان (Stream Iterators) جریان‌های ورودی/خروجی را به رابط تکرارگر تطبیق می‌دهند.

insert_stream_iterators.cpp
#include <iterator>
#include <vector>
#include <algorithm>
#include <iostream>
#include <sstream>

int main() {
    std::vector<int> src = {1, 2, 3, 4, 5};
    std::vector<int> dst;

    // back_inserter — calls push_back on each assignment
    std::copy(src.begin(), src.end(), std::back_inserter(dst));
    // dst: {1, 2, 3, 4, 5}

    // front_inserter — calls push_front (works with deque, list)
    // inserter — calls insert at a given position

    // ostream_iterator — write to an output stream
    std::copy(dst.begin(), dst.end(),
              std::ostream_iterator<int>(std::cout, ", "));
    std::cout << '\n';  // 1, 2, 3, 4, 5,

    // istream_iterator — read from an input stream
    std::istringstream iss("10 20 30 40");
    std::vector<int> from_stream(
        std::istream_iterator<int>(iss),
        std::istream_iterator<int>()   // end-of-stream sentinel
    );
    // from_stream: {10, 20, 30, 40}
}
دام

افزایش تکرارگر فراتر از end() یا کاهش فراتر از begin() رفتار نامشخص (undefined behavior) است. همین مسئله برای ارجاع‌زدایی end() صدق می‌کند — یک نگهبان (sentinel) است، نه یک عنصر معتبر.

یک اشتباه رایج نوشتن it + 1 بدون بررسی اینکه آیا it قبلاً در یا نزدیک انتها است:

auto it = vec.end();
++it;    // UB — past-the-end
*it;     // UB — dereferencing end()

همیشه قبل از عملیات، اعتبار تکرارگر را بررسی کنید. از std::next و std::prev با بررسی فاصله استفاده کنید، یا از حلقه‌های for مبتنی بر بازه استفاده کنید که مرزها را به طور خودکار مدیریت می‌کنند.

بهترین شیوه

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

1. جفت تکرارگر (یا بازه‌ها در ‎C++‎20) را بپذیرید به جای کانتینرهای خاص
2. از typename یا auto برای نوع تکرارگر استفاده کنید
3. به جای تفریق (که به دسترسی تصادفی نیاز دارد) از std::distance استفاده کنید
4. به جای حساب +/- از std::next/std::prev استفاده کنید
5. در ‎C++‎20، مفاهیم (concepts) را برای محدود کردن الزامات تکرارگر ترجیح دهید:

template<std::forward_iterator Iter>
auto my_algorithm(Iter first, Iter last) { ... }

این تضمین می‌کند که کد شما با وسیع‌ترین بازه از کانتینرها کار می‌کند.

نکات کلیدی
  • تکرارگرها کانتینرها را از الگوریتم‌ها جدا می‌کنند — اصل طراحی اصلی STL
  • شش دسته‌بندی تکرارگر وجود دارد: ورودی، خروجی، رو به جلو، دوطرفه، دسترسی تصادفی، پیوسته
  • از توابع آزاد std::begin() / std::end() برای کد عمومی که با کانتینرها و آرایه‌های C کار می‌کند استفاده کنید
  • std::next، std::prev، std::advance، std::distance در تمام دسته‌بندی‌های تکرارگر کار می‌کنند
  • تکرارگرهای درج (back_inserter، front_inserter، inserter) به الگوریتم‌ها اجازه رشد کانتینرها را می‌دهند
  • هرگز end() را ارجاع‌زدایی نکنید یا فراتر از آن افزایش ندهید — هر دو رفتار نامشخص هستند

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

(15 امتیاز)

1. `std::sort` به کدام دسته‌بندی تکرارگر نیاز دارد؟

2. `std::back_inserter(vec)` چه چیزی ایجاد می‌کند؟

3. پیچیدگی زمانی `std::distance(first, last)` روی `std::list` چیست؟