تکرارگرها: چسب 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() دارد کار میکنند.
#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
سرآیند توابع کمکی فراهم میکند که در تمام دستهبندیهای تکرارگر کار میکنند و به طور خودکار کارآمدترین پیادهسازی را انتخاب میکنند.
#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) جریانهای ورودی/خروجی را به رابط تکرارگر تطبیق میدهند.
#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` چیست؟