Learn C++ STL

lesson ১ / ৯ · iterator: container আর algorithm-এর মাঝখানের জিনিসটা

Module ১১ · iterator: container আর algorithm-এর মাঝখানের জিনিসটা

iterator: এমন একটা position, যে নিজেই সরতে জানে

Freeপড়া

এই lesson-এ যা শিখবে

  • প্রতিটা container-এর iterator কোন category-র, input, output, forward, bidirectional নাকি random access, সেটা বলতে পারবে, আর প্রতিটা category কী কী পারে সেটাও।
  • যেকোনো container-এ সামনের দিকে হাঁটতে পারবে begin() থেকে শেষের পরের end() পর্যন্ত, পেছনের দিকে rbegin() থেকে rend() পর্যন্ত, আর শুধু পড়ার জন্য cbegin() দিয়ে।
  • এক বাক্যে বুঝিয়ে বলতে পারবে invalidated iterator কী, আর vector-এর iterator কখন ভেঙে যায়।

Kenji প্রতি ঘণ্টায় স্কুলের বৃষ্টির পানির ট্যাংকে পানি কতটা উঁচু, সেটা মাপে, আর মাপগুলো রাখে একটা vector<int>-এ। ওর function printGaps(v) v[i] - v[i - 1] দিয়ে print করে, প্রতি ঘণ্টায় পানি কতটা বাড়ল বা কমল। Maria নিজের ট্যাংকের জন্যও ঠিক এই report চায়। ওর log-এ দেরিতে আসা মাপ মাঝখানে ঢোকাতে হয়, তাই ও রাখে একটা std::list-এ, আর একদম প্রথম l[1]-এই GCC 12 আটকে দেয়। তাহলে কি প্রতিটা container-এর জন্য function-টা আলাদা করে লিখতে হবে?

একটা function, vector আর list দুটোর জন্যই

Maria-র প্রথম চেষ্টা হলো Kenji-র index দিয়ে পড়াটাই, ওর list<int> l-এ: cout << l[1] - l[0];। Runner-এর flag-এ এটা error দিয়ে থেমে যায়, প্রতিটা index-এর জন্য একবার করে: error: no match for 'operator[]' (operand types are 'std::__cxx11::list<int>' and 'int')। List-এর কোনো [] নেই, কারণ হেঁটে না গিয়ে ও position 1-এ লাফ দিতে পারে না। তবে দুই container-এরই যেটা আছে, সেটা হলো একটা iterator: এমন একটা position, যেটা সেখানকার element পড়তে পারে আর পরেরটায় সরে যেতে পারে।

তাই Kenji printGaps নতুন করে লেখে: এবার vector না নিয়ে নেয় দুইটা iterator, first আর last। template <typename It> লাইনটা বলে, It যেকোনো iterator type হতে পারে। তখন compiler নিজেই একটা copy লেখে vector-এর iterator-এর জন্য, আরেকটা list-এর জন্য। আইডিয়াটা Module 1-এর lesson 05 দেখিয়েছিল, আর এই track শুধু ওটার এই ছোট রূপটাই লেখে।

#include <iostream>
#include <iterator>
#include <list>
#include <vector>
using namespace std;

template <typename It>
void printGaps(It first, It last) {
    cout << "gaps:";
    if (first != last) {
        It before = first;
        ++first;
        while (first != last) {
            cout << ' ' << *first - *before;
            before = first;
            ++first;
        }
    }
    cout << '\n';
}

int main() {
    vector<int> kenji{12, 15, 11, 18, 18};
    list<int> maria{12, 15, 11, 18, 18};
    maria.insert(next(maria.begin(), 2), 20);
    printGaps(kenji.begin(), kenji.end());
    printGaps(maria.begin(), maria.end());
    return 0;
}
gaps: 3 -4 7 0
gaps: 3 5 -9 7 0

ভেতরে function-টা শুধু != দিয়ে দুইটা iterator মেলায়, * দিয়ে পড়ে, আর ++ দিয়ে এক পা এগোয়। দুই container-ই এই তিনটা কাজ পারে, তাই একই লেখা দুটোর জন্যই compile হয়ে গেল। Maria-র list মাঝখানে 20 নিয়েছে, আর ওর gap-গুলোও সেই হিসেবে বদলে গেছে। তাই দুইটা iterator-এর উপর লেখা function চলে প্রতিটা container-এ, যেটা হাঁটতে জানে।

begin() আর end(), তিন রকম memory-তে

Module 0-এর lesson 04-এ হাঁটাটা দেখা হয়ে গেছে। begin() হলো প্রথম element, end() হলো শেষটার ঠিক পরের খালি ঘর, আর loop চলে যতক্ষণ it != end()। দুটো মিলে হয় half-open range [begin, end), যেটা যেকোনো algorithm যেকোনো container থেকে নিতে পারে। Container বদলালে যা বদলায়, তা হলো হাঁটার নিচের memory। Widget-টা একই চারটা value-এর উপর একটা loop চালায় তিনটা container-এ, প্রতিটায় পাঁচবার থেমে।

পনেরোবার থামা: একই loop 12, 15, 11 আর 18-এর উপর দিয়ে হাঁটে একটা vector, একটা deque আর একটা list-এ। প্রতিটা row বলে কোন হাঁটা, it কোথায়, আর *it কী পড়ে।

হাঁটাit কোথায়*it কী পড়ে
std::vector<int> cbegin(), একমাত্র block-এর position 012
std::vector<int> cposition 1, পরের ঠিকানা15
std::vector<int> cposition 2, পরের ঠিকানা11
std::vector<int> cposition 3, পরের ঠিকানা18
std::vector<int> cend(), 18-এর পরের ঘরend(), পড়া হয় না
std::deque<int> cbegin(), block 1, ঘর 112
std::deque<int> cblock 1, ঘর 215
std::deque<int> cblock 2, ঘর 1: পরের block-এ লাফ11
std::deque<int> cblock 2, ঘর 218
std::deque<int> cend(), 18-এর পরের ঘরend(), পড়া হয় না
std::list<int> cbegin(), node 112
std::list<int> cnode 2, link ধরে পৌঁছানো15
std::list<int> cnode 3, link ধরে পৌঁছানো11
std::list<int> cnode 4, link ধরে পৌঁছানো18
std::list<int> cend(), শেষ node-এর পরেend(), পড়া হয় না

Vector ওর চারটা value পাশাপাশি রাখে একটাই block-এ, তাই ++ চলে যায় পরের ঠিকানায়। Deque রাখে নির্দিষ্ট মাপের কয়েকটা block। ছবিতে block-এ 2টা করে, কিন্তু GCC 12-এর আসল block-এ 128টা int ধরে, তাই ++ নতুন block-এ লাফ দেয় শুধু block-এর কিনারায় এসে। List-এর প্রতিটা node আলাদা করে নেওয়া memory, যেখানে খালি জায়গা মিলেছে সেখানেই, তাই ++ একটা link ধরে এগোয়। Compiler Explorer-এ একটা program তিনটার জন্যই একই হাঁটা print করেছে: 12 15 11 18, তারপর end()।

তাই loop দেখতে সব জায়গায় একই, কিন্তু একটা ++ প্রতিটা container-এ আলাদা একটা move। এই পার্থক্যই ঠিক করে দেয়, একটা iterator কোন কোন move পারবে।

পাঁচটা category: একটা iterator কী কী পারে

C++ standard iterator-গুলোকে পাঁচটা category-তে ভাগ করে, কে কোন move পারে সেই হিসেবে। প্রতিটা container-এর iterator এদের কোনো একটায় পড়ে। GCC 12 সেটা জানায় std::iterator_traits দিয়ে, যেটা lesson 02-এ চালিয়ে দেখা হবে। এই table-টা হাতের কাছে রেখো।

Categoryপড়া *itলেখা *it = x++--+ n, - n, [n], <, it2 - it1GCC 12-এ কে দেয়
inputহ্যাঁ, একবারইনাহ্যাঁনানাistream_iterator
outputনাহ্যাঁ, একবারইহ্যাঁনানাostream_iterator, back_insert_iterator
forwardহ্যাঁ, বারবারহ্যাঁ, key বাদেহ্যাঁনানাforward_list, unordered_set, unordered_map
bidirectionalহ্যাঁহ্যাঁ, key বাদেহ্যাঁহ্যাঁনাlist, set, map
random accessহ্যাঁহ্যাঁহ্যাঁহ্যাঁহ্যাঁvector, deque, array, string, আর int*-এর মতো সাধারণ pointer

"Key বাদে" মানে set-এর element আর map-এর key শুধু পড়া যায়, Module 8 আর 10 যেমন দেখিয়েছে; map-এর value অবশ্য লেখা যায়। একবারই মানে iterator এগিয়ে গেলে value-টা আর থাকে না: keyboard থেকে পড়া একটা সংখ্যা দ্বিতীয়বার পড়া যায় না। Table-টা নিচ থেকে উপরে পড়ো। Random access iterator সব পারে, আর উপরের প্রতিটা row কিছু না কিছু ছেড়ে দেয়।

পাঁচটা iterator category, বাক্সের ভেতরে বাক্স প্রতিটা বাক্স ওর ভেতরের সব বাক্সের সব কাজ পারে random access: + n, - n, [n], <, it2 - it1 vector, deque, array, string, int* bidirectional: সাথে -- যোগ হয় list, set, map forward: বারবার হাঁটা, *it = x forward_list, unordered_set, unordered_map input: *it পড়া, ++ istream_iterator প্রতিটা value একবারই পড়া যায়, তারপর iterator এগিয়ে যায় একদম আলাদা জাত output: *it = x, ++ ostream_iterator back_insert_iterator শুধু লেখে, পড়ে না
প্রতিটা container-এর জন্য GCC 12 যে category জানায়। ভেতরের যেকোনো বাক্স যেখানে চাওয়া হয়, সেখানে বাইরের বাক্সের iterator-ও চলে।

বাক্সগুলো একটার ভেতরে আরেকটা, কারণ প্রতিটা category ওর ভেতরেরটার সব কাজ পারে। তাই std::find, যার শুধু একটা input iterator লাগে, যেকোনো container নিয়ে চলে। std::sort চায় random access, তাই list ওটা ব্যবহার করতে পারে না, নিজেই নিজেকে sort করে, Module 5 যেমন দেখিয়েছে। সাধারণ pointer-ও random access, এজন্যই C array প্রতিটা algorithm-এর সাথে চলে।

In C++20

C++20 প্রতিটা category-কে একটা concept হিসেবে নাম দিয়েছে: একটা type নিয়ে হ্যাঁ-না প্রশ্ন, যার উত্তর দেয় compiler। সাথে এসেছে ষষ্ঠ একটা, contiguous_iterator, এমন element-এর জন্য যেগুলো memory-তে পাশাপাশি বসে। Program-টা পাঁচটা প্রশ্ন করে, হ্যাঁ হলে 1 আর না হলে 0 print করে। এই block চলে C++20-এ, আর এর Run button-ও এটাকে সেখানেই খোলে।

#include <forward_list>
#include <iostream>
#include <iterator>
#include <list>
#include <vector>
using namespace std;

int main() {
    cout << contiguous_iterator<vector<int>::iterator> << ' '
         << random_access_iterator<vector<int>::iterator> << ' '
         << random_access_iterator<list<int>::iterator> << ' '
         << bidirectional_iterator<list<int>::iterator> << ' '
         << bidirectional_iterator<forward_list<int>::iterator> << '\n';
    return 0;
}
1 1 0 1 0
Run in Compiler

চারটা প্রান্ত: begin, end, rbegin আর rend

যে container পেছনের দিকে হাঁটতে পারে, সে আরও দুইটা প্রান্ত দেয়। rbegin() হলো শেষ element-এর উপর বসা একটা reverse iterator, আর rend() হলো প্রথমটার আগের খালি ঘর। Reverse iterator ++-কে উল্টে দেয়, তাই প্রতিটা ++ এক element করে সামনের দিকে সরে। পেছনের loop মানে সামনের loop-ই, শুধু দুইটা নামের আগে একটা r বসানো।

একটা vector-এ begin, end, rbegin আর rend একটা vector-এর চারটা প্রান্ত 12 15 11 18 begin() end() rbegin() rend() সামনে: begin() থেকে end()। পেছনে: rbegin() থেকে rend(), প্রতিটা ++ এক ঘর বাঁয়ে। ভাঙা দাগের ঘরে কোনো element নেই, তাই end() বা rend() কখনো পড়া হয় না।
begin() আর rbegin() বসে আসল element-এর উপর; end() আর rend() হলো সেই খালি ঘর, যেখানে প্রতিটা হাঁটা থামে।
#include <iostream>
#include <vector>
using namespace std;

int main() {
    vector<int> v{12, 15, 11, 18};
    cout << "*begin() " << *v.begin() << ", *rbegin() " << *v.rbegin() << '\n';
    cout << "forward: ";
    for (auto it = v.begin(); it != v.end(); ++it) {
        cout << ' ' << *it;
    }
    cout << "\nbackward:";
    for (auto it = v.rbegin(); it != v.rend(); ++it) {
        cout << ' ' << *it;
    }
    cout << '\n';
    cout << "end() - begin() = " << v.end() - v.begin() << ", rend() - rbegin() = " << v.rend() - v.rbegin() << '\n';
    return 0;
}
*begin() 12, *rbegin() 18
forward:  12 15 11 18
backward: 18 11 15 12
end() - begin() = 4, rend() - rbegin() = 4

*rbegin() পড়েছে 18, শেষ value, আর পেছনের হাঁটা rend()-এ এসে সেটা না পড়েই থেমেছে। দুই range-এই চারটা করে element: rend() - rbegin() হলো 4, ঠিক end() - begin()-এর মতো। যে container-এর iterator bidirectional বা random access, তার সবারই rbegin() আছে। forward_list-এর নেই: GCC 12 বলে error: 'class std::forward_list<int>' has no member named 'rbegin'; did you mean 'begin'?, আর একটা unordered_set-ও একই উত্তর পায়। তাই পেছনে হাঁটো rbegin() আর rend() দিয়ে, আর test হিসেবে !=-ই রাখো।

cbegin(): যে হাঁটা শুধু পড়ে

cbegin() আর cend() হলো এমন হাঁটার begin() আর end(), যেটা কথা দেয় কিছু লিখবে না। এরা ফেরত দেয় একটা const_iterator, যেটা *it পড়ে কিন্তু *it = x মানে না। crbegin() আর crend() একই কথা দেয় পেছনের দিকে। Loop যখন শুধু দেখে, তখন এগুলো নাও, আর compiler তোমাকে কথাটা রাখতে বাধ্য করবে। নিচের Example 3 GCC 12-এর নিজের ভাষায় সেই না-টা দেখায়।

List-এর iterator যা করতে রাজি না

List-এর iterator bidirectional: ও ++ আর -- দিয়ে চলে, একবারে এক link। ওর কোনো + n নেই, <-ও নেই, কারণ এর কোনোটাই link ধরে এক পা না। GCC 12 l.begin() + 2 আর it < l.end() বাতিল করে দেয়, আর ভুল 2 আর 3 ওর বার্তাগুলো তুলে দেখায়। এর বদলে k পা সরতে লেখো std::next(it, k), আর প্রতিটা loop শেষ করো != দিয়ে।

#include <iostream>
#include <iterator>
#include <list>
using namespace std;

int main() {
    list<int> l{12, 15, 11, 18};
    auto third = next(l.begin(), 2);
    cout << "third: " << *third << '\n';
    --third;
    cout << "one back: " << *third << '\n';
    int steps = 0;
    for (auto it = l.begin(); it != l.end(); ++it) {
        steps++;
    }
    cout << "steps to end(): " << steps << '\n';
    return 0;
}
third: 11
one back: 15
steps to end(): 4

next(l.begin(), 2) দুইটা link পেরিয়ে 11-এ পৌঁছেছে, আর --third এক পা পিছিয়ে 15-এ এসেছে। != দেওয়া loop end()-এ পৌঁছাতে চার পা নিয়েছে। Vector-এ next এক লাফে যায়, আর list-এ হেঁটে যায়; পার্থক্যটা lesson 02 মেপে দেখাবে। তাই next আর != লেখো, তাহলে একই code প্রতিটা container-এ চলবে।

Iterator কখন আর valid থাকে না

Iterator শুধুই একটা position। Container যদি ওর element-গুলো সরিয়ে ফেলে, আগে রাখা iterator তখনো পুরোনো জায়গাটাই দেখায়, আর সেটা হয়ে যায় invalidated। ওটা দিয়ে পড়া undefined behaviour: তখন কী হবে, C++ standard তার কোনো কথাই দেয় না। Vector নিজের block ছাড়িয়ে বড় হলে সবকিছু সরিয়ে নেয়, Module 2 যেমন দেখিয়েছে। এই program একটা vector-কে ওর capacity 4 পর্যন্ত ভরে, তারপর পঞ্চম একটা value যোগ করে।

#include <iostream>
#include <vector>
using namespace std;

int main() {
    vector<int> v;
    v.reserve(4);
    for (int x : {10, 20, 30, 40}) v.push_back(x);
    const int* before = v.data();
    cout << "size " << v.size() << ", capacity " << v.capacity() << '\n';
    v.push_back(50);
    cout << "size " << v.size() << ", capacity " << v.capacity() << '\n';
    cout << "first element moved: " << (v.data() != before) << '\n';
    return 0;
}
size 4, capacity 4
size 5, capacity 8
first element moved: 1
push_back(50)-এর আগে রাখা iterator এখন ছেড়ে দেওয়া block-এ ভরা vector-এ push_back(50) প্রতিটা element সরিয়ে নেয় আগে: capacity 4, আর block-টা ভরা 10 20 30 40 পুরোনো block: সরানোর পরে ছেড়ে দেওয়া it, আগে রাখা 4টা value সরল পরে: নতুন block, capacity 8 10 20 30 40 50 v.begin(), নতুন করে নেওয়া it এখনো ছেড়ে দেওয়া block-এর দিকেই তাকিয়ে: *it পড়া undefined behaviour।
সরানোর পরে পুরোনো block ছেড়ে দেওয়া হয়। Push-এর আগে রাখা iterator তখনো সেখানেই তাকিয়ে থাকে।

পঞ্চম value-টা আর ধরেনি, তাই vector 8-এর একটা নতুন block নিয়েছে, চারটা value সেখানে সরিয়েছে, আর পুরোনো block ছেড়ে দিয়েছে। push_back(50)-এর আগে রাখা একটা iterator এখনো সেই ছেড়ে দেওয়া block-এর দিকেই তাকিয়ে আছে। কেউ সাবধান করবে না, আর কপাল ভালো থাকলে program হয়তো পুরোনো value-টাই print করে দেবে। তাই vector বড় হতে পারে এমন যেকোনো কাজের পরে begin() থেকে নতুন iterator নাও। প্রতিটা container-এর নিয়ম দেবে lesson 02, আর lesson 05 সরানোটা ধাপে ধাপে আবার দেখাবে।

প্রথম দিনেই যে syntax লাগবে

Iterator: প্রান্ত, move, আর একটা range-এর উপর function

c.begin()    c.end()          the first element, and the empty slot past the last
c.rbegin()   c.rend()         the last element, and the empty slot before the first
c.cbegin()   c.cend()         begin and end for a walk that only reads
*it                           the element it points at (never *c.end())
++it                          one step forward, on every container
--it                          one step back (not forward_list, not unordered)
it != c.end()                 the loop test that works on every container
next(it, k)   prev(it, k)     a copy of it moved k steps forward or back

template <typename It>
void f(It first, It last)     one function for any container's range
  • প্রতিটা container-এর begin() আর end() আছে; [begin(), end()) মানে পুরো container, আর খালি container-এ begin() == end()।
  • auto it = c.begin(); লিখলে iterator-এর type-এর নাম compiler নিজেই বসিয়ে নেয়, আর নামগুলো বেশ লম্বা: vector<int>::iterator, list<string>::const_iterator।
  • next আর prev থাকে <iterator>-এ। Random access-এ এরা লাফ দেয়, বাকি সব জায়গায় এক পা এক পা করে হাঁটে।
  • Arithmetic, মানে it + 2, it2 - it1 আর it < end, আছে শুধু random access iterator-এ।

তাই একটাই loop-এর ছাঁচ, begin() থেকে end(), সাথে != আর ++, এই track-এর প্রতিটা container-এ হাঁটে।

Example 1: vector আর list দুটোর জন্য একটাই printAll

দুইটা iterator নেওয়া সবচেয়ে ছোট function একটা range-কে bracket-এর ভেতরে print করে। এটা চলে নম্বরের একটা vector-এ, খাবারের একটা list-এ, সেই list-এর একটা অংশে, আর একটা খালি range-এ।

#include <iostream>
#include <iterator>
#include <list>
#include <string>
#include <vector>
using namespace std;

template <typename It>
void printAll(It first, It last) {
    cout << '[';
    for (It it = first; it != last; ++it) {
        if (it != first) {
            cout << ", ";
        }
        cout << *it;
    }
    cout << "]\n";
}

int main() {
    vector<int> marks{4, 8, 15};
    list<string> snacks{"tea", "bun", "jam"};
    printAll(marks.begin(), marks.end());
    printAll(snacks.begin(), snacks.end());
    printAll(next(snacks.begin()), snacks.end());
    printAll(marks.end(), marks.end());
    return 0;
}
[4, 8, 15]
[tea, bun, jam]
[bun, jam]
[]

একই function একটা int vector আর একটা string list দুটোই print করেছে। next(snacks.begin()) range শুরু করেছে এক element পরে, তাই tea বাদ পড়েছে। printAll(marks.end(), marks.end()) ছিল একটা খালি range, তাই loop একবারও চলেনি।

Run in Compiler
Example 2: browser history, নতুনটা আগে

Amara-র browser ওর খোলা প্রতিটা page রাখে একটা list<string>-এ, পুরোনোটা আগে। History page দেখায় নতুনটা আগে, তাই হাঁটে rbegin() থেকে rend()। Back button নিয়ে যায় সবচেয়ে নতুনটার এক পা পেছনের page-এ।

#include <iostream>
#include <iterator>
#include <list>
#include <string>
using namespace std;

int main() {
    list<string> history{"home", "news", "maps", "mail"};
    history.push_back("music");
    cout << "newest first:\n";
    int n = 1;
    for (auto it = history.rbegin(); it != history.rend(); ++it) {
        cout << n << ". " << *it << '\n';
        n++;
    }
    cout << "now on " << *history.rbegin() << ", Back goes to " << *next(history.rbegin()) << '\n';
    return 0;
}
newest first:
1. music
2. mail
3. maps
4. news
5. home
now on music, Back goes to mail

হাঁটা প্রথমে print করেছে music, সবার শেষে যোগ হওয়া page, আর একদম শেষে home, প্রথম page। next(history.rbegin()) সামনের দিকে এক পা সরে mail-এ এসেছে। List bidirectional, তাই উল্টো হাঁটাতেও প্রতি পায়ে খরচ একটা link, সামনের হাঁটার মতোই।

Run in Compiler
Example 3: cbegin দিয়ে শুধু পড়ার হাঁটা

Zara একটা বাজারের তালিকার সব দাম যোগ করে, আর গোনে কয়টা জিনিসের দাম 100-এর বেশি। Loop শুধু পড়ে, তাই হাঁটে cbegin() আর cend() দিয়ে।

#include <iostream>
#include <vector>
using namespace std;

int main() {
    vector<int> price{120, 45, 80, 300, 60};
    int total = 0;
    int over100 = 0;
    for (auto it = price.cbegin(); it != price.cend(); ++it) {
        total += *it;
        if (*it > 100) {
            over100++;
        }
    }
    cout << "total " << total << ", over 100: " << over100 << '\n';
    cout << "first price still " << price[0] << '\n';
    return 0;
}
total 605, over 100: 2
first price still 120

মোট 605, আর দুইটা দাম 100-এর বেশি। এবার Bob প্রতিটা দাম 100-এ বেঁধে দিতে চায়, আর একই cbegin() loop-এর ভেতরে লেখে *it = 100;। Runner-এর flag-এ GCC 12 থেমে যায়: error: assignment of read-only location 'it.__gnu_cxx::__normal_iterator<const int*, std::vector<int> >::operator*()'। নামের ভেতরের const int*-টাই সেই কথা: এই iterator শুধু পড়ে। দাম বদলাতে চাইলে হাঁটো begin() দিয়ে।

Run in Compiler

এটা কোথায় কাজে লাগে

  • LLVM। একটা BasicBlock, মানে compiler-এর instruction-এর একটানা একটা সারি, ওর instruction list-এর উপর begin() আর end() দেয়। LLVM Programmer's Manual দেখায়, এই list-এর উপর হাঁটা যায় for (Instruction &I : BB) দিয়ে, মানে সেই iterator-গুলোর উপর একটা range-for।
  • Python-এর for statement। for x in items প্রথমে iter(items) call করে একটা iterator নেয়, তারপর প্রতিটা element-এর জন্য ওটার উপর next() call করে। কিছু বাকি না থাকলে iterator StopIteration ছুড়ে দেয়, আর সেটাই end()-এর কাজটা করে।
  • Java-র Iterator। Java-র একটা collection দেয় hasNext() আর next()-ওয়ালা একটা Iterator। Java Language Specification collection-এর উপর enhanced for-কে ঠিক এই দুইটা call-এ বদলে দেয়।

যে ভুলগুলো সবাই করে

১. List-এ index দেওয়া।

list<int> l{12, 15, 11, 18};
cout << l[1] - l[0] << '\n';

Runner-এর flag-এ error, প্রতিটা index-এর জন্য একবার: error: no match for 'operator[]' (operand types are 'std::__cxx11::list<int>' and 'int')। List হেঁটে না গিয়ে position 1-এ পৌঁছাতে পারে না, তাই ও কোনো [] দেয় না। Iterator দিয়ে হাঁটো, আর একটা element লাগলে লেখো *next(l.begin(), 1)। তুমি এটা লিখবে, কারণ সবাই প্রথম যে loop শেখে, সেটা v[i]।

২. List-এর iterator-এ একটা সংখ্যা যোগ করা।

list<int> l{12, 15, 11, 18};
auto it = l.begin() + 2;

Runner-এর flag-এ error: error: no match for 'operator+' (operand types are 'std::__cxx11::list<int>::iterator' and 'int')। পেছনে আসে এক পাতা note: candidate: লাইন; কাজের লাইন ওই প্রথমটাই। লেখো auto it = next(l.begin(), 2);। তুমি এটা লিখবে, কারণ vector-এ এই একই লাইন দিব্যি চলে।

৩. List-এর loop < দিয়ে শেষ করা।

for (auto it = l.begin(); it < l.end(); ++it) {
    cout << *it << ' ';
}

Runner-এর flag-এ error: error: no match for 'operator<' (operand types are 'std::_List_iterator<int>' and 'std::__cxx11::list<int>::iterator'), তারপর এক পাতা candidate। দুইটা list node-এর memory-তে কোনো ক্রম নেই, তাই <-এর তুলনা করার কিছুই নেই। লেখো it != l.end(), যেটা প্রতিটা container-এ চলে। তুমি এটা লিখবে, কারণ গোনার loop-এ সবসময় লেখা থাকে i < n।

মাথা খাটাও

Zara একটা vector-এর উল্টো হাঁটা নিয়ে দুইটা কথা print করে।

#include <iostream>
#include <vector>
using namespace std;

int main() {
    vector<int> v{10, 20, 30, 40};
    cout << *v.rbegin() << ' ' << (v.rbegin().base() == v.end()) << '\n';
    return 0;
}

GCC 12 print করে 40 1। base() দেয় সেই সাধারণ iterator-টা, যেটা একটা reverse iterator নিজের ভেতরে রাখে। তাহলে v.rbegin() ভেতরে রাখে v.end(), মানে 40-এর পরের খালি ঘর, অথচ *v.rbegin() পড়ে 40। দুটো একসাথে সত্যি হয় কীভাবে, আর library এটা এভাবে বানাল কেন? দুই-তিন বাক্যে উত্তর দাও।

Reverse iterator যদি নিজের element-এর position-টাই রাখত, তাহলে rend()-কে কোথায় বসতে হতো? ভেবে দেখো, কোনো container-এ প্রথম element-এর আগে আদৌ কোনো position আছে কি না।

অনুশীলন ১সহজ

Maria-র রাস্তা পশ্চিম থেকে পূর্বে গিয়ে সাগরে শেষ হয়েছে। একটা building-এর ছাদ থেকে সাগর দেখা যায়, যদি building-টা ওর পূর্বের প্রতিটা building-এর চেয়ে উঁচু হয়; সমান হলে চলবে না। শেষ building, একদম সাগরের পাশেরটা, সবসময় সাগর দেখে। Maria জানতে চায় কোন কোন building সাগর দেখে। Starter input শেষ না হওয়া পর্যন্ত প্রতিটা উচ্চতা পড়ে; ওর প্রথম লাইনটা বুঝিয়ে বলবে lesson 02।

Input. Building-গুলোর উচ্চতা, মিটারে, পশ্চিম থেকে পূর্বে, মাঝে space বা নতুন লাইন, input শেষ হওয়া পর্যন্ত।

Output. প্রথম লাইনে, কয়টা building সাগর দেখে। দ্বিতীয় লাইনে ওদের position, পশ্চিম থেকে পূর্বে, 1 থেকে গুনে, মাঝে একটা করে space।

Constraints. 1 থেকে 100000টা building, প্রতিটা 1 থেকে 1000000 মিটার উঁচু।

Sample. Input 5 9 4 7 2 6 3 দিলে 4 আর 2 4 6 7। সাগরের পাশের 3 সাগর দেখে, আর 6, 7 আর 9 প্রত্যেকে নিজের পূর্বের সবার চেয়ে উঁচু।

#include <deque>
#include <iostream>
#include <iterator>
#include <vector>
using namespace std;

int main()
{
    ios::sync_with_stdio(false);
    cin.tie(nullptr);

    // Every height until the input ends: lesson 02 explains this line.
    vector<int> height(istream_iterator<int>(cin), {});

    deque<int> view;   // the positions with a sea view, west to east
    // Walk from the sea end with a reverse iterator and keep the tallest
    // height seen so far. A building taller than all of them has the view:
    // put its position, counted from 1, at the front of view.

    cout << view.size() << '\n';
    // Print the positions in view, separated by single spaces.
    return 0;
}

sea-view নামে grade হয়। এটা এই module-এর problem set-এর একটা free problem। Hidden test-এ আছে একটামাত্র building, আর একই উচ্চতার দশটা building। আরও আছে শুধু বাড়তে থাকা 100000টা উচ্চতা, শুধু কমতে থাকা 100000টা, আর মাত্র দশ রকম উচ্চতার 1000টা building।

Run in Compiler
অনুশীলন ২মাঝারি

Zara প্রতিদিনের আবহাওয়ার জন্য একটা করে অক্ষর লেখে: রোদের দিনে S, বৃষ্টির দিনে R, মেঘলা দিনে C। Log-টা ও রাখে একটা std::list<char>-এ, কারণ প্রায়ই ভুলে যাওয়া একটা দিন মাঝখানে ঢোকাতে হয়। এবার ও log-টা ছোট করতে চায়: একই অক্ষরের প্রতিটা টানা সারি হয়ে যাবে অক্ষরটা আর সারির দৈর্ঘ্য।

Input. এক লাইনে অক্ষরগুলো, প্রতিটা S, R বা C।

Output. এক লাইন: প্রতিটা সারি ওর অক্ষর আর দৈর্ঘ্য হিসেবে, সারিগুলোর মাঝে একটা করে space।

Constraints. লাইনে 1 থেকে 100000টা অক্ষর।

Sample. Input SSSRRCS দিলে S3 R2 C1 S1। শেষ S-টা আলাদা একটা সারি, কারণ মাঝে একটা মেঘলা দিন এসে পড়েছে।

#include <iostream>
#include <iterator>
#include <list>
#include <string>
using namespace std;

int main() {
    string line;
    cin >> line;
    list<char> days(line.begin(), line.end());   // one letter per day

    // Walk days with an iterator. For every run of equal letters, print the
    // letter and the length of the run, the runs separated by single spaces.

    return 0;
}

আলাদা করে grade হয় না। একটা iterator রাখো সারির প্রথম দিনে, আর দ্বিতীয় একটাকে ++ দিয়ে এগোও, যতক্ষণ না অন্য অক্ষর বা end() পায়।

Run in Compiler
অনুশীলন ৩কঠিন

Kenji-র ট্যাংকের sensor মাঝে মাঝে লাফ দেয়। ওর vector<int>-এ পাশাপাশি দুইটা মাপের পার্থক্য d-এর বেশি হলে, ও দুটোর মাঝে ওদের গড় ঢোকায়: যোগফলকে 2 দিয়ে ভাগ করে নিচের দিকে গোল করা। ঢোকানো value আর কখনো তুলনা হয় না, তাই প্রতিটা ফাঁকে বড়জোর একটা নতুন মাপ বসে।

Input. এক লাইনে n আর d, তারপর n-টা মাপ।

Output. এক লাইন: সব insert-এর পরের মাপগুলো, মাঝে একটা করে space।

Constraints. 2 <= n <= 1000, 0 <= d <= 1000000, আর প্রতিটা মাপ 0 থেকে 1000000।

Sample. Input 5 5 আর 10 12 30 31 50 দিলে 10 12 21 30 31 40 50। 12 আর 30-এর পার্থক্য 18, তাই ওদের মাঝে বসে 21, আর 31 আর 50-এর মাঝে বসে 40।

#include <cstdlib>
#include <iostream>
#include <iterator>
#include <vector>
using namespace std;

int main() {
    int n, d;
    cin >> n >> d;
    vector<int> readings(n);
    for (int& x : readings) {
        cin >> x;
    }

    // Walk readings with an iterator. Between two neighbours that differ by
    // more than d, insert their average (their sum divided by 2, rounded
    // down). A value you inserted is never compared again. Then print
    // readings on one line, separated by single spaces.

    return 0;
}

আলাদা করে grade হয় না। Lesson যে কথাটা শুধু ইঙ্গিতে বলেছে: insert পুরো vector সরিয়ে ফেলতে পারে, আর তাতে আগে রাখা প্রতিটা iterator invalidated হয়। insert যে iterator ফেরত দেয়, সেটা রাখো, ওটা নতুন value-এর উপর বসে থাকে, আর সেখান থেকে এগোও।

Run in Compiler

সচরাচর যে প্রশ্নগুলো আসে

  • Iterator কি আসলে শুধু একটা pointer?

    Vector-এ GCC 12-এর iterator হলো একটা ছোট class-এ মোড়ানো pointer, 8 byte। List-এর iterator-ও 8 byte, কিন্তু ও রাখে একটা node-এর pointer, আর ওর ++ node-এর link ধরে এগোয়। দুটোই খুলে দেখাবে lesson 05।

  • Loop-এ it != end() কেন লেখে, it < end() কেন না?

    কারণ < আছে শুধু random access iterator-এ, আর != আছে সবার। != দেওয়া loop কিছু না বদলেই vector, list বা set-এ চলে।

  • শেষ element পেতে কি *end() পড়া যায়?

    না। end() হলো শেষ element-এর পরের খালি ঘর, আর সেটা পড়া undefined behaviour। শেষ element হলো *rbegin() বা *prev(end()), আর তাও শুধু container খালি না হলে।

  • Range-for না লিখে কখন iterator loop লিখব?

    Range-for আসলে এমন একটা iterator loop, যেটা compiler তোমার হয়ে begin() থেকে end() পর্যন্ত লিখে দেয়। Position লাগলে, পেছনে হাঁটতে হলে, বা একসাথে দুইটা container লাগলে loop নিজে লেখো। কোন ক্ষেত্রে কোনটা, সেটা গুছিয়ে বলবে lesson 04।

মূল কথা

  • Iterator একটা position, যেটা * দিয়ে নিজের element পড়ে আর ++ দিয়ে সরে; begin() হলো প্রথম element, আর end() শেষটার পরের খালি ঘর।
  • পাঁচটা category, input, output, forward, bidirectional আর random access, বলে দেয় একটা iterator কোন কোন move পারে; + n আর < আছে শুধু random access-এর।
  • vector, deque, array, string আর সাধারণ pointer দেয় random access; list, set আর map দেয় bidirectional; forward_list আর unordered container-গুলো দেয় forward।
  • rbegin() থেকে rend() হাঁটে পেছনে, cbegin() থেকে cend() হাঁটে কিছু না লিখে, আর next-এর সাথে != চলে প্রতিটা container-এ।
  • যে vector capacity ছাড়িয়ে যায়, সে প্রতিটা element সরায়, আর সরানোর আগে রাখা প্রতিটা iterator invalidated হয়ে যায়।
  • আরও গভীরে: Under the Hood, iterator আসলে মোড়ানো একটা pointer (Pro)।

এরপর lesson 02 iterator-এর প্রতিটা operation একটা একটা করে দেখাবে, প্রতিটা category-তে কোনটার খরচ কত, সেটাসহ।

lesson ১ শেষ

শেষ হলে চিহ্ন দিন, অগ্রগতি আপনার সাথে থাকবে।

পরেরটা: iterator-এর প্রতিটা operation, একটা একটা করে, খরচসহ