এই পর্বে যা শিখবেন
- iterator-জোড়ার ঝামেলা, আর
std::ranges-এর algorithm কেন সেটা মেটায় - projection দিয়ে struct-এর একটি member ধরে সাজানো ও খোঁজা
- view, pipe
|আর অলস হিসাব (lazy evaluation) — উপাদান কীভাবে একটি একটি করে টানা হয় - filter, transform, take, drop, reverse, iota, keys, split, join আর C++23-এর zip, enumerate, chunk, slide
ranges::toদিয়ে ফল জমানো,ranges::dangling, মৃত container-এর ওপর view-এর বিপদ, আর AI-এর লেখা পাইপলাইন যাচাই
১ · সমস্যাটা আগে — কী চাই তা বলা, কীভাবে করব তা নয়
গত কয়েকটি পর্বে আপনি মডিউল ৫-এর চারটি বড় যন্ত্র হাতে পেয়েছেন: container (পর্ব ৩০-৩১) ডেটা রাখে, iterator (পর্ব ৩২) সেই ডেটার ওপর দিয়ে হাঁটে, STL algorithm (পর্ব ৩৩) তৈরি সমাধান দেয়, আর lambda (পর্ব ৩৪) algorithm-কে নিয়ম বলে দেয়। এই পর্বে চারটিকে একসাথে জোড়া লাগাব। শুরু করুন একটি সাধারণ কাজ দিয়ে।
একটি কোচিং সেন্টারের ছাত্রছাত্রীদের তালিকা আছে। আপনার দরকার: “যারা পাস করেছে, তাদের নাম, প্রথম ৩ জন।” মুখে বলতে এক লাইন। কিন্তু C++20-এর আগে STL দিয়ে লিখলে কোডটা দাঁড়াত এমন:
// পুরোনো পথ: প্রতিটি ধাপে একটি নতুন vector, প্রতিবার begin/end-এর জোড়া
std::vector<Student> passed;
std::copy_if(all.begin(), all.end(), std::back_inserter(passed),
[](const Student& s) { return s.marks >= 40; });
std::vector<std::string> names;
std::transform(passed.begin(), passed.end(), std::back_inserter(names),
[](const Student& s) { return s.name; });
if (names.size() > 3) names.resize(3); // প্রথম ৩ জনকোডটা ঠিকঠাক চলে, কিন্তু তিনটি সমস্যা চোখে পড়ে। এক, প্রতিটি algorithm-কে শুরু আর শেষ — দুটি iterator আলাদা করে দিতে হয়। এই iterator-এর জোড়া ভুল করে দুটি ভিন্ন container থেকে দিলেও (a.begin(), b.end()) compiler কিছু বলে না, আর ফল হয় undefined behavior. দুই, মাঝখানে দুটি অস্থায়ী vector তৈরি হলো — passed আর names — অথচ শেষে লাগবে মাত্র ৩টি নাম। এক লাখ ছাত্রছাত্রী হলে সবার কপি তৈরি হতো, তারপর বেশিরভাগ ফেলে দেওয়া হতো। তিন, কোড পড়ে “পাস করা, নাম, প্রথম ৩” — এই মূল চিন্তাটা খুঁজে বের করতে হয় যন্ত্রপাতির ভিড়ের মধ্য থেকে।
বাড়ির পানির ফিল্টারের কথা ভাবুন। ওপরে বালির স্তর, তার নিচে কয়লা, তার নিচে সিরামিক ক্যান্ডেল। প্রতিটি স্তর একটি কাজ জানে — কেউ ময়লা আটকায়, কেউ গন্ধ টানে। স্তরগুলো একটির ওপর আরেকটি বসিয়ে দিলেই ফিল্টার তৈরি। আর লক্ষ করুন: ফিল্টার নিজে পানি জমিয়ে রাখে না। কল না খোলা পর্যন্ত এক ফোঁটাও নিচে নামে না; কল খুললে যতটুকু গ্লাসে চান, ঠিক ততটুকুই ফিল্টারের ভেতর দিয়ে বয়ে আসে। গ্লাস ভরে গেলে কল বন্ধ — ট্যাংকের বাকি পানি ফিল্টারের ভেতর দিয়ে যায়ই না। C++-এর view ঠিক এই ফিল্টারের স্তর, আর pipe | হলো স্তরগুলো একটির নিচে আরেকটি বসানো।
C++20-এর ranges লাইব্রেরি দিয়ে একই কাজ লেখা যায় এভাবে — প্রায় মুখের ভাষার ক্রমে:
auto top3 = all
| std::views::filter([](const Student& s) { return s.marks >= 40; })
| std::views::transform(&Student::name)
| std::views::take(3); // এখনো কোনো হিসাব হয়নি, কোনো কপিও নাranges-এর লক্ষ্য হলো কী চান তা একটি পাইপলাইন হিসেবে বর্ণনা করা — “নাও, ছাঁকো, বদলাও, প্রথম কয়েকটি রাখো” — আর কীভাবে লুপ চালাতে হবে, কোথায় অস্থায়ী জায়গা লাগবে, সেসব লাইব্রেরির হাতে ছেড়ে দেওয়া। প্রতিটি ধাপ ছোট, নামযুক্ত, আলাদাভাবে বোঝা যায়। আর পুরো পাইপলাইন অলস: ফল না চাওয়া পর্যন্ত কিছুই ঘটে না।
পুরোনো পথ আর নতুন পথের পার্থক্য ধাপে ধাপে দেখুন:
std::sort(v.begin(), v.end()); // ঠিক
std::sort(v.begin(), w.end()); // compile হয়! কিন্তু ভিন্ন container
// => undefined behavior
std::sort(v.end(), v.begin()); // উল্টো ক্রম — এটিও compile হয়
all (১,০০,০০০ জন)
└─ copy_if ──► passed (ধরুন ৮০,০০০ জনের কপি)
└─ transform ──► names (৮০,০০০টি নামের কপি)
└─ resize(3) ──► শেষে লাগল মাত্র ৩টি
মাঝের দুই vector-এ বাড়তি মেমরি আর বাড়তি সময়
std::ranges::sort(v); // পুরো container একসাথে
std::ranges::sort(v, std::ranges::greater{}); // বড় থেকে ছোট
ভুল জোড়া দেওয়ার সুযোগই নেই — begin আর end
একই জিনিস থেকে লাইব্রেরি নিজে নেয়।
all | filter(পাস) | transform(নাম) | take(3) for লুপ প্রথম নাম চাইলে: পাস করা প্রথম জনকে খুঁজে নাম দেয় তৃতীয় নামের পরে: থামো — বাকি ৯৯,৯০০+ জনকে ছোঁয়াই হয় না মাঝখানে কোনো vector নেই, কোনো নাম কপি হয়নি
- দুটি iterator, দুটি ভুলের সুযোগ। ক্লাসিক STL-এ প্রতিটি algorithm শুরু আর শেষ আলাদা করে নেয়। নমনীয়তা আছে — চাইলে অর্ধেক অংশ সাজানো যায় — কিন্তু ভুল জোড়া দিলে compiler ধরতে পারে না, কারণ দুটি iterator-ই একই টাইপের। এমন ভুল চলার সময় মেমরি নষ্ট করে।
- মাঝের অস্থায়ী container. পুরোনো algorithm-গুলো “এক ধাপ শেষ, তারপর পরের ধাপ” — প্রতিটির ফল কোথাও রাখতে হয়। শেষে ৩টি নাম লাগলেও আগে হাজার হাজার উপাদান কপি হয়। এটাকে বলে eager evaluation — আগেভাগে সব হিসাব করে ফেলা।
- পুরো range একসাথে।
std::ranges-এর algorithm পুরো container নেয়। শুরু আর শেষ একই জিনিস থেকে আসে, তাই জোড়া ভুল হওয়ার উপায় নেই। ভুল টাইপ দিলে compiler পর্ব ৪০-এর concepts দিয়ে স্পষ্ট বার্তা দেয়। - কপি ছাড়া, যতটুকু দরকার ততটুকু। view-এর পাইপলাইনে কোনো মধ্যবর্তী container নেই। লুপ একটি উপাদান চাইলে সেটি সব স্তর পেরিয়ে আসে, তারপর পরেরটি। দরকার ফুরালে কাজও থামে — ঠিক ফিল্টারের কলের মতো।
২ · range কী — begin আর end থাকলেই হলো
C++20-এর ভাষায় একটি range হলো এমন যেকোনো জিনিস, যার ওপর begin() ডাকলে একটি iterator আর end() ডাকলে শেষের চিহ্ন পাওয়া যায়। ব্যস, এটুকুই। পর্ব ৮-এর range-for লুপ যেসব জিনিসের ওপর চলে, প্রায় সবই range: std::vector, std::array, std::string, std::map, সাধারণ C-array, এমনকি std::string_view আর std::span.
একটি সূক্ষ্ম পরিবর্তন আছে: C++20-এ শেষের চিহ্নটি iterator-এর টাইপেরই হতে হবে, এমন বাধ্যবাধকতা নেই। একে বলে sentinel — যার কাজ শুধু বলা “এখানে শেষ”। যেমন একটি C-string-এর শেষ চেনা যায় '\0' দেখে; আগে থেকে শেষের ঠিকানা জানার দরকার নেই। আর std::views::iota(1)-এর মতো অসীম range-এর শেষই নেই — তার sentinel কখনো মেলে না। এই নমনীয়তাই পরে view-এর পাইপলাইন সম্ভব করেছে।
std::ranges-এর algorithm
পর্ব ৩৩-এর প্রায় প্রতিটি algorithm-এর একটি নতুন সংস্করণ আছে std::ranges namespace-এ। এরা পুরো range নেয়, আবার চাইলে পুরোনো মতো iterator-জোড়াও নেয়:
std::vector<int> v{5, 2, 8, 1};
std::ranges::sort(v); // [1, 2, 5, 8]
auto it = std::ranges::find(v, 8); // iterator ফেরত দেয়
bool any = std::ranges::any_of(v, [](int x) { return x > 7; });
auto [lo, hi] = std::ranges::minmax(v); // lo = 1, hi = 8
int n = std::ranges::count_if(v, [](int x) { return x % 2 == 0; });std::ranges::sort-এর সাথে পুরোনো std::sort-এর পার্থক্য শুধু লেখায় নয়। নতুনগুলো concepts দিয়ে শর্ত বেঁধে দেয় — যেমন sort-এর জন্য random-access iterator লাগে, তাই std::list দিলে error-এর শুরুতেই বলা থাকে কোন শর্ত মেলেনি — template-এর গভীর থেকে আসা দুর্বোধ্য বার্তার তুলনায় অনেক পরিষ্কার। এরা আরও তথ্যপূর্ণ ফল ফেরত দেয়, আর দুটি নতুন সুবিধা আনে: projection (পরের অংশ) আর dangling-সুরক্ষা (৭ নম্বর অংশ)।
সব algorithm-এর ranges সংস্করণ নেই। যেমন <numeric>-এর std::accumulate-এর সরাসরি ranges রূপ আসেনি; C++23 এনেছে std::ranges::fold_left, যা একই কাজ করে। আর parallel execution policy (std::execution::par) C++23 পর্যন্ত শুধু পুরোনো সংস্করণগুলো নেয়। নতুন কোডে ranges সংস্করণ আগে খুঁজুন, না পেলে পুরোনোটি নিশ্চিন্তে ব্যবহার করুন।
৩ · projection — “এই member দেখে সাজাও”
ধরুন struct Student { std::string name; int marks; };-এর একটি vector আছে, আর নম্বর অনুযায়ী সাজাতে চান। পুরোনো পথে তুলনার একটি lambda লিখতে হতো:
// পুরোনো পথ: পুরো তুলনা হাতে লেখা
std::sort(s.begin(), s.end(),
[](const Student& a, const Student& b) { return a.marks < b.marks; });
// ranges পথ: তুলনা ডিফল্ট (<), শুধু বলে দিন কী তুলনা করতে হবে
std::ranges::sort(s, {}, &Student::marks);দ্বিতীয় লাইনের তৃতীয় argument-টিকে বলে projection. algorithm প্রতিটি উপাদান তুলনা বা পরীক্ষা করার আগে সেটিকে এই projection-এর মধ্য দিয়ে পাঠায়। &Student::marks হলো একটি pointer to member — এর মানে “যেকোনো Student থেকে তার marks বের করো”। মাঝের {} মানে তুলনার নিয়ম ডিফল্ট রাখো — std::ranges::less, অর্থাৎ ছোট থেকে বড়। উল্টো চাইলে std::ranges::greater{}.
পুরোনো lambda-তে একই কথা দুইবার লিখতে হতো (a.marks আর b.marks), আর একটিতে ভুল করে a.marks < a.marks লিখে ফেলাও খুব সহজ ছিল। projection-এ “কী তুলনা করব” আর “কীভাবে তুলনা করব” আলাদা হয়ে যায়। projection শুধু sort-এ নয় — find, max_element, count_if-সহ প্রায় সব ranges algorithm-এ কাজ করে:
auto it = std::ranges::find(s, "Mitu", &Student::name); // নাম দিয়ে খোঁজা
auto top = std::ranges::max_element(s, {}, &Student::marks); // সর্বোচ্চ নম্বর
// projection একটি lambda-ও হতে পারে:
std::ranges::sort(s, {}, [](const Student& x) { return x.name.size(); });একই vector তিনভাবে সাজালে projection কী দেখে আর ফল কী দাঁড়ায়, ধাপে ধাপে দেখুন:
1std::vector<Student> s{{"Rafi",72},{"Mitu",91},{"Tanha",64},{"Joy",85}};2std::ranges::sort(s, {}, &Student::marks);3std::ranges::sort(s, std::ranges::greater{}, &Student::marks);4std::ranges::sort(s, {}, &Student::name);
| projection যা দেখে | |
| তুলনার নিয়ম | |
| vector s এখন |
- ডেটা তৈরি। চারজন ছাত্রছাত্রী, প্রত্যেকের নাম আর নম্বর। কারও নম্বর সমান নয় — ইচ্ছা করেই, কারণ
sortstable নয়: সমান মানের উপাদানের আগের ক্রম রাখার নিশ্চয়তা দেয় না। সেটা দরকার হলেstd::ranges::stable_sort. - marks দেখে, ছোট থেকে বড়। sort যখনই দুজনকে তুলনা করতে চায়, আগে projection দিয়ে প্রত্যেকের marks বের করে, তারপর
<দিয়ে তুলনা করে। কিন্তু সরানো হয় পুরো Student object — শুধু নম্বর নয়। ফল: Tanha (64), Rafi (72), Joy (85), Mitu (91). - একই projection, উল্টো নিয়ম। শুধু তুলনার নিয়ম বদলে
greaterদিলেই বড় থেকে ছোট — মেধাতালিকা। lambda-য়<উল্টে>লেখার মতো ছোট ভুলের জায়গাই নেই। - এবার নাম দেখে। projection বদলে
&Student::nameদিলে std::string-এর তুলনা খাটে — অক্ষরের ক্রমে: Joy, Mitu, Rafi, Tanha. একটি শব্দ বদলেই সাজানোর ভিত্তি বদলে গেল; তুলনার কোড একবারও লিখতে হয়নি।
৪ · view আর pipe — অলস পাইপলাইন
ranges algorithm-গুলো (sort, find…) সঙ্গে সঙ্গে কাজ করে, ফল দেয়। কিন্তু ranges লাইব্রেরির আসল নতুনত্ব আরেক জাতের জিনিস: view. view হলো একটি range, যা অন্য একটি range-এর ওপর বসে তার উপাদানগুলো বদলে দেখায় — ছেঁকে, রূপান্তর করে, কেটে — কিন্তু নিজে কোনো উপাদান জমিয়ে রাখে না। view তৈরি করার যন্ত্রগুলোকে বলে range adaptor, আর এরা থাকে std::views namespace-এ:
views::filter
একটি শর্ত নেয়; শুধু যে উপাদান শর্ত পূরণ করে, সেগুলো দেখায়। বাকিগুলো বাদ — ফিল্টারের বালির স্তর।
ছাঁকা · keep ifviews::transform
একটি ফাংশন নেয়; প্রতিটি উপাদানকে সেই ফাংশনের ফল হিসেবে দেখায়। পয়সা থেকে টাকা, Student থেকে নাম।
রূপান্তর · mapviews::take
একটি সংখ্যা নেয়; প্রথম ততগুলো উপাদান দেখিয়ে থেমে যায়। গ্লাস ভরে গেলে কল বন্ধ।
প্রথম n টিএকাধিক view জোড়া দেওয়ার জন্য আছে pipe অপারেটর | — pipe syntax. r | views::filter(f) আর views::filter(r, f) একই জিনিস; কিন্তু pipe-এ ধাপগুলো বাঁ থেকে ডানে, যে ক্রমে ডেটা বয়ে যায় সেই ক্রমেই পড়া যায়। Linux বা macOS-এর terminal-এ যাঁরা cat file | grep error | head -5 লিখেছেন, তাঁদের কাছে এটা চেনা লাগবে — নাম আর ধারণা সেখান থেকেই এসেছে।
অলস মানে সত্যিই অলস
view-এর সবচেয়ে গুরুত্বপূর্ণ বৈশিষ্ট্য হলো lazy evaluation — অলস হিসাব। পাইপলাইন লেখার মুহূর্তে কিছুই হিসাব হয় না; তৈরি হয় শুধু একটি “রেসিপি”। কেউ একটি উপাদান চাইলে (লুপের পরের পাক, বা *it), তখন ঠিক সেই একটি উপাদান পেতে যতটুকু কাজ লাগে, ততটুকুই হয়। নিচের প্রোগ্রামে filter আর transform-এর ভেতরে একটি করে println বসানো আছে, যাতে কে কখন কাজ করে তা চোখে দেখা যায়। আর উৎস হলো std::views::iota(1) — ১, ২, ৩, … করে অসীম পর্যন্ত চলা সংখ্যার ধারা:
#include <print>
#include <ranges>
int main() {
auto is_even = [](int n) {
std::println(" filter sees {}", n);
return n % 2 == 0;
};
auto square = [](int n) {
std::println(" transform gets {}", n);
return n * n;
};
auto pipe = std::views::iota(1) | std::views::filter(is_even)
| std::views::transform(square) | std::views::take(2);
std::println("pipeline built, nothing ran yet");
for (int x : pipe) std::println("loop got {}", x);
}আউটপুটের প্রথম লাইন pipeline built, nothing ran yet — অসীম একটি ধারার ওপর পাইপলাইন বানানো হলো, অথচ একটি সংখ্যাও তৈরি হয়নি, কোনো lambda ডাকা হয়নি। তারপর লুপ চলতেই: filter sees 1, filter sees 2, transform gets 2, loop got 4; আবার filter sees 3, filter sees 4, transform gets 4, loop got 16; আর শেষে filter sees 5, filter sees 6, তারপর থামা। মোট ১১ লাইন। লক্ষ করুন, প্রতিটি সংখ্যা পুরো পাইপ পেরিয়ে লুপে পৌঁছায়, তারপর পরের সংখ্যা শুরু হয় — “সব filter, তারপর সব transform” নয়। iota অসীম হলেও প্রোগ্রাম থেমেছে, কারণ take(2) দুটি পেয়েই আর চায়নি। শেষের ৫ আর ৬-এর রহস্য নিচের ধাপে।
লুপের প্রতিটি পাকে কোন স্তর কী করছে, আর কোন উপাদান কখনো কাজেই লাগে না — ধাপে ধাপে দেখুন। প্রতিটি ধাপে আগে নিজে অনুমান করুন, পরের আউটপুট লাইন কী হবে:
1auto pipe = iota(1) | filter(is_even)2 | transform(square) | take(2);3println("pipeline built, nothing ran yet");4for (int x : pipe)5 println("loop got {}", x);
| iota তৈরি করল | |
| filter-এর রায় | |
| transform | |
| take-এর বাকি গোনা |
- শুধু রেসিপি। প্রথম তিন লাইন চলল, কিন্তু iota একটি সংখ্যাও বানায়নি, কোনো lambda ডাকা হয়নি।
pipeভ্যারিয়েবলটি একটি ছোট object — ভেতরে আছে “কোথা থেকে নেব” আর দুটি lambda, ব্যস। অসীম ধারার ওপর পাইপলাইন বানানো সম্ভব হলো ঠিক এই কারণেই। - প্রথম উপাদানের খোঁজ। range-for লুপ শুরুতে
begin()চায়। take সেটা চায় transform-এর কাছে, transform চায় filter-এর কাছে। filter তখন iota থেকে টানতে শুরু করে: ১ — শর্তে বাদ; ২ — পাস। filter থামল প্রথম যোগ্য উপাদানে। transform এখনো কিছু করেনি — কারণ মান এখনো কেউ চায়নি, শুধু অবস্থান খোঁজা হয়েছে। - মান চাওয়া হলো। লুপ
*itদিয়ে মানটি চাইতেই transform এবার ২-কে বর্গ করে ৪ দিল, আর লুপ ছাপলloop got 4. মান তৈরি হয় ঠিক যখন দরকার, তার আগে নয়। - পরের পাক।
++itআবার পাইপ বেয়ে নেমে যায়: iota দেয় ৩ (বাদ), ৪ (পাস); transform বানায় ১৬। take-এর গোনা এক কমে হলো ১, আর লুপ ছাপলloop got 16— এটিই দ্বিতীয় ও শেষ দরকারি উপাদান। - যে উপাদান কাজে লাগল না। দুটি পাওয়া হয়ে গেছে, কিন্তু লুপের
++itনিয়মমতো আরেকবার এগোয়, আর take ভেতরের iterator-কে এগিয়ে দিয়ে গোনা কমায় — ফলে filter দেখে ৫ আর ৬. তারপর গোনা ০, লুপ শেষ। ৬ filter পাস করেছিল, কিন্তু transform তাকে কখনো পায়নি; আর ৭, ৮, ৯… iota কখনো বানায়ইনি। এই এক ধাপ বাড়তি টানাtake-এর পরিচিত স্বভাব — সাধারণত নিরীহ, কিন্তু শর্তের lambda-য় পার্শ্বপ্রতিক্রিয়া (যেমন ফাইল পড়া) থাকলে চমকে দিতে পারে।
১. view কাজ করে না, কাজ করায় — হিসাব হয় শুধু উপাদান চাওয়ার মুহূর্তে। ২. view সাধারণত ডেটার মালিক নয় — container-এর দিকে তাকিয়ে থাকে, ঠিক পর্ব ১৪-এর string_view-এর মতো। ৩. view কপি করা সস্তা — ভেতরে কয়েকটি pointer আর lambda; লাখ উপাদানের ওপর বসা view কপি করলেও উপাদান কপি হয় না। তাই view ফাংশনে by value পাঠানোই রীতি।
৫ · view-এর বাক্স — কোনটি কী করে
standard library-তে কয়েক ডজন view আছে। প্রতিদিনের কাজে লাগে এর অল্প কয়েকটি। নিচের তালিকায় পাশে দেখানো ফলগুলো v = {1, 2, 3, 4, 5, 6} ধরে। শেষের চারটি C++23 থেকে; এই পর্বের সব উদাহরণ আমরা GCC-র নতুন সংস্করণে (-std=c++23) পরীক্ষা করেছি — পুরোনো compiler-এ C++23-এর view না-ও থাকতে পারে।
| view | কাজ | উদাহরণ ও ফল |
|---|---|---|
| filter | শর্ত মিললে রাখে | v | filter(বিজোড়) → 1 3 5 |
| transform | প্রতিটিকে রূপান্তর | v | transform(x*10) → 10 20 … 60 |
| take / drop | প্রথম n টি রাখে / বাদ দেয় | take(2) → 1 2; drop(4) → 5 6 |
| take_while / drop_while | শর্ত যতক্ষণ সত্য | take_while(x < 3) → 1 2 |
| reverse | উল্টো ক্রমে দেখায় | v | reverse → 6 5 … 1 |
| iota | সংখ্যার ধারা তৈরি | iota(1, 4) → 1 2 3 (শেষটি বাদ) |
| keys / values | pair বা map-এর প্রথম / দ্বিতীয় অংশ | marks | keys → সব নাম |
| split | একটি চিহ্নে টুকরো করে | "a,b,c" | split(',') → a, b, c |
| join | range-এর range সমান করে এক সারি | {{1,2},{3}} | join → 1 2 3 |
| enumerate (C++23) | সাথে ক্রমিক নম্বর | (0, 1), (1, 2), … |
| zip (C++23) | দুই range পাশাপাশি জোড়া | zip(names, marks) → (Rafi, 72), … |
| chunk (C++23) | n টি করে দলে ভাগ | chunk(2) → [1,2] [3,4] [5,6] |
| slide (C++23) | n মাপের সরে-চলা জানালা | slide(3) → [1,2,3] [2,3,4] … |
তিনটি বাস্তব ব্যবহার
প্রথম ১০টি জোড় সংখ্যার বর্গ। iota একটি অসীম ধারা দেয়; জোড় বানানো আর বর্গ করা দুটি transform; take(10) থামায়। কোনো লুপ-সূচক নেই, কোনো “কতদূর যাব” হিসাব নেই:
auto even_squares = std::views::iota(1)
| std::views::transform([](int n) { return 2 * n; }) // 2, 4, 6, ...
| std::views::transform([](int n) { return n * n; }) // 4, 16, 36, ...
| std::views::take(10);
std::println("{}", even_squares); // [4, 16, 36, 64, 100, 144, 196, 256, 324, 400]শেষ লাইনটি খেয়াল করুন: C++23 থেকে std::println (পর্ব ৭) একটি পুরো range সরাসরি ছাপতে পারে — বর্গবন্ধনী আর কমাসহ। vector, map, এমনকি একটি view-ও। ডিবাগ করার সময় এটা খুব কাজের; আগে এজন্য একটি লুপ লিখতে হতো। (string-এর range ছাপলে প্রতিটি লেখা উদ্ধৃতিচিহ্নসহ আসে: ["rice", "dal"].)
কমা দিয়ে আলাদা করা একটি লাইন ভাঙা। CSV ফাইলের একটি লাইন বা একটি ফর্মের ইনপুট প্রায়ই এমন আসে। split প্রতিটি টুকরোকে একটি ছোট sub-range হিসেবে দেয়, যা থেকে সরাসরি string_view বানানো যায় — কোনো অক্ষর কপি না করে:
std::string_view line = "Dhaka,Rajshahi,Khulna";
for (auto part : line | std::views::split(','))
std::println("city: {}", std::string_view(part)); // Dhaka, Rajshahi, Khulna
// টুকরোগুলো রেখে দিতে চাইলে (C++23):
auto cities = line | std::views::split(',')
| std::ranges::to<std::vector<std::string>>();একটি map-এর শুধু চাবি বা শুধু মান। পর্ব ৩১-এর std::map<std::string, int> marks থেকে সব নাম চাইলে marks | std::views::keys, সব নম্বর চাইলে marks | std::views::values. আগে এর জন্য structured binding-এর লুপ লিখে একটি অংশ ফেলে দিতে হতো।
pipe-এ ধাপের ক্রম বদলালে অর্থ বদলায়। iota(1, 11) | take(5) | filter(জোড়) মানে “প্রথম ৫টি সংখ্যা নাও, তার মধ্যে জোড়গুলো” → 2 4. আর iota(1, 11) | filter(জোড়) | take(5) মানে “জোড়গুলোর মধ্যে প্রথম ৫টি” → 2 4 6 8 10. মুখে বলে দেখুন, কোনটি আপনি আসলে চাইছেন — তারপর সেই ক্রমেই লিখুন।
৬ · বাস্তব পাইপলাইন আর ফল জমানো — ranges::to
এবার মডিউল ৫-এর সব যন্ত্র একসাথে। একটি অনলাইন দোকানের অর্ডারের তালিকা: প্রতিটি অর্ডারের id, টাকা পরিশোধ হয়েছে কি না, আর মোট দাম — পয়সায়, যাতে ভগ্নাংশের ঝামেলা না থাকে। দরকার: পরিশোধিত অর্ডারগুলোর মধ্যে সবচেয়ে বড় ৫টির দাম, টাকায়, আর তাদের যোগফল। ব্যবহার হয়েছে container (vector), algorithm (sort), projection, তিনটি view, lambda, আর শেষে ফল জমানো:
#include <algorithm>
#include <functional>
#include <print>
#include <ranges>
#include <vector>
struct Order { int id; bool paid; int paisa; };
int main() {
std::vector<Order> orders{{101, true, 45000}, {102, false, 99000},
{103, true, 120050}, {104, true, 30000}, {105, false, 15000},
{106, true, 87500}, {107, true, 64000}, {108, true, 9900}};
std::ranges::sort(orders, std::ranges::greater{}, &Order::paisa);
auto top = orders
| std::views::filter([](const Order& o) { return o.paid; })
| std::views::transform([](const Order& o) { return o.paisa / 100; })
| std::views::take(5);
std::println("top paid (taka): {}", top);
auto saved = top | std::ranges::to<std::vector>();
std::println("saved {} values, total = {}", saved.size(),
std::ranges::fold_left(saved, 0, std::plus{}));
}আউটপুট দুই লাইন: top paid (taka): [1200, 875, 640, 450, 300] আর saved 5 values, total = 3465. প্রথমে projection দিয়ে পুরো vector দামের বড় থেকে ছোট ক্রমে সাজানো হলো — sort একটি algorithm, এটি সঙ্গে সঙ্গে কাজ করে আর vector-টিকেই বদলায়। তারপর পাইপলাইন: অপরিশোধিত 102 (৯৯০ টাকা) বাদ পড়ল filter-এ, transform পয়সাকে টাকা বানাল (পূর্ণসংখ্যার ভাগ, তাই 120050 → 1200), take থামল ৫টিতে। ছোট্ট 108 অর্ডারটি transform পর্যন্ত পৌঁছায়নি — take-এর সেই এক ধাপ বাড়তি টানায় filter শুধু তাকে পরীক্ষা করে দেখেছে। std::ranges::to<std::vector>() অলস view-কে একটি আসল vector-এ জমা করল — এবার ডেটার মালিক saved. আর fold_left (C++23) সবগুলো যোগ করল।
খেয়াল করুন, sort-কে পাইপের ভেতরে রাখা যায়নি — আগে আলাদা লাইনে করতে হয়েছে। কারণ সাজাতে হলে সব উপাদান একসাথে দেখতে হয়, আর অলস view তা পারে না। “কোনটি সবচেয়ে বড়” জানতে শেষ উপাদান পর্যন্ত না দেখে উপায় নেই। তাই নিয়ম: যে কাজে পুরো ডেটা একসাথে লাগে (sort, unique, shuffle), সেটি algorithm; যে কাজ একটি একটি করে করা যায় (ছাঁকা, রূপান্তর, কাটা), সেটি view.
view থেকে আসল container — materialize করা
view-এর ফল কখনো কখনো ধরে রাখতে হয় — ফাংশন থেকে ফেরত দিতে, একাধিকবার ব্যবহার করতে, বা মূল container বদলানোর আগে। একে বলে materialize করা। C++23 থেকে সবচেয়ে সহজ পথ std::ranges::to:
auto v1 = view | std::ranges::to<std::vector>(); // element-এর টাইপ নিজে বোঝে
auto v2 = view | std::ranges::to<std::vector<long>>(); // টাইপ নিজে বলে দেওয়া
auto s = chars | std::ranges::to<std::string>();
// C++20-এ ranges::to নেই — একটি সাধারণ লুপই সবচেয়ে পরিষ্কার:
std::vector<int> v3;
for (int x : view) v3.push_back(x);C++20-এ ranges এল, কিন্তু ranges::to এল এক সংস্করণ পরে — তাই অনেক C++20 কোডে আর অনলাইনের পুরোনো উত্তরে আপনি হাতে লেখা লুপ দেখবেন। দুটিই ঠিক। শুধু একটি জিনিস মনে রাখুন: std::vector<int> v(view.begin(), view.end()) সব view-এ চলে না, কারণ কিছু view-এর begin আর end-এর টাইপ আলাদা (সেই sentinel). লুপ বা ranges::to সবসময় চলে।
৭ · view আর lifetime — যেখানে পাইপ ফেটে যায়
view ডেটার মালিক নয় — এটাই তার শক্তি, আর এটাই তার বিপদ। পর্ব ১৮-এ আপনি দেখেছেন, dangling reference বা dangling view মানে এমন কিছুর দিকে তাকিয়ে থাকা, যা ইতিমধ্যে ধ্বংস হয়েছে। ranges-এ এই ভুলের দুটি রূপ আছে — একটি লাইব্রেরি নিজে আটকায়, অন্যটি আপনাকে আটকাতে হয়।
লাইব্রেরি যা আটকায় — ranges::dangling
একটি ফাংশন vector ফেরত দেয়, আর আপনি সেই ফলের ওপর সরাসরি find বা max_element ডাকলেন:
std::vector<int> prices(); // নতুন vector ফেরত দেয়
auto it = std::ranges::max_element(prices()); // compile হয়, কিন্তু...
int m = *it; // error: no match for 'operator*'
// (operand type is 'std::ranges::dangling')prices()-এর ফল একটি temporary object — লাইনটি শেষ হতেই সেটি ধ্বংস। পুরোনো std::max_element হলে একটি iterator ফেরত আসত, যা মৃত মেমরির দিকে তাকিয়ে আছে, আর *it চুপচাপ undefined behavior হতো। ranges সংস্করণ বুঝে ফেলে যে আপনি একটি temporary পাঠিয়েছেন, তাই iterator-এর বদলে ফেরত দেয় std::ranges::dangling নামের একটি ফাঁকা টাইপ। এটি দিয়ে কিছুই করা যায় না — dereference করতে গেলেই compile error. চলার সময়ের একটি নীরব বাগ হয়ে গেল চোখের সামনের error. সমাধান: আগে ফলটি একটি নামযুক্ত ভ্যারিয়েবলে রাখুন — auto p = prices(); — তারপর তার ওপর algorithm ডাকুন।
(যেসব range নিজে ডেটার মালিক নয়, যেমন std::string_view বা std::span, সেগুলো temporary হলেও তাদের iterator নিরাপদ, কারণ ডেটা অন্য কোথাও বেঁচে আছে। এমন range-কে বলে borrowed range, আর এদের বেলায় dangling ফেরত আসে না।)
যা আপনাকে আটকাতে হয় — মৃত container-এর ওপর view
নিচের ফাংশনটি দেখতে নিরীহ। এটি একটি view ফেরত দেয়, যাতে ডাকার জায়গায় অলসভাবে পাস নম্বরগুলো পাওয়া যায়। কিন্তু view যে container-এর দিকে তাকিয়ে আছে, সেটি ফাংশনের local ভ্যারিয়েবল (এই কোড চালাবেন না):
1auto passed_marks() {2 std::vector<int> marks{72, 35, 91, 28};3 return marks | std::views::filter(pass);4} // marks এখানে ধ্বংস5auto v = passed_marks();6for (int m : v) std::println("{}", m);
| marks | |
| view-এর ভেতরে | |
| ফেরত আসা জিনিস |
- local vector জন্মাল। ফাংশনের ভেতরে
marksতৈরি হলো; তার চারটি সংখ্যা আছে heap-এ। এর মালিক এই ফাংশনের stack frame — ফাংশন শেষ হলে সে-ও শেষ। - view তৈরি — কপি ছাড়া।
marks | filter(pass)একটি view বানাল।marksনামযুক্ত ভ্যারিয়েবল (lvalue), তাই view তার দিকে শুধু একটি ঠিকানা রাখে (ভেতরে ref_view) — সংখ্যাগুলো কপি করে না। এখনো সব ঠিক আছে, কারণ marks জীবিত। - ফাংশন শেষ, মালিক মৃত। return-এর পরে
marks-এর destructor চলল, heap-এর মেমরি ফেরত গেল। কিন্তু view-টি কপি হয়ে বাইরে এল — ভেতরে সেই পুরোনো ঠিকানা নিয়ে। compiler কোনো error দেয়নি; টাইপের দিক থেকে সব বৈধ। - অলসতার দাম। এবার লুপ উপাদান চাইল, আর view প্রথমবার আসল কাজ শুরু করল — মৃত মেমরি পড়ে। এটি undefined behavior: কখনো ঠিক ফল দেখায় (সবচেয়ে বিপজ্জনক — পরীক্ষায় ধরা পড়ে না), কখনো আবর্জনা, কখনো crash. AddressSanitizer (
-fsanitize=address) সাধারণত একে heap-use-after-free হিসেবে ধরে। - সমাধান: মালিককে ফেরত দিন। ফাংশন থেকে view নয়, ডেটা ফেরত দিন:
return marks | std::views::filter(pass) | std::ranges::to<std::vector>();— ফেরত টাইপstd::vector<int>. অথবা container-কে বাইরে রাখুন আর ফাংশন সেটি reference হিসেবে নিক। নিয়ম: view-এর আয়ু কখনো তার উৎসের চেয়ে বেশি হবে না।
একটি আশার খবর: ২০২১ সালে গৃহীত একটি সংশোধনী (P2415, যা C++20-এর ওপরও প্রয়োগ করা হয়েছে) অনুযায়ী, pipe-এর বাঁ দিকে যদি সরাসরি একটি temporary container থাকে — যেমন make_marks() | views::filter(pass) — তাহলে view সেটিকে move করে নিজের ভেতরে নেয় (owning_view), আর তখন view-ই মালিক। তাই এই রূপটি নিরাপদ। বিপদ থাকে নামযুক্ত ভ্যারিয়েবলে, যার আয়ু view-এর আগে শেষ হয় — ঠিক উপরের মতো। আর মনে রাখবেন, এই নিয়ম পুরোনো compiler-এ না-ও থাকতে পারে।
view চলার সময় container বদলানো
দ্বিতীয় বিপদটিও পর্ব ৩০-৩২-এর পরিচিত iterator invalidation. একটি view একটি vector-এর দিকে তাকিয়ে আছে, আর লুপের ভেতরে আপনি সেই vector-এ push_back করলেন। vector বড় জায়গায় সরে গেলে view-এর iterator পুরোনো, ফেরত দেওয়া মেমরির দিকে তাকিয়ে থাকে — আবার undefined behavior. নিয়ম: view দিয়ে পড়ার সময় উৎস container-এর আকার বদলাবেন না। বদলাতে হলে আগে ranges::to দিয়ে আলাদা কপি নিন, অথবা C++20-এর std::erase_if-এর মতো ফাংশন ব্যবহার করুন।
filter view কেন const দিয়ে চলে না
একটি অবাক-করা নিয়ম, যা AI-এর কোডেও প্রায়ই compile error আনে: void show(const auto& r) ফাংশনে একটি filter view পাঠালে GCC বলে passing 'const ...filter_view...' as 'this' argument discards qualifiers. কারণ filter view প্রথমবার begin() ডাকার সময় প্রথম যোগ্য উপাদান খুঁজে বের করে আর সেই অবস্থান মনে রাখে (cache করে), যাতে পরের বার আবার খুঁজতে না হয়। মনে রাখা মানে নিজেকে বদলানো, তাই begin() const হতে পারে না। সমাধান সহজ: view-কে by value নিন (void show(auto r)) — view কপি সস্তা — অথবা forwarding reference (auto&& r). এর পেছনের নকশার কারণ ভাবনার প্রশ্ন ৩-এ।
একই cache-এর একটি সূক্ষ্ম পরিণতি: একটি filter view একবার ঘোরানোর পরে উৎস container-এর উপাদান এমনভাবে বদলাবেন না যে আগের পাস করা প্রথম উপাদানটি এখন আর শর্ত পূরণ করে না — view তখনো পুরোনো, মনে রাখা অবস্থান থেকে শুরু করবে। standard-এর ভাষায় এটি নিয়মভঙ্গ। নিরাপদ অভ্যাস: view বানান, ব্যবহার করুন, ফেলে দিন — দীর্ঘদিন ধরে রেখে দেবেন না।
৮ · গতি, পাঠযোগ্যতা, আর কখন সাধারণ লুপই ভালো
গতি। view-এর পাইপলাইন optimization চালু করে (-O2) compile করলে সাধারণত হাতে লেখা লুপের কাছাকাছি গতিতে চলে, কারণ প্রতিটি view একটি template, আর lambda-গুলো inline হয়ে যায় — শেষে মেশিন কোডে প্রায় একটি সাধারণ লুপই থাকে। মধ্যবর্তী vector না থাকায় পুরোনো copy_if-transform পথের চেয়ে প্রায়ই দ্রুতও। তবে কিছু সতর্কতা আছে। optimization ছাড়া (debug build) ranges-এর কোড লক্ষণীয়ভাবে ধীর হতে পারে, কারণ স্তরগুলো আলাদা ফাংশন-ডাক হিসেবে থেকে যায়। কিছু জোড়া — যেমন filter-এর পরে reverse, বা উপরের take-এর এক ধাপ বাড়তি টানা — বাড়তি কাজ করে। আর template-ভারী কোড compile হতে বেশি সময় নেয়, error message-ও দীর্ঘ হয়। “ranges ধীর” বা “ranges দ্রুত” — কোনোটাই অন্ধভাবে বিশ্বাস করবেন না; দরকার হলে পর্ব ৪৯-এর মতো মেপে দেখুন।
পাঠযোগ্যতা। ranges-এর আসল লাভ পরিষ্কার কোড। কিন্তু pipe-এর নেশা লাগলে উল্টো ফল হয়। আট ধাপের একটি পাইপলাইন, যার প্রতিটি ধাপে তিন লাইনের lambda, আর মাঝখানে zip-chunk-join — পড়তে গিয়ে পাঠককে মাথায় পুরো ডেটার আকার কল্পনা করতে হয়। তুলনা করুন:
pipe ভালো যখন
২-৪টি সরল ধাপ, প্রতিটির নাম বোঝায় কী হচ্ছে; lambda এক লাইনের বা নামযুক্ত; ডেটা একটি দিকেই বয়ে যায়; ফল একবারই ব্যবহার হবে।
ছাঁকো → বদলাও → কাটোলুপ ভালো যখন
একই পাকে কয়েকটি ভিন্ন হিসাব (যোগফল, সর্বোচ্চ, গোনা একসাথে); মাঝপথে break বা error সামলানো; জটিল অবস্থা; বা ডিবাগারে ধাপে ধাপে দেখা দরকার।
for + if — কোনো লজ্জা নেইলম্বা পাইপলাইন ভাঙার একটি সহজ কৌশল: মাঝের অংশগুলোকে নাম দিন। view কপি সস্তা আর অলস, তাই auto paid = orders | views::filter(is_paid); লিখে তারপর auto top = paid | views::transform(to_taka) | views::take(5); — একই কাজ, একই গতি, কিন্তু প্রতিটি নাম একটি মন্তব্যের কাজ করে। lambda-কেও নাম দিন (is_paid, to_taka), পর্ব ৩৪-এর মতো।
ranges-এর গল্প প্রায় এক দশকের। STL-এর নকশাকার Alexander Stepanov iterator-এর জোড়াকেই মূল ধারণা করেছিলেন, আর C++98 সেটাই নেয়। ২০০০-এর দশকে Boost.Range লাইব্রেরি range-কে একটি জিনিস হিসেবে ধরার পথ দেখায়। তারপর Eric Niebler ২০১৩-১৪ সাল থেকে range-v3 নামে একটি লাইব্রেরি লেখেন, যেখানে view, pipe আর অলস পাইপলাইন সব ছিল। এটি প্রথমে একটি Technical Specification হিসেবে মানদণ্ডে ঢোকে (Ranges TS, ২০১৭), তারপর Niebler, Casey Carter আর আরও কয়েকজনের “One Ranges Proposal”-এর মাধ্যমে C++20-এর অংশ হয়। C++20-এর ranges দাঁড়িয়ে আছে concepts-এর ওপর (পর্ব ৪০), আর C++23 যোগ করে ranges::to, zip, enumerate, chunk, slide, fold এবং range ছাপার সুবিধা।
C-তে ranges বা view নেই — প্রতিটি ধাপ হাতে লেখা লুপ, আর একটি অ্যারে মানে সবসময় দুটি জিনিস: শুরুর pointer আর দৈর্ঘ্য। qsort-এ আপনি ঠিকানা, সংখ্যা, প্রতিটির মাপ আর একটি তুলনার ফাংশন — চারটি জিনিস আলাদা করে দেন, আর একটিও ভুল হলে compiler কিছু বলে না। C++-এর range সেই সব একটি টাইপে বেঁধে দেয়, আর projection তুলনার ফাংশনের বড় অংশ মুছে দেয়। C-তে তুলনার ফাংশন আর callback কীভাবে কাজ করে, দেখতে C কোর্সের ফাংশন পয়েন্টার ও callback পর্ব পড়ুন।
- view কী, আর container থেকে এর পার্থক্য কী? — view একটি অলস range, যা সাধারণত ডেটার মালিক নয়; উপাদান চাওয়ার সময় হিসাব করে, আর কপি করা সস্তা। container নিজে ডেটা রাখে।
std::sortআরstd::ranges::sort-এর পার্থক্য? — ranges সংস্করণ পুরো range নেয়, concepts দিয়ে টাইপ যাচাই করে, projection নেয়, আর temporary পেলে dangling-সুরক্ষা দেয়।- views কি eager না lazy? — lazy; পাইপলাইন লেখার সময় কিছু হিসাব হয় না, লুপ বা
ranges::toউপাদান চাইলে তবেই। std::ranges::danglingকেন আসে? — iterator-ফেরত-দেওয়া ranges algorithm-কে temporary (borrowed নয় এমন) range দিলে, যাতে মৃত container-এর iterator ব্যবহার compile error হয়।- view থেকে vector বানাবেন কীভাবে? — C++23-এ
view | std::ranges::to<std::vector>(); C++20-এ একটি লুপে push_back.
৯ · AI-এর যুগে — AI-এর লেখা পাইপলাইন যাচাই
AI টুল ranges-এর কোড লিখতে ভালোবাসে, কারণ pipe-এর কোড দেখতে আধুনিক আর সংক্ষিপ্ত। কিন্তু ranges এসেছে মাত্র C++20-এ, আর C++23-এর অংশগুলো আরও নতুন — তাই প্রশিক্ষণের ডেটায় পুরোনো, ভুল বা range-v3-এর মিশ্রিত উদাহরণ প্রচুর। তার ওপর view-এর সবচেয়ে বড় ভুলগুলো (lifetime) compile হয় এবং ছোট পরীক্ষায় ঠিক ফলও দেয়। সাধারণ ভুলগুলো:
| AI যা করে | সমস্যা | যা হওয়া উচিত |
|---|---|---|
| ফাংশন থেকে local container-এর ওপর বসা view ফেরত দেয় | container মরে যায়, view মৃত মেমরি পড়ে — UB | ডেটা ফেরত দিন (ranges::to) |
| view বা struct-এর member হিসেবে view রেখে দেয়, উৎস আগে মরে | dangling view; ছোট পরীক্ষায় প্রায়ই ধরা পড়ে না | view ছোট পরিসরে, তৈরি করে সঙ্গে সঙ্গে ব্যবহার |
| view দিয়ে ঘোরার সময় একই vector-এ push_back বা erase | iterator invalidation — UB | আগে কপি, অথবা erase_if |
| ৬-৮ ধাপের পাইপ, বড় বড় নামহীন lambda | পড়া, ডিবাগ আর review কঠিন | মাঝের অংশে নাম, অথবা সাধারণ লুপ |
ranges::to চেনে না; বা C++20 প্রজেক্টে ব্যবহার করে | লম্বা ঘোরানো কোড, অথবা compile error | standard-এর সংস্করণ জেনে বেছে নিন |
ranges::v3, views::remove_if-এর মতো range-v3-এর নাম | standard-এ নেই; compile হয় না | শুধু std::views-এর নাম |
filter view const& দিয়ে পাঠায় | compile error | view by value নিন |
AI-কে ranges-এর কোড লিখতে বলার সময় compiler-এর সংস্করণ আর lifetime-এর নিয়ম আগেই বলে দিন, আর প্রতিটি ধাপ ব্যাখ্যা করতে বলুন:
AI-এর ranges কোড যাচাইয়ের চেকলিস্ট
ভাবনার প্রশ্ন
প্রতিটি প্রশ্ন নিয়ে নিজে কিছুক্ষণ ভাবুন — তারপর “→ উত্তর” চাপুন। এগুলোই আপনাকে কোড-লেখক থেকে সিস্টেম-চিন্তক বানাবে।
প্র ০১একটি লাইব্রেরি (range-v3) থেকে C++20-এর standard ranges — এই যাত্রা এত লম্বা হলো কেন, আর পথে কী কী বদলাল?
STL-এর মূল নকশায় Alexander Stepanov iterator-এর জোড়াকে ভিত্তি করেছিলেন, কারণ এতে একটি algorithm যেকোনো container-এর যেকোনো অংশে কাজ করতে পারত। এই সিদ্ধান্ত অসাধারণ সফল হয়, কিন্তু বছরের পর বছর ব্যবহারে দুর্বলতাও স্পষ্ট হয়: জোড়া ভুল দেওয়া যায়, algorithm জোড়া লাগানো যায় না, আর প্রতিটি ধাপে মধ্যবর্তী container লাগে। ২০০০-এর দশকে Boost.Range দেখায় যে “range”-কে একটি জিনিস হিসেবে ধরা যায়। অন্য ভাষাও এগোচ্ছিল — D ভাষা iterator-এর বদলে range-কেই মূল ধারণা করে, আর Andrei Alexandrescu-র “Iterators Must Go” বক্তৃতা (২০০৯) এই আলোচনাকে জোরালো করে।
Eric Niebler ২০১৩-১৪ সালের দিকে range-v3 লেখা শুরু করেন, আর ব্লগে ধারাবাহিকভাবে নকশার প্রশ্নগুলো নিয়ে লেখেন। তাঁর একটি বড় সিদ্ধান্ত ছিল sentinel: শেষের চিহ্ন iterator-এর টাইপের না হলেও চলবে। এতে অসীম range, '\0'-এ শেষ হওয়া লেখা, আর “শর্ত পূরণ পর্যন্ত” ধরনের range স্বাভাবিকভাবে লেখা যায়। আরেকটি সিদ্ধান্ত: view অলস, আর pipe দিয়ে জোড়া। কিন্তু standard-এ ঢোকাতে দরকার ছিল টাইপের শর্ত প্রকাশের একটি উপায় — “এই টাইপ কি range?”, “এই iterator কি random-access?” range-v3 এটি করত template-এর জটিল কৌশলে, যা standard-এর জন্য যথেষ্ট পরিষ্কার নয়। তাই ranges-কে অপেক্ষা করতে হয় concepts-এর জন্য, যা নিজেই দীর্ঘ বিতর্কের পরে C++20-এ আসে।
মাঝের ধাপ ছিল Ranges TS (২০১৭) — একটি Technical Specification, যেখানে মূল লাইব্রেরি না ভেঙে নতুন নকশা পরীক্ষা করা যায়। তারপর “One Ranges Proposal” (P0896) TS-এর বড় অংশকে C++20-এ নিয়ে আসে। কমিটি সময় বাঁচাতে কিছু অংশ বাদ দেয় — সবচেয়ে চোখে পড়ার মতো ছিল ranges::to, যা range-v3-এ থাকলেও C++20-এ আসেনি, এসেছে C++23-এ। zip, enumerate, chunk, slide-ও এসেছে C++23-এ। এমনকি C++20 প্রকাশের পরেও কয়েকটি বড় সংশোধন (যেমন owning_view, আর views::split-এর নতুন নকশা) আগের সংস্করণের ওপর প্রয়োগ করা হয়।
এই ধীরগতি দুর্বলতা নয়, C++-এর চরিত্র। standard-এ একবার কিছু ঢুকলে তা কয়েক দশক টিকে থাকে, আর কোটি কোটি লাইনের কোড তার ওপর নির্ভর করে। তাই কমিটি আগে লাইব্রেরি হিসেবে বাস্তবে পরীক্ষা, তারপর TS, তারপর standard — এই ধাপগুলো মানে।
মূল উপলব্ধি: ranges এক দিনের আবিষ্কার নয়; এটি STL-এর দুই দশকের অভিজ্ঞতা, একটি পরীক্ষিত লাইব্রেরি আর concepts-এর মতো ভাষার নতুন যন্ত্র — তিনটির মিলিত ফল। নতুন C++ ফিচারের ক্ষেত্রেও একই প্রশ্ন করুন: এটি কোন সমস্যা থেকে এসেছে, আর কোন সংস্করণে কতটুকু এসেছে?
প্র ০২view-কে অলস (lazy) হতেই হবে কেন? সব কাজ আগেভাগে করে একটি vector দিলে কী ক্ষতি হতো?
ধরুন views::filter সঙ্গে সঙ্গে কাজ করে একটি নতুন vector ফেরত দিত। প্রথম ক্ষতি মেমরি আর সময়: পাইপলাইনের প্রতিটি ধাপ একটি নতুন container তৈরি করত — ঠিক পুরোনো copy_if-transform পথের মতো। শেষে ৫টি উপাদান লাগলেও আগে লাখ উপাদানের কয়েকটি কপি তৈরি হতো। অলস পাইপলাইনে মধ্যবর্তী কোনো জায়গা লাগে না; প্রতিটি উপাদান একবার সব স্তর পেরিয়ে যায়, আর CPU-র cache-এ সেটি গরম থাকতেই কাজ শেষ।
দ্বিতীয় ক্ষতি আরও মৌলিক: অসীম বা অজানা দৈর্ঘ্যের উৎস। views::iota(1) অসীম; একে আগেভাগে vector বানানো অসম্ভব। একইভাবে একটি নেটওয়ার্ক সংযোগ বা ফাইল থেকে লাইন পড়ার ধারা (std::views::istream) — কত লাইন আসবে, আগে জানা নেই। অলস পাইপলাইনে “প্রথম যে লাইনে error আছে, সেটি পেলেই থামো” লেখা স্বাভাবিক; eager হলে পুরো ফাইল আগে মেমরিতে তুলতে হতো।
তৃতীয় লাভ হলো composition-এর খরচ শূন্যের কাছাকাছি। দুটি view জোড়া দিলে একটি নতুন, ছোট object তৈরি হয় — কোনো উপাদান নড়ে না। তাই পাইপলাইনকে টুকরো টুকরো নাম দিয়ে, ফাংশনে পাঠিয়ে, পরে আরও ধাপ যোগ করে গড়ে তোলা যায়, প্রতিবার কোনো দাম ছাড়াই। এই ধারণা নতুন নয় — Haskell-এর মতো ভাষায় সব হিসাবই ডিফল্টভাবে অলস, আর Python-এর generator, Java-র Stream, C#-এর LINQ একই নীতিতে চলে।
কিন্তু অলসতারও দাম আছে, আর এই পর্বে আপনি তা দেখেছেন। হিসাব হয় পরে, তাই উৎস তখনো বেঁচে আছে কি না — সেটি লেখার মুহূর্তে নয়, ব্যবহারের মুহূর্তে গুরুত্বপূর্ণ; এ থেকেই dangling view-এর বিপদ। একই view দুইবার ঘোরালে কাজও দুইবার হয় — transform-এর lambda ভারী হলে সেটা লক্ষণীয়। আর lambda-য় পার্শ্বপ্রতিক্রিয়া থাকলে কখন, কতবার তা ঘটবে, অনুমান করা কঠিন — যেমন take-এর এক ধাপ বাড়তি টানা। তাই view-এর lambda-গুলো pure function রাখাই নিরাপদ।
মূল উপলব্ধি: অলসতা view-কে অসীম ধারা, শূন্য-কপি আর সস্তা composition দেয় — আর বিনিময়ে দাবি করে যে আপনি উৎসের আয়ু আর পার্শ্বপ্রতিক্রিয়া নিয়ে সচেতন থাকবেন। ফল ধরে রাখতে বা বারবার ব্যবহার করতে হলে, সচেতনভাবে materialize করুন।
প্র ০৩filter view তার begin() cache করে, তাই const দিয়ে ঘোরানো যায় না। standard এমন অদ্ভুত নকশা বেছে নিল কেন?
শুরু করুন range-এর একটি প্রতিশ্রুতি দিয়ে। C++20-এর range concept চায়, begin() আর end() ডাকা হবে “amortized constant time”-এ — অর্থাৎ বারবার ডাকলে গড়ে প্রতিবার সামান্য খরচ। কারণ algorithm আর অন্য view-রা ধরে নেয়, begin() ডাকা সস্তা; অনেক কোড একে বারবার ডাকে। এখন filter view-এর কথা ভাবুন। তার প্রথম উপাদান হলো উৎসের প্রথম যে উপাদান শর্ত পূরণ করে। সেটি খুঁজতে হয়তো লাখ উপাদান পেরোতে হবে। প্রতিবার begin() ডাকলে এই খোঁজ আবার হলে প্রতিশ্রুতি ভাঙে।
সমাধান: প্রথমবার খুঁজে ফলটি view-এর ভেতরে মনে রাখা। দ্বিতীয়বার থেকে begin() সঙ্গে সঙ্গে উত্তর দেয় — গড়ে স্থির খরচ, প্রতিশ্রুতি রক্ষা। কিন্তু মনে রাখা মানে object-এর অবস্থা বদলানো, আর C++-এ const member function অবস্থা বদলাতে পারে না। mutable দিয়ে লুকানো যেত, কিন্তু তাতে নতুন সমস্যা: একাধিক thread একই const view-এ একসাথে begin() ডাকলে data race হতো, কারণ standard library-র নিয়ম হলো const operation একসাথে কয়েকটি thread থেকে নিরাপদ হতে হবে। তাই কমিটি সৎ পথ নেয়: filter view-এর begin() non-const. একই কারণে drop view (কিছু ক্ষেত্রে), reverse view (কিছু ক্ষেত্রে), split আর chunk_by-ও cache করে।
বিকল্প নকশা ছিল — cache না করা (প্রতিশ্রুতি ভাঙা), বা thread-safe cache (atomic আর lock-এর বাড়তি খরচ, যা zero-overhead নীতির বিরুদ্ধে)। প্রতিটির দাম আছে। বেছে নেওয়া নকশার দাম হলো ব্যবহারে অস্বস্তি: const auto& দিয়ে সব range নেওয়ার পুরোনো অভ্যাস view-এ খাটে না। এ নিয়ে C++ সমাজে অনেক বিতর্ক হয়েছে, আর Nicolai Josuttis-এর মতো লেখকেরা এর কড়া সমালোচনা করেছেন।
বাস্তব শিক্ষা: generic ফাংশনে range নিতে auto&& (forwarding reference) ব্যবহার করুন, বা view by value নিন। আর মনে রাখুন, cache মানে view-এর একটি “স্মৃতি” আছে — উৎস বদলালে সেই স্মৃতি পুরোনো হয়ে যেতে পারে। তাই view বানিয়ে সঙ্গে সঙ্গে ব্যবহার করুন, দীর্ঘদিন ধরে রাখবেন না।
মূল উপলব্ধি: একটি নকশা-সিদ্ধান্ত প্রায়ই তিনটি লক্ষ্যের টানাপোড়েন — এখানে গতি (amortized O(1)), thread-নিরাপত্তা, আর ব্যবহারের সুবিধা। তিনটি একসাথে পাওয়া যায়নি; standard প্রথম দুটি বেছে নিয়েছে। “অদ্ভুত” নিয়মের পেছনের এমন টানাপোড়েন বুঝলে নিয়মটি মুখস্থ করতে হয় না।
প্র ০৪pipe | এসেছে Unix থেকে, আর filter-map এসেছে functional programming থেকে। এই দুই পুরোনো ধারা ranges-এ কীভাবে মিলল?
১৯৬০-এর দশকে Bell Labs-এর Doug McIlroy একটি নোটে লেখেন, প্রোগ্রামগুলোকে বাগানের পানির পাইপের মতো জোড়া লাগানোর উপায় থাকা উচিত — এক প্রোগ্রামের আউটপুট সরাসরি আরেকটির ইনপুট। কয়েক বছর পরে, ১৯৭৩ সালে, Ken Thompson Unix-এ pipe যোগ করেন, আর | চিহ্নটি এর প্রতীক হয়ে যায়। Unix-এর দর্শন দাঁড়ায়: প্রতিটি প্রোগ্রাম একটি কাজ ভালোভাবে করুক, আর প্রোগ্রামগুলো একসাথে জোড়া লাগুক। একটি বিখ্যাত ঘটনা আছে: ১৯৮৬ সালে Jon Bentley-র “Programming Pearls” কলামে Donald Knuth একটি লেখার সবচেয়ে বেশি ব্যবহৃত শব্দ বের করার জন্য কয়েক পাতার যত্নশীল একটি প্রোগ্রাম লেখেন, আর McIlroy তার পর্যালোচনায় একই কাজ দেখান ছয়টি ছোট Unix কমান্ডের একটি pipe দিয়ে — tr, sort, uniq, sed জুড়ে।
অন্য ধারাটি আরও পুরোনো। ১৯৫৮ সালে John McCarthy-র Lisp-এ এমন ফাংশন ছিল, যা অন্য একটি ফাংশন নিয়ে একটি তালিকার প্রতিটি উপাদানে প্রয়োগ করে — আজকের map. যে ফাংশন অন্য ফাংশন নেয় বা ফেরত দেয়, তাকে বলে higher-order function. functional programming-এর ঐতিহ্যে map, filter আর reduce (fold) হয়ে ওঠে তালিকা নিয়ে কাজের মূল তিন যন্ত্র; পরে Haskell-এর মতো ভাষা এর সাথে অলস হিসাব যোগ করে। C++-এর views::transform হলো map, views::filter হলো filter, আর C++23-এর ranges::fold_left হলো fold.
ranges এই দুই ধারা মেলায়: functional programming থেকে নেয় ছোট, বিশুদ্ধ, জোড়া-লাগানো যায় এমন রূপান্তর, আর Unix থেকে নেয় বাঁ-থেকে-ডানে পড়া pipe-এর লেখার ধরন ও “একটি একটি করে বয়ে যাওয়া”। Unix pipe-ও আসলে অলস: head -5 পাঁচ লাইন পেয়ে বন্ধ হলে আগের প্রোগ্রামও থেমে যায়, পুরো ফাইল প্রক্রিয়া করে না। তবে একটি বড় পার্থক্য আছে: Unix pipe-এ প্রতিটি ধাপ আলাদা process, ডেটা যায় বাইট হিসেবে; C++ view-এ সব ধাপ compile-এর সময় একটি লুপে মিশে যেতে পারে, টাইপ যাচাইসহ, প্রায় বাড়তি খরচ ছাড়া।
এই ধারণাগুলো আজ প্রায় সব জনপ্রিয় ভাষায় আছে — C#-এর LINQ, Java-র Stream, Rust-এর iterator adaptor, JavaScript-এর array method, Python-এর generator expression. C++ এসেছে দেরিতে, কিন্তু নিজের শর্তে: অলস, মালিকানাহীন, আর zero-overhead-এর লক্ষ্য নিয়ে।
মূল উপলব্ধি: ranges নতুন আবিষ্কারের চেয়ে বেশি একটি পুরোনো, প্রমাণিত চিন্তার C++-রূপ: “ছোট ছোট কাজ জোড়া লাগিয়ে বড় কাজ”। এক ভাষায় এই চিন্তা শিখলে অন্য ভাষাতেও তা কাজে লাগে।
প্র ০৫C++23 ranges-এ কী কী যোগ করল, আর C++20-এর ranges দিয়ে কাজ করা মানুষদের কোন অভাবগুলো এতে মিটল?
C++20 ranges-এর ভিত্তি দিয়েছিল — concepts, ranges algorithm, projection, আর filter-transform-take-এর মতো মূল view. কিন্তু প্রথম দিন থেকেই ব্যবহারকারীরা কয়েকটি বড় অভাব টের পান। সবচেয়ে বড়টি ছিল view থেকে container বানানো: ফল vector-এ রাখতে লুপ লিখতে হতো, বা begin/end-এর জোড়া দিতে হতো — যা অনেক view-এ চলে না। C++23-এর std::ranges::to (প্রস্তাবক Corentin Jabot ও অন্যরা) এটা মেটায়, আর তার সাথে container-গুলোতে range নিয়ে তৈরি হওয়ার নতুন পথও আসে (std::from_range, insert_range, append_range).
দ্বিতীয় অভাব ছিল কয়েকটি একসাথে চলা। দুটি তালিকা পাশাপাশি — নাম আর নম্বর — ঘোরাতে C++20-এ সূচক দিয়ে লুপ লিখতে হতো। C++23 আনে views::zip আর zip_transform; ক্রমিক নম্বরের জন্য views::enumerate; পাশাপাশি জোড়ার জন্য adjacent; সব জোড়া মেলাতে cartesian_product. তৃতীয়, দলে ভাগ করা: chunk (n টি করে), slide (সরে-চলা জানালা — চলমান গড়ের জন্য উপযোগী), chunk_by (শর্ত অনুযায়ী দল), stride (প্রতি n টির একটি)। আরও আসে join_with (মাঝে বিভাজক বসিয়ে জোড়া), as_const, as_rvalue, আর repeat.
চতুর্থ, algorithm-এর দিকে: ranges::fold_left ও তার আত্মীয়রা — <numeric>-এর accumulate-এর ranges-রূপের অভাব মেটাল — আর ranges::contains, starts_with, ends_with, find_last. পঞ্চম, ছাপা: C++23-এর formatting লাইব্রেরি range ছাপতে পারে, তাই std::println("{}", v) সরাসরি [1, 2, 3] দেখায়; map ছাপলে {"a": 1}-এর মতো। এর সাথে C++20-এর কিছু নকশা-ত্রুটিও সারানো হয় — যেমন temporary-কে নিরাপদে ধরার owning_view, আর আরও সহজে ব্যবহারযোগ্য split.
বাস্তবে এর অর্থ: আপনি কোন standard-এ লিখছেন, সেটা জানা জরুরি। একটি C++20 প্রজেক্টে zip বা ranges::to লিখলে compile হবে না; আর compiler-ভেদে C++23-এর অংশগুলো ধাপে ধাপে এসেছে। AI-এর উত্তর মেলানোর সময়, আর চাকরির কোডবেসে ঢোকার সময়, প্রথম প্রশ্ন হোক: “এটা কোন -std দিয়ে compile হয়?”
মূল উপলব্ধি: C++20 ranges দিয়েছিল কাঠামো, C++23 দিয়েছে দৈনন্দিন ব্যবহারের আরাম। নতুন ফিচার একবারে সম্পূর্ণ আসে না — প্রথম সংস্করণে ভিত্তি, পরের সংস্করণে বাস্তব অভিজ্ঞতা থেকে পাওয়া অভাব পূরণ। একটি ফিচার শেখার সময় তার “সংস্করণ-ইতিহাস”-ও জেনে রাখুন।
অনুশীলন
বেশিরভাগ কাজে কোড লিখতে হবে না — আগে অনুমান করুন, তারপর উত্তর মিলিয়ে নিন।
-
আউটপুট অনুমান:
iota(1, 11)থেকে শুরু করে পরপরfilter(বিজোড়),transform(x * 10),drop(1)আরtake(2)— এই পাইপলাইন ছাপলে কী আসবে? প্রতিটি ধাপের পরে ধারাটি কেমন দাঁড়ায়, লিখে দেখুন।[30, 50]. ধাপে ধাপে: iota → 1 … 10; filter (বিজোড়) → 1 3 5 7 9; transform → 10 30 50 70 90; drop(1) → 30 50 70 90; take(2) → 30 50. অলস হলেও ফল একই, যেন প্রতিটি ধাপ আলাদাভাবে চলেছে — শুধু ৭ আর ৯-এর পরের কাজগুলো কখনো হয় না। -
বাগ শিকার: একটি AI এই কোড দিয়েছে —
auto get_names(const std::vector<Student>& all)ফাংশনের ভেতরেstd::vector<std::string> names;লুপে ভরে, তারপরreturn names | std::views::take(3);. ডাকার জায়গায়for (auto& n : get_names(list)) .... প্রোগ্রাম ছোট পরীক্ষায় ঠিক চলেছে। সমস্যা কোথায়, আর কীভাবে সারাবেন?namesফাংশনের local ভ্যারিয়েবল (নামযুক্ত lvalue), তাইnames | take(3)শুধু তার ঠিকানা রাখে (ref_view). return-এর পরে names ধ্বংস হয়, আর লুপ মৃত মেমরি পড়ে — undefined behavior. ছোট পরীক্ষায় ঠিক চলা এখানে কোনো প্রমাণ নয়; মুক্ত মেমরি প্রায়ই কিছুক্ষণ আগের মান ধরে রাখে। সমাধান: ফেরত টাইপstd::vector<std::string>করুন —if (names.size() > 3) names.resize(3); return names;— অথবাreturn names | std::views::take(3) | std::ranges::to<std::vector>();.-fsanitize=addressদিয়ে চালালে মূল কোডে heap-use-after-free ধরা পড়ত। -
গুনে দেখুন:
auto p = iota(1) | filter(৩ দিয়ে বিভাজ্য) | take(2);আর শর্তের lambda প্রতিবার ডাকা হলে একটি counter বাড়ায়।for (int x : p)লুপ শেষে counter কত হবে? লুপ কী ছাপবে?লুপ ছাপবে 3 আর 6; counter হবে ৯. begin() খুঁজতে শর্ত ডাকা হয় ১, ২, ৩-এ (৩টি); প্রথম
++it-এ ৪, ৫, ৬ (আরও ৩টি); আর দ্বিতীয়++it-এ take নিয়মমতো ভেতরের iterator এগিয়ে দেয়, তাই ৭, ৮, ৯ (আরও ৩টি), তারপর গোনা শূন্য হয়ে লুপ থামে। অনেকে ৬ ভাবেন — শেষের তিনটি ডাক take-এর সেই এক ধাপ বাড়তি টানা, যা চতুর্থ অংশের stepper-এ দেখেছেন। শিক্ষা: শর্তের lambda-য় পার্শ্বপ্রতিক্রিয়া রাখবেন না। -
নিজে লিখুন (কোড): উপরের
top_orders.cpp-এtopতৈরির পরে C++23-এরstd::views::enumerateদিয়ে একটি মেধাতালিকা ছাপুন — প্রতিটি লাইনে ক্রম আর দাম, যেমন1. 1200. enumerate-এর সূচক 0 থেকে শুরু হয়, সেটা খেয়াল রাখুন। প্রথম তিন লাইন কী হবে, আগে অনুমান করুন।যোগ করুন:
for (auto [i, taka] : std::views::enumerate(top)) std::println("{}. {}", i + 1, taka);. enumerate প্রতিটি উপাদানের সাথে একটি সূচক জোড়া দেয়, আর structured binding (পর্ব ১২) দিয়ে দুটি আলাদা নাম পাওয়া যায়। প্রথম তিন লাইন:1. 1200,2. 875,3. 640; তারপর4. 450আর5. 300.i + 1না লিখলে তালিকা 0 থেকে শুরু হতো। আরও চেষ্টা করুন:top-কেconst auto&হিসেবে একটি ফাংশনে পাঠালে কী error আসে, আর কেন।
নিজেকে যাচাই করুন
ভুল হলে কারণসহ ব্যাখ্যা পাবেন, আবার চেষ্টাও করতে পারবেন।
ক্রমC++23-এ অর্ডারের একটি vector থেকে “পরিশোধিত অর্ডারগুলোর মধ্যে সবচেয়ে দামি ৫টির দাম, টাকায়, একটি নতুন vector-এ” পেতে ধাপগুলো সঠিক ক্রমে সাজান (▲▼ চেপে সরান)
ranges::sortআর projection দিয়ে দাম অনুযায়ী বড় থেকে ছোট সাজানোviews::filterদিয়ে শুধু পরিশোধিত অর্ডার রাখাviews::transformদিয়ে পয়সাকে টাকায় রূপান্তরviews::take(5)দিয়ে প্রথম ৫টি রাখাranges::toদিয়ে ফল একটি vector-এ জমা করা
C++20-এ auto r = v | std::views::filter(f); লাইনটি চলার মুহূর্তে শর্তের ফাংশন f কতবার ডাকা হয়? (v-তে ১০টি উপাদান)
- একবারও না
- একবার, প্রথম উপাদানের জন্য
- ১০ বার, প্রতিটি উপাদানের জন্য
- f-এর ফলের ওপর নির্ভর করে
C++20-এ std::ranges::sort(s, {}, &Student::marks); লাইনে তৃতীয় argument &Student::marks-এর ভূমিকা কী?
- তুলনার নিয়ম বলে দেয় (less বা greater)
- শুধু marks-গুলোকে আলাদা করে সাজায়, নামগুলো জায়গায় থাকে
- projection — তুলনার আগে প্রতিটি Student থেকে marks বের করে
- এটি compile error, কারণ sort দুটি argument নেয়
C++23-এ একটি view-এর সব উপাদান দিয়ে একটি নতুন std::vector বানানোর সবচেয়ে সরাসরি উপায় কোনটি?
- view-কে static_cast দিয়ে vector-এ রূপান্তর
- view | std::ranges::to<std::vector>()
- std::views::all(view)
- std::vector<int> v(view.begin(), view.end()) — সব view-এ চলে
C++20-এ std::vector<int> make(); একটি নতুন vector ফেরত দেয়। auto it = std::ranges::max_element(make()); int m = *it; লিখলে কী হয়?
- compile হয়, কিন্তু চলার সময় undefined behavior
- ঠিকঠাক সর্বোচ্চ মান পাওয়া যায়
- max_element ডাকার লাইনেই compile error — temporary নেওয়া যায় না
- it-এর টাইপ std::ranges::dangling, তাই *it লাইনে compile error
C++20-এ std::views::iota(1) | std::views::filter(জোড়) | std::views::transform(বর্গ) | std::views::take(3) ঘুরিয়ে ছাপলে কী আসে?
- 1 9 25
- 4 16 36
- 1 4 9
- প্রোগ্রাম কখনো থামে না, কারণ iota অসীম
C++20-এ void show(const auto& r) ফাংশনে একটি views::filter view পাঠিয়ে ভেতরে range-for লুপ চালালে compile error আসে কেন?
- view কোনো ফাংশনে পাঠানোই যায় না
- filter view-এর উপাদান const হতে পারে না
- filter view-এর begin() প্রথম যোগ্য উপাদান cache করে, তাই এটি const নয়
- range-for লুপ view-এর ওপর চলে না
শব্দকোষ
লেখার মধ্যে ডটেড দাগ দেওয়া শব্দে মাউস রাখুন বা চাপুন — অর্থ দেখাবে। এই পর্বের সব নতুন শব্দ এখানে একসাথে:
মনে রাখুন
- range = begin + end;
std::ranges-এর algorithm পুরো range নেয় — iterator-জোড়ার ভুল নেই, temporary পেলেdangling-সুরক্ষা। - projection দিয়ে member ধরে সাজান ও খুঁজুন:
ranges::sort(s, {}, &Student::marks). - view অলস, মালিকানাহীন, কপি সস্তা;
|দিয়ে জোড়া লাগে, আর উপাদান চাওয়া হলে তবেই একটি একটি করে হিসাব হয়। - view-এর আয়ু উৎসের চেয়ে বেশি নয়; ফাংশন থেকে ডেটা ফেরত দিন, view নয় — C++23-এ
ranges::to. ঘোরার সময় উৎস বদলাবেন না। - sort-এর মতো পুরো-ডেটার কাজ algorithm, ছাঁকা-বদলানো-কাটা view; ২-৪ ধাপের পাইপ, নামযুক্ত lambda, আর জটিল হলে সাধারণ লুপ।
আরও পড়ুন · ABCL TECH-এ আপনার পরবর্তী পদক্ষেপ
- পর্ব ৩৬ · move semantics — কপি না করে সরানোপরবর্তী পর্বমডিউল ৬-এর শুরু: বড় ডেটা কপি না করে মালিকানা হস্তান্তর।
- C++ প্রোগ্রামিং · কোর্স হোম৫৫টি পর্বপুরো কোর্সের রোডম্যাপ — কোন পর্ব কখন।
- পর্ব ৩৩ · STL algorithm — তৈরি সমাধানআবার দেখুনsort, find, count_if, accumulate — ranges-এর algorithm যাদের নতুন রূপ।