সরাসরি পর্বে যান
পর্ব ৪৪ · ৫৫-এর মধ্যে · মডিউল ৭

ফাইল I/O ও filesystem — ডেটাকে প্রোগ্রামের পরেও বাঁচিয়ে রাখা

File I/O and std::filesystem — making data outlive the program

Taught in Bangla, this lesson covers persisting data with files: ofstream, ifstream and open modes, stream states and the while(!eof()) bug, reading CSV and config files line by line, text versus binary files, std::filesystem paths and error codes, safe saving with a temp file and rename, and Unicode file names.

এই পর্বে যা শিখবেন

  • ফাইল কেন লাগে, আর “ফাইল = বাইটের সারি, অর্থ দেয় format” — এই মূল ধারণা
  • ofstream, ifstream, fstream, open mode, buffer ও flush, আর RAII দিয়ে নিজে থেকে বন্ধ হওয়া
  • stream-এর অবস্থা (good, fail, eof, bad), eof()-লুপের বাগ, আর লাইন ধরে CSV ও config ফাইল পড়া
  • text বনাম binary: Windows-এর newline, struct-এর কাঁচা বাইট কেন বিপদ, আর নিজের format ঠিক করা
  • std::filesystem: path, ফোল্ডারের তালিকা, কপি-মোছা, exception বনাম error_code, নিরাপদে সেভ, বাংলা নামের ফাইল আর ব্যবহারকারীর দেওয়া পথ

১ · প্রোগ্রাম থামলে মেমরি শূন্য — ফাইল কেন লাগে

এই কোর্সে এ পর্যন্ত আপনার প্রতিটি প্রোগ্রামের একটি অদ্ভুত স্বভাব ছিল: সব ভুলে যায়। একটি std::vector-এ সারাদিনের বিক্রির হিসাব রাখলেন, প্রোগ্রাম বন্ধ করলেন — পরের বার চালালে vector আবার খালি। কারণ ভ্যারিয়েবল থাকে RAM-এ, আর প্রোগ্রাম শেষ হলে অপারেটিং সিস্টেম সেই মেমরি ফেরত নিয়ে অন্য কাজে দেয়। বিদ্যুৎ চলে গেলে তো কথাই নেই — RAM-এর সব তথ্য মুছে যায়।

পাড়ার মুদি দোকানের করিম ভাইকে ভাবুন। দিনের বেলা তিনি হিসাব রাখেন একটি ছোট চক-বোর্ডে — দ্রুত লেখা যায়, দ্রুত মোছা যায়। কিন্তু রাতে দোকান বন্ধের আগে বোর্ডের হিসাব তুলে রাখেন বাঁধানো খাতায়, কারণ সকালে বোর্ড পরিষ্কার থাকবে। পহেলা বৈশাখে নতুন হালখাতা খোলার সময় পুরোনো খাতাই বলে দেয় কার কাছে কত বাকি। RAM হলো চক-বোর্ড, ফাইল হলো খাতা। খাতায় কোন কলামে কী লিখবেন — তারিখ আগে না নাম আগে — সেটা করিম ভাই নিজে ঠিক করেছেন। ফাইলের বেলাতেও তাই।

শেষ কথাটিই এই পর্বের সবচেয়ে গভীর ধারণা। অপারেটিং সিস্টেমের চোখে একটি ফাইল শুধু একটি নাম আর তার পেছনে একটি বাইটের সারি — ০ থেকে ২৫৫ মানের একের পর এক সংখ্যা। “এটা একটি লেখা”, “এটা একটি ছবি”, “এখানে কমা দিয়ে আলাদা করা হিসাব” — এসব অর্থ ফাইলের ভেতরে লেখা থাকে না। অর্থ দেয় যে প্রোগ্রাম পড়ছে, একটি চুক্তি মেনে। সেই চুক্তির নাম file format. একই বাইটগুলো তিনভাবে পড়া যায়:

লাইনভিত্তিক লেখা

বাইটগুলো অক্ষর; '\n' বাইট (মান 10) এলে নতুন লাইন। লগ ফাইল, নোট, সাধারণ রিপোর্ট। মানুষ Notepad-এ খুলে পড়তে পারে।

log.txt, notes.txt

CSV ও config

লাইনভিত্তিক লেখার ওপর আরেকটি নিয়ম: প্রতি লাইনে কমা দিয়ে আলাদা ঘর (CSV), বা key=value. Excel বা অন্য প্রোগ্রামও পড়তে পারে।

sales.csv, app.ini

Binary রেকর্ড

বাইটগুলো সরাসরি সংখ্যা — যেমন প্রতি ৮ বাইটে একটি দাম। ছোট আর দ্রুত, কিন্তু মানুষ পড়তে পারে না, আর নিয়ম (বাইটের ক্রম, মাপ) লিখে না রাখলে অন্য প্রোগ্রামও না।

ছবি, ডেটাবেস, গেম-সেভ

এই পর্বে আমরা চারটি বাস্তব কাজ ধরে এগোব: একটি দোকানের দিনের বিক্রির খাতা CSV-তে সেভ ও লোড করা, একটি লগ ফাইলে প্রতিটি ঘটনা যোগ করা, একটি ফোল্ডারের সব .txt ফাইল আকারসহ তালিকা করা, আর একটি config ফাইল পড়া। প্রতিটির পেছনে একই প্রশ্ন থাকবে: কী ভুল হতে পারে, আর ভুল হলে ডেটা বাঁচবে তো?

মূল ধারণা

ফাইল হলো প্রোগ্রামের বাইরের জগৎ। মেমরির ভেতরের ভ্যারিয়েবল আপনার নিয়ন্ত্রণে, কিন্তু ফাইল না-ও থাকতে পারে, অনুমতি না-ও থাকতে পারে, ডিস্ক ভরে যেতে পারে, অন্য কেউ মাঝপথে বদলে দিতে পারে, বিদ্যুৎ চলে যেতে পারে। তাই ফাইলের কোডে “সুখের পথ” লেখা অর্ধেক কাজ; বাকি অর্ধেক হলো প্রতিটি ধাপে ভুল ধরা।

২ · ফাইলও একটি stream — ofstream, ifstream আর open mode

পর্ব ৭-এ দেখেছেন, std::cin আর std::cout হলো stream — বাইটের একটি প্রবাহ, যার একদিকে আপনার প্রোগ্রাম আর অন্যদিকে কিবোর্ড বা স্ক্রিন। C++-এর নকশার সুন্দর দিক হলো, অন্যদিকে স্ক্রিনের বদলে একটি ফাইল বসালেও আপনার কোড প্রায় একই থাকে। header <fstream>-এ তিনটি টাইপ:

টাইপকাজকার আত্মীয়ডিফল্ট mode
std::ofstreamফাইলে লেখাstd::cout-এর মতো (<<, std::print)out — পুরোনো লেখা মুছে শুরু
std::ifstreamফাইল থেকে পড়াstd::cin-এর মতো (>>, getline)in — ফাইল থাকতেই হবে
std::fstreamএকই ফাইলে পড়া ও লেখাদুটিরইin ও out — ফাইল না থাকলে খোলা ব্যর্থ

ফাইল কীভাবে খোলা হবে, তা ঠিক করে open mode — std::ios-এর কয়েকটি flag, যেগুলো | দিয়ে জোড়া যায়:

  • std::ios::in — পড়ার জন্য। std::ios::out — লেখার জন্য; একা দিলে পুরোনো লেখা মুছে যায়।
  • std::ios::trunc — খোলার সময়েই ফাইল খালি করা (out-এর সাথে নিজে থেকেই হয়)।
  • std::ios::app — append: প্রতিটি লেখা যায় ফাইলের শেষে, পুরোনো লেখা অক্ষত। লগ ফাইলের জন্য ঠিক এটাই চাই।
  • std::ios::binary — বাইট যেমন আছে তেমন; newline-এর কোনো রূপান্তর নয় (অংশ ৫-এ বিস্তারিত)।
  • std::ios::ate — খুলেই শেষে চলে যাওয়া; আর C++23 থেকে std::ios::noreplace — ফাইল আগে থেকে থাকলে খোলাই ব্যর্থ, যাতে ভুল করে কারও ফাইল মুছে না যায়।

সবচেয়ে সাধারণ দুর্ঘটনা: লগ বা খাতায় নতুন লাইন যোগ করতে গিয়ে শুধু std::ofstream log{"shop.log"}; লেখা। ডিফল্ট mode out, তাই খোলার মুহূর্তেই আগের সব লেখা শূন্য — একটি লাইনও লেখার আগে। যোগ করতে চাইলে স্পষ্ট করে std::ios::app লিখুন।

একটি লগ ফাইলে প্রতিটি বিক্রির ঘটনা যোগ করার স্বাভাবিক রূপ এমন:

#include <fstream>
#include <print>
#include <string_view>

bool log_event(std::string_view msg) {
    std::ofstream log{"shop.log", std::ios::app};   // না থাকলে তৈরি, থাকলে শেষে যোগ
    if (!log) return false;                          // খোলা গেল কি না
    std::println(log, "{}", msg);                     // C++23: সরাসরি ফাইলে print
    return static_cast<bool>(log);                   // লেখাও সফল হলো কি না
}   // এখানে log ধ্বংস — destructor নিজেই flush করে ফাইল বন্ধ করে

তিনটি জিনিস খেয়াল করুন। প্রথমত, C++23 থেকে std::print আর std::println সরাসরি একটি std::ostream-এ (header <ostream>) বা C-এর FILE*-এ (header <print>) লিখতে পারে — ফরম্যাটের নিয়ম পর্ব ৭-এর মতোই। পুরোনো log << msg << '\n';-ও চলে। দ্বিতীয়ত, কোথাও close() নেই। std::ofstream একটি RAII টাইপ (পর্ব ১৭): constructor ফাইল খোলে, destructor buffer-এর বাকি লেখা পাঠিয়ে ফাইল বন্ধ করে — ফাংশন স্বাভাবিকভাবে শেষ হোক, আগে return হোক, বা exception ছুটুক। তৃতীয়ত, std::endl নেই। প্রতিটি endl একটি flush ঘটায়, আর হাজার লাইনের ফাইলে হাজারবার flush মানে অকারণে ধীর প্রোগ্রাম।

লেখা আসলে কখন ডিস্কে পৌঁছায়

std::println(log, ...) শেষ হওয়ার মানে কিন্তু এই নয় যে লেখাটি ডিস্কে বসে গেছে। মাঝে দুটি অপেক্ষার ঘর আছে — stream-এর নিজের buffer, আর অপারেটিং সিস্টেমের page cache. প্রতিটি ছোট লেখার জন্য ডিস্কে যাওয়া খুব ধীর, তাই সবাই জমিয়ে একসাথে পাঠায়। একটি লাইনের পুরো যাত্রা ধাপে ধাপে দেখুন:

  1. প্রোগ্রামtotal = 435RAM-এর ভ্যারিয়েবল
  2. stream bufferofstreamপ্রোগ্রামের ভেতরে জমা
  3. OS cachepage cacheঅপারেটিং সিস্টেমের মেমরি
  4. ডিস্কsales.csvবিদ্যুৎ গেলেও থাকে
  5. আবার পড়াifstreamপরের দিন, নতুন প্রোগ্রাম
শুরুশুধু RAM-এ
int total = 435;
// প্রোগ্রাম এখন বন্ধ হলে: 435 হারিয়ে গেল
std::println(out, ...)buffer-এ জমা
std::println(out, "total,{}", total);
// "total,435\n" আছে ofstream-এর buffer-এ
// ডিস্কের ফাইল এখনো খালি হতে পারে
// এখন crash হলে: লেখাটি হারাবে
flush বা destructorOS-এর হাতে
out.flush();      // বা } — RAII destructor
// বাইটগুলো এখন OS-এর মেমরিতে
// প্রোগ্রাম crash হলেও টিকে থাকবে
// কিন্তু বিদ্যুৎ গেলে: না-ও থাকতে পারে
OS নিজের সময়ে লেখেডিস্কে স্থায়ী
// কয়েক সেকেন্ডের মধ্যে OS ডিস্কে পাঠায়
// এখনই নিশ্চিত চাইলে OS-এর fsync লাগে
// standard C++-এ এর কোনো ফাংশন নেই
std::ifstream in{"sales.csv"}বাইট থেকে আবার অর্থ
std::getline(in, line);   // "total,435"
// বাইটগুলো ফিরল; "435" লেখাটিকে
// আবার সংখ্যা বানাতে হবে — format-এর চুক্তি মেনে
  1. মান আছে শুধু RAM-এ। চক-বোর্ডের হিসাব। প্রোগ্রাম শেষ, crash, বা বিদ্যুৎ চলে যাওয়া — যেকোনোটিতে মানটি নেই।
  2. stream buffer-এ জমা। ofstream প্রতিটি ছোট লেখা সাথে সাথে পাঠায় না; একটি buffer ভরলে বা flush হলে একসাথে পাঠায়। এই মুহূর্তে প্রোগ্রাম হঠাৎ মারা গেলে (যেমন segfault) buffer-এর লেখা কখনো ফাইলে পৌঁছায় না।
  3. flush — OS-এর হাতে তুলে দেওয়া। flush(), std::endl, বা stream ধ্বংস হলে buffer খালি হয়ে বাইটগুলো অপারেটিং সিস্টেমের কাছে যায়। এখন আপনার প্রোগ্রাম crash করলেও লেখা থাকবে, কারণ সেটা আর আপনার প্রোগ্রামের মেমরিতে নেই।
  4. ডিস্কে স্থায়ী। OS সাধারণত কয়েক সেকেন্ডের মধ্যে cache থেকে ডিস্কে লেখে। পুরো কম্পিউটার হঠাৎ বন্ধ হলে (লোডশেডিং, UPS নেই) এই শেষ ধাপের আগে থাকা লেখা হারাতে পারে। একদম নিশ্চিত হতে OS-এর নিজস্ব ফাংশন (POSIX-এ fsync) লাগে।
  5. আবার পড়া — অর্থ ফিরিয়ে আনা। পরের দিন নতুন প্রোগ্রাম ifstream দিয়ে বাইটগুলো ফেরত পায়। কিন্তু "435" এখন তিনটি অক্ষর; সংখ্যা বানাতে হলে লেখার সময়ের চুক্তি মেনেই পার্স করতে হবে।
C জানলে

C-এর fopen-এর mode আর C++-এর open mode প্রায় এক-এক মেলে: "w" = out, "a" = app, "r+" = in|out, "b" = binary, আর C11-এর "x" = C++23-এর noreplace. বড় পার্থক্য দুটি: C++-এ fclose ভুলে যাওয়ার সুযোগ নেই, কারণ destructor বন্ধ করে; আর ভুলের খবর আসে stream-এর অবস্থায়, NULL বা EOF ফেরত মানে নয়। buffer, feof-এর বাগ আর binary ফাইলের গভীর আলোচনা আছে C কোর্সের ফাইল I/O পর্বে।

৩ · ভুল ধরা — stream-এর অবস্থা আর eof()-এর বিখ্যাত ফাঁদ

একটি ফাইল খুলতে চাইলেন, কিন্তু নামটি ভুল, বা ফোল্ডারে পড়ার অনুমতি নেই। std::ifstream in{"sales.csv"}; কোনো exception ছোড়ে না, প্রোগ্রামও থামে না — শুধু stream-টি “ব্যর্থ” অবস্থায় চলে যায়, আর এরপর প্রতিটি পড়া নীরবে কিছুই করে না। আপনি যাচাই না করলে প্রোগ্রাম আনন্দে “আজ বিক্রি ০ টাকা” দেখাবে। তাই প্রথম নিয়ম: খোলার পরেই if (!in).

পর্ব ৭-এ cin-এর জন্য যে stream state দেখেছিলেন, ফাইলেও হুবহু তাই। চারটি প্রশ্ন করা যায়:

ফাংশনtrue মানেকখন হয়
good()কোনো সমস্যা নেইসব flag শূন্য
eof()পড়তে গিয়ে ফাইলের শেষে ধাক্কা লেগেছেশেষের পরে পড়ার চেষ্টা হয়েছে — শেষে পৌঁছানো মাত্র নয়
fail()শেষ কাজটি ব্যর্থ (failbit বা badbit)খোলা ব্যর্থ; int-এর জায়গায় abc; শেষে পৌঁছে আর কিছু পাওয়া যায়নি
bad()গুরুতর সমস্যানিচের স্তরের পড়া/লেখা ব্যর্থ, যেমন ডিস্কের ভুল

আর if (in) বা if (!in) লিখলে stream নিজেকে bool-এ রূপান্তর করে: true মানে !fail(). আরও সুন্দর ব্যাপার হলো, in >> x আর std::getline(in, line) দুটিই stream-টি নিজেকেই ফেরত দেয়। তাই while (in >> x) মানে: আগে পড়ার চেষ্টা, আর পড়া সফল হলে তবেই লুপের ভেতরে ঢোকা।

এখন সেই বাগটি, যা প্রায় প্রতিটি প্রোগ্রামার জীবনে অন্তত একবার লেখেন — আর AI এখনো প্রায়ই লেখে। একটি ফাইলে তিনটি বিক্রির অঙ্ক: 120, 75, 240, প্রতিটি নিজের লাইনে, শেষেও একটি newline. যোগফল হওয়া উচিত ৪৩৫. ধাপে ধাপে চালিয়ে দেখুন, প্রতিটি ধাপে আগে নিজে অনুমান করুন:

1std::ifstream f{"amounts.txt"};2while (!f.eof()) {3    f >> x;4    sum += x;5}6// ঠিক রূপ:7while (f >> x) sum += x;
এই ধাপের পরে
x
sum
eof()
fail()
  1. ফাইল খোলা হলো। ভেতরে বাইটগুলো 120\n75\n240\n. কিছু পড়া হয়নি, তাই কোনো flag নেই।
  2. প্রথম পাক। eof() false, লুপে ঢুকল। f >> x 120 পড়ল, যোগফল 120. পড়া থেমেছে প্রথম newline-এর ঠিক আগে।
  3. দ্বিতীয় পাক। শুরুর ফাঁকা জায়গা (newline) বাদ দিয়ে 75 পড়ল। যোগফল 195. সব ঠিকঠাক চলছে বলে মনে হচ্ছে।
  4. তৃতীয় পাক — আসল মুহূর্ত। 240 পড়ল, যোগফল 435 — সঠিক উত্তর! কিন্তু পড়া থেমেছে শেষ newline-এর আগে। stream এখনো ফাইলের শেষের পরে পড়ার চেষ্টা করেনি, তাই eof() এখনো false. লুপ আবার ঘুরবে।
  5. চতুর্থ পাক — ব্যর্থ পড়া। f >> x newline বাদ দিতে গিয়ে ফাইলের শেষে ধাক্কা খেল। এবার eofbit আর failbit দুটিই উঠল। কোনো সংখ্যা পড়া যায়নি, তাই x-এ আগের 240 রয়ে গেছে।
  6. তবু যোগ হলো। লুপের ভেতরে পড়া সফল কি না কেউ দেখেনি, তাই পুরোনো 240 আবার যোগ হলো: 675. এরপর eof() true, লুপ শেষ। প্রশ্ন ছিল “শেষে পৌঁছেছি?”; অথচ জানা দরকার ছিল “এইমাত্র পড়াটা সফল হয়েছে?”
  7. সঠিক রূপ: আগে পড়া, তারপর যাচাই। while (f >> x) প্রতিবার পড়ার ফলটাই শর্ত হিসেবে দেখে। চতুর্থ চেষ্টা ব্যর্থ হতেই লুপ থামে — যোগফল 435. লাইন ধরে পড়ার জন্য একই নিয়ম: while (std::getline(f, line)).

getline দিয়ে while (!f.eof()) লিখলে বাগটি একটু অন্য চেহারায় আসে: শেষ পাকে getline কিছুই পায় না, line খালি হয়ে যায়, আর আপনার কোড একটি খালি লাইন প্রক্রিয়া করে — CSV পার্সার তখন “ভুল লাইন” বলে চিৎকার করে, বা আরও খারাপ, একটি শূন্য-দামের পণ্য খাতায় যোগ করে। আর ফাইলের শেষে newline না থাকলে বাগটি লুকিয়ে যায় — তাই পরীক্ষায় ধরা পড়ে না, ধরা পড়ে গ্রাহকের ফাইলে।

নিয়ম

eof() ভবিষ্যৎ বলে না, অতীত বলে: “শেষ পড়ার চেষ্টায় ফাইলের শেষে ধাক্কা লেগেছিল।” তাই লুপের শর্তে পড়ার কাজটিই রাখুন। লুপ শেষ হওয়ার পরে eof() দেখা অর্থবহ: true হলে ফাইল স্বাভাবিকভাবে শেষ; false হলে মাঝপথে কোনো ভুল লেখা পেয়ে থেমেছে।

শুধু পড়াই নয়, লেখাও ব্যর্থ হতে পারে — ডিস্ক ভরে গেলে, পেনড্রাইভ খুলে নিলে, নেটওয়ার্ক ফোল্ডার হারিয়ে গেলে। লেখার পরে if (!out) দেখুন। কিন্তু একটি সূক্ষ্ম বিষয় আছে: buffer-এর শেষ অংশ ফাইলে যায় বন্ধ হওয়ার সময়, আর destructor সেই সময়ের ভুল আপনাকে জানাতে পারে না (destructor থেকে ভুল জানানোর ভালো উপায় নেই)। গুরুত্বপূর্ণ ফাইলে তাই নিজে out.close() ডেকে তারপর if (!out) দেখুন। চাইলে f.exceptions(std::ios::badbit) দিয়ে নির্দিষ্ট অবস্থায় stream-কে exception ছুড়তে বলা যায়, তবে বেশিরভাগ কোড অবস্থা দেখেই কাজ চালায়।

৪ · লাইন ধরে পড়া — দোকানের খাতা, CSV আর config ফাইল

বেশিরভাগ বাস্তব ফাইল লাইনভিত্তিক: প্রতি লাইনে একটি রেকর্ড। পড়ার নিরাপদ কৌশল দুই ধাপের — প্রথমে পুরো লাইন std::getline দিয়ে তুলে আনুন, তারপর সেই লাইনটিকে আলাদা করে পার্স করুন। সরাসরি ফাইলে >> চালালে একটি ভুল লাইন পুরো stream-কে fail অবস্থায় ফেলে দেয়, আর আপনি জানতেও পারেন না কোন লাইনে সমস্যা। লাইন আলাদা করে পড়লে একটি লাইন ভুল হলেও বাকিগুলো পড়া চলে, আর ভুল লাইনটি নাম ধরে জানানো যায়।

একটি লাইনকে ঘরে ভাগ করার দুটি জনপ্রিয় উপায়। std::istringstream একটি string-কে ছোট্ট একটি stream বানিয়ে দেয়, যাতে >> আর getline(row, item, ',') চালানো যায় — সহজ, পরিচিত। আর পর্ব ৪১-এর std::from_chars দ্রুততর, locale দেখে না, আর ঠিক কোথায় পড়া থামল তা জানায় — লাখ লাইনের ফাইলে বা কঠোর যাচাইয়ে সেটিই ভালো। এবার করিম ভাইয়ের দোকানের দিনের খাতা: লিখি, বন্ধ করি, আবার পড়ি — যার একটি লাইনে কেউ ভুল করে সংখ্যার জায়গায় x লিখে ফেলেছেন:

#include <fstream>
#include <print>
#include <sstream>
#include <string>
int main() {
    { std::ofstream out{"sales.csv"};                   // create or truncate
      std::print(out, "item,qty,price\ntea,12,10\nbiscuit,5,15\nchop,x,20\ncake,2,120\n"); }
    std::ifstream in{"sales.csv"};                      // closed above by RAII
    if (!in) { std::println("cannot open sales.csv"); return 1; }
    std::string line, item;
    std::getline(in, line);                             // skip the header
    int total = 0;
    while (std::getline(in, line)) {
        std::istringstream row{line};
        int qty = 0, price = 0; char comma = 0;
        if (std::getline(row, item, ',') && row >> qty >> comma >> price && comma == ',') {
            std::println("{:<8}{:>3} x {:>3} = {:>4}", item, qty, price, qty * price);
            total += qty * price;
        } else std::println("bad line: {}", line);
    }
    std::println("total = {} taka", total);
}

আউটপুট পাঁচ লাইন: tea 12 x 10 = 120, biscuit 5 x 15 = 75, bad line: chop,x,20, cake 2 x 120 = 240, আর total = 435 taka (আসল আউটপুটে কলামগুলো ফাঁকা জায়গা দিয়ে সারি করা)। প্রথম { } ব্লকটি ইচ্ছা করে দেওয়া: ব্লক শেষ হতেই out ধ্বংস হয়, ফাইল flush হয়ে বন্ধ হয় — তারপরই পড়া শুরু। ব্লক না দিলে লেখা হয়তো তখনো buffer-এ, আর in একটি খালি ফাইল দেখত। প্রথম getline শিরোনামের লাইন (item,qty,price) বাদ দেয়। ভেতরের শর্তটি পুরো লাইনের চুক্তি যাচাই করে: নাম, কমা পর্যন্ত; তারপর একটি সংখ্যা, একটি কমা, আরেকটি সংখ্যা। chop,x,20-এ qty পড়া ব্যর্থ, তাই row fail — শর্ত মিথ্যা, লাইনটি নাম ধরে জানানো হলো, কিন্তু বাইরের in অক্ষত থাকায় পরের লাইন cake ঠিকই পড়া হলো। এই প্রোগ্রাম অনলাইন compiler-এর নিজের কাজের ফোল্ডারে ফাইল তৈরি করে, তাই সেখানে নিশ্চিন্তে চালাতে পারেন।

এই পার্সার “সরল CSV”-র জন্য — যেখানে কোনো ঘরের ভেতরে কমা নেই। আসল CSV-তে "Dhaka, Bangladesh"-এর মতো উদ্ধৃতিচিহ্নের ভেতরে কমা থাকতে পারে, উদ্ধৃতিচিহ্নের ভেতরে উদ্ধৃতিচিহ্ন ("") থাকতে পারে, এমনকি একটি ঘর কয়েক লাইনে ছড়াতে পারে। বাইরের মানুষের পাঠানো CSV পড়তে হলে এই নিয়মগুলো (RFC 4180) মেনে চলা একটি লাইব্রেরি ব্যবহার করুন, বা নিজের ফাইলের format এমনভাবে ঠিক করুন যাতে এমন ঘর আসতেই না পারে।

config ফাইল — key=value

অনেক অ্যাপ নিজের সেটিংস রাখে এমন একটি ফাইলে: lang=bn, vat=15, আর # দিয়ে শুরু লাইন মন্তব্য। পড়ার কাঠামো ওপরের মতোই — লাইন ধরে, তারপর ভাগ:

std::map<std::string, std::string> load_config(const std::filesystem::path& p) {
    std::map<std::string, std::string> cfg;
    std::ifstream in{p};
    if (!in) return cfg;                         // ফাইল নেই — ডিফল্ট সেটিংস চলবে
    std::string line;
    while (std::getline(in, line)) {
        if (line.empty() || line[0] == '#') continue;
        auto eq = line.find('=');
        if (eq == std::string::npos) continue;   // বা: লাইন নম্বরসহ সতর্কবার্তা
        cfg[line.substr(0, eq)] = line.substr(eq + 1);
    }
    return cfg;
}

লক্ষ করুন, ফাইল না থাকা এখানে ভুল নয় — প্রথমবার অ্যাপ চালালে config থাকবেই না। কোনটি “স্বাভাবিক অনুপস্থিতি” আর কোনটি “আসল ভুল”, সেই সিদ্ধান্ত আপনার নকশার অংশ। আরেকটি বাস্তব সমস্যা: Windows-এ Notepad দিয়ে সম্পাদিত ফাইলের লাইনের শেষে '\r' থেকে যেতে পারে (পরের অংশে কারণ), তখন "bn\r" আর "bn" মেলে না। ভালো loader মান থেকে দুই পাশের ফাঁকা জায়গা আর '\r' ছেঁটে নেয়।

পুরো ফাইল একবারে একটি string-এ

ছোট ফাইল (কয়েক MB পর্যন্ত) একবারে পড়ে নেওয়া প্রায়ই সবচেয়ে সরল। দুটি প্রচলিত রূপ:

std::ifstream in{"notice.txt", std::ios::binary};
std::string all{std::istreambuf_iterator<char>{in}, {}};   // শুরু থেকে শেষ পর্যন্ত সব বাইট

// অথবা: আকার জেনে একবারে পড়া (বড় ফাইলে দ্রুত)
std::string buf(std::filesystem::file_size("notice.txt"), '\0');
in.read(buf.data(), static_cast<std::streamsize>(buf.size()));

এখানে binary দেওয়া হয়েছে যাতে বাইটগুলো হুবহু আসে — আর আকার জেনে পড়ার সময় ফাইলের আকার আর পড়া বাইটের সংখ্যা যেন মেলে। তবে খেয়াল রাখুন, আকার জানা আর পড়ার মাঝে অন্য কেউ ফাইলটি বদলে দিতে পারে; তাই in.gcount() দিয়ে আসলে কত বাইট এল, তা দেখে নেওয়া ভালো অভ্যাস। আর ব্যবহারকারী যদি ১০ GB-র একটি ফাইল দেন? “সব একবারে RAM-এ” তখন আর সরল সমাধান নয় — লাইন ধরে পড়াই নিরাপদ।

৫ · text বনাম binary — newline, padding আর বাইটের ক্রম

“text mode” আর “binary mode”-এর পার্থক্য Linux বা macOS-এ প্রায় নেই, কিন্তু Windows-এ আছে। ইতিহাসটি টাইপরাইটারের: “লাইনের শুরুতে ফেরো” (carriage return, '\r') আর “এক লাইন নিচে নামো” (line feed, '\n') ছিল দুটি আলাদা কাজ। Windows (MS-DOS-এর উত্তরাধিকারে) লাইনের শেষ চিহ্ন হিসেবে দুটিই রাখে — \r\n; Unix শুধু \n. C আর C++ প্রোগ্রামের ভেতরে লাইন-শেষ সবসময় একটি '\n', তাই text mode-এ Windows-এর লাইব্রেরি লেখার সময় প্রতিটি \n-কে \r\n বানায় আর পড়ার সময় উল্টোটা করে। একে বলে newline translation.

  • লেখা ফাইলের জন্য সুবিধা: আপনার কোড সব প্ল্যাটফর্মে একই, আর Windows-এর পুরোনো প্রোগ্রামও ফাইলটি ঠিকমতো দেখায়।
  • অন্য সবকিছুর জন্য বিপদ: একটি ছবি বা সংখ্যার ফাইলে মান 10-এর বাইট আসলে “newline” নয়, শুধু একটি সংখ্যা। text mode সেটিকে দুই বাইট বানিয়ে ফাইল নষ্ট করবে। Windows-এর C লাইব্রেরি text mode-এ পড়ার সময় মান 26 (Ctrl+Z)-এর বাইটকে ফাইলের শেষও ধরে নিতে পারে। তাই লেখা নয় এমন সব ফাইলে binary mode.
  • মিশ্র জগতে: Linux সার্ভারে Windows থেকে আসা CSV পড়লে প্রতিটি লাইনের শেষে '\r' থেকে যায়, কারণ Linux-এ কোনো রূপান্তর হয় না। টেকসই পার্সার এটা মেনে নেয়।

struct-এর বাইট সরাসরি লেখা — কেন এড়িয়ে চলবেন

binary ফাইলের সবচেয়ে লোভনীয় শর্টকাট — আর AI-এর প্রিয় — হলো একটি struct-এর মেমরি হুবহু ফাইলে ঢেলে দেওয়া:

struct Sale { char item[16]; int qty; double price; };

Sale s{"tea", 12, 10.0};
out.write(reinterpret_cast<const char*>(&s), sizeof s);   // এক লাইনে "সেভ"!
in.read(reinterpret_cast<char*>(&s), sizeof s);           // এক লাইনে "লোড"!

একই কম্পিউটারে, একই compiler-এ, একই সেটিংসে এটা কাজ করে — আর সেজন্যই ফাঁদ। ফাইলটি অন্য কোথাও নিলে বা কয়েক বছর পরে খুললে চারটি জিনিস ভাঙতে পারে:

  1. Padding: qty-এর পরে double-কে ৮-এর গুণিতক ঠিকানায় বসাতে compiler হয়তো ৪টি ফাঁকা বাইট দিয়েছে। কয়টি, তা compiler আর প্ল্যাটফর্মের ওপর নির্ভর করে। সেই ফাঁকা বাইটে মেমরির পুরোনো আবর্জনাও ফাইলে চলে যায় — কখনো কখনো এমন তথ্য, যা বাইরে যাওয়ার কথা ছিল না।
  2. মাপ: standard শুধু বলে int অন্তত ১৬ বিট; বেশিরভাগ জায়গায় ৩২ বিট, কিন্তু নিশ্চয়তা নেই। long Windows-এ ৩২ বিট, ৬৪-বিট Linux-এ ৬৪ বিট।
  3. বাইটের ক্রম (endianness): 1000 সংখ্যাটি (hex 0x03E8) x86 আর বেশিরভাগ ARM-এ মেমরিতে থাকে E8 03 00 00 (little-endian), কিছু অন্য প্রসেসর আর নেটওয়ার্ক প্রোটোকলে 00 00 03 E8 (big-endian). ভুল ক্রমে পড়লে 1000 হয়ে যায় একটি বিশাল সংখ্যা।
  4. নকশা বদল: পরের সংস্করণে struct-এ একটি discount ঘর যোগ করলেন — পুরোনো সব ফাইল এখন ভুলভাবে পড়বে, আর কোন ফাইল কোন সংস্করণের, বোঝার উপায় নেই। আর struct-এ যদি std::string বা pointer থাকে, ফাইলে যায় শুধু মেমরির ঠিকানা, আসল লেখা নয় — পরের বার চালালে সেই ঠিকানা অর্থহীন, পড়লেই undefined behavior.

সমাধান হলো নিজের format নিজে নির্ধারণ করা, আর প্রতিটি ঘর আলাদা করে লেখা-পড়া — একে বলে serialization. text হলে: ঘরের ক্রম, বিভাজক, সংখ্যার রূপ (দশমিক বিন্দু, locale-নিরপেক্ষ), আর প্রথম লাইনে একটি সংস্করণ বা শিরোনাম। binary হলে: প্রতিটি সংখ্যা নির্দিষ্ট মাপের টাইপে (std::uint32_t), নির্দিষ্ট বাইট-ক্রমে (C++20-এর std::endian দিয়ে জানা যায় নিজের যন্ত্রের ক্রম, C++23-এর std::byteswap দিয়ে উল্টানো যায়), ফাইলের শুরুতে একটি “magic” চিহ্ন আর সংস্করণ নম্বর। বড় প্রজেক্টে এই কাজের জন্য Protocol Buffers বা JSON-এর মতো প্রতিষ্ঠিত format আর লাইব্রেরিই সাধারণত ভালো পছন্দ।

মূল ধারণা

মেমরির বিন্যাস (layout) compiler-এর ব্যক্তিগত ব্যাপার; ফাইলের format হলো সবার সাথে একটি প্রকাশ্য চুক্তি। দুটিকে এক ধরে নিলে আজকের compiler-এর খেয়ালখুশি আপনার কয়েক বছরের ডেটার স্থায়ী নিয়ম হয়ে যায়।

৬ · std::filesystem — পথ, ফোল্ডার আর ভুলের দুই রকম খবর

ফাইলের ভেতরের বাইট পড়ে-লেখে stream; আর ফাইলের বাইরের প্রশ্ন — কোথায় আছে, আছে কি না, কত বড়, ফোল্ডারে আর কী আছে, কপি বা মোছা — এসবের জন্য C++17-এর std::filesystem (header <filesystem>). এর আগে এসব কাজে Windows-এ এক API, Linux-এ আরেক API লিখতে হতো। পর্ব ৪১-এ এর এক ঝলক দেখেছিলেন; এবার পুরো ছবি। সাধারণত লেখা হয় namespace fs = std::filesystem;.

path — string নয়, পথের নিজস্ব টাইপ

fs::path শুধু একটি string-এর মোড়ক নয়; এটি পথের অংশগুলো বোঝে। / অপারেটর দুটি অংশ জোড়ে আর প্রয়োজনমতো বিভাজক বসায়, তাই "C:\\data\\" + name-এর মতো হাতে জোড়ার দরকার নেই:

namespace fs = std::filesystem;
fs::path dir = "shop";
fs::path p = dir / "2026" / "sales.csv";   // shop/2026/sales.csv (Windows-এ \ দিয়ে)

p.filename();      // "sales.csv"
p.stem();          // "sales"
p.extension();     // ".csv"
p.parent_path();   // "shop/2026"
p.replace_extension(".bak");   // p এখন shop/2026/sales.bak

Windows নিজেও / বিভাজক মেনে নেয়, তাই সোজা / লেখা portable; কিন্তু "\\" শুধু Windows-এ বিভাজক, Linux-এ সেটা ফাইলের নামের একটি সাধারণ অক্ষর! তাই "reports\\jan.csv" Linux-এ একটি ফোল্ডার নয়, বরং অদ্ভুত নামের একটি ফাইল। নিয়ম সহজ: পথ জোড়ায় শুধু / অপারেটর।

প্রশ্ন আর কাজ — এক নজরে

ফাংশনকাজমনে রাখুন
fs::exists(p)পথে কিছু আছে কি নাউত্তর পাওয়ার পরের মুহূর্তেই বদলে যেতে পারে
fs::is_regular_file(p), fs::is_directory(p)কী ধরনের জিনিসsymlink নিজে থেকে অনুসরণ করে
fs::file_size(p)বাইটে আকারশুধু সাধারণ ফাইলের জন্য নির্ভরযোগ্য
fs::create_directories(p)দরকারি সব ফোল্ডার একসাথে তৈরিআগে থেকে থাকলে ভুল নয়, false ফেরত
fs::copy_file(a, b, opt)ফাইল কপিb থাকলে ডিফল্টে ভুল; overwrite_existing দিন
fs::rename(a, b)নাম বদল বা সরানোএকই ডিস্কের ভেতরে এক ধাপে; অংশ ৭ দেখুন
fs::remove(p), fs::remove_all(p)মোছাremove_all পুরো ফোল্ডার গাছ মোছে — Recycle Bin নেই
fs::directory_iteratorএকটি ফোল্ডারের ভেতরের তালিকাক্রম অনির্দিষ্ট; . আর .. আসে না
fs::recursive_directory_iteratorভেতরের সব ফোল্ডারসহ পুরো গাছবড় ডিস্কে খুব ধীর হতে পারে

ভুলের দুই রকম খবর — exception অথবা error_code

filesystem-এর প্রায় প্রতিটি ফাংশনের দুটি রূপ আছে। শেষে একটি std::error_code না দিলে ভুল হলে ছোড়ে fs::filesystem_error — একটি exception, যার ভেতরে ভুলের কোড (code()) আর জড়িত পথ (path1(), path2()) থাকে। শেষে std::error_code& দিলে কোনো exception নেই; ভুলটি সেই ভ্যারিয়েবলে লেখা হয়, আর আপনাকে নিজে দেখতে হয়।

exception রূপ

ভুল সত্যিই ব্যতিক্রম হলে — যেমন অ্যাপের নিজের ডেটা ফোল্ডার তৈরি হলো না। ভুলে যাওয়ার উপায় নেই; ধরা না হলে প্রোগ্রাম থামবে। পর্ব ৩৭-এর নিয়ম মেনে ওপরের কোনো স্তরে catch করুন।

fs::file_size(p)

error_code রূপ

ভুল স্বাভাবিক ঘটনা হলে — যেমন হাজার ফাইল স্ক্যানে কয়েকটি পড়ার অনুমতি নেই। প্রতিটির জন্য exception ছোড়া-ধরা ধীর আর কোড এলোমেলো করে। কিন্তু ec না দেখলে ভুল নীরবে হারায়।

fs::file_size(p, ec)

এবার সব একসাথে: একটি ফোল্ডার তৈরি, তাতে তিনটি ফাইল নিরাপদ পদ্ধতিতে লেখা (পরের অংশে এর কারণ), শুধু .txt ফাইলগুলো আকারসহ তালিকা, আর exception ছাড়া একটি ভুল ধরা:

#include <algorithm>
#include <filesystem>
#include <fstream>
#include <print>
#include <vector>
namespace fs = std::filesystem;
int main() {
    const fs::path dir = fs::path{"shop"} / "notes";
    fs::create_directories(dir);                          // like mkdir -p
    for (std::string_view name : {"stock.txt", "dues.txt", "app.ini"}) {
        fs::path tmp = dir / name;  tmp += ".tmp";
        std::ofstream out{tmp};
        out << "data for " << name << '\n';
        out.close();                                      // flush now, then check
        if (!out) return 1;                               // never rename a broken file
        fs::rename(tmp, dir / name);                      // replace in one step
    }
    std::vector<std::pair<std::string, std::uintmax_t>> txt;
    for (const auto& e : fs::directory_iterator{dir})
        if (e.path().extension() == ".txt") txt.emplace_back(e.path().filename().string(), e.file_size());
    std::ranges::sort(txt);                               // directory order is unspecified
    for (const auto& [name, size] : txt) std::println("{:<10}{:>3} bytes", name, size);
    std::error_code ec;
    fs::rename(dir / "old.txt", dir / "x.txt", ec);       // no exception: error goes to ec
    std::println("old.txt not found? {}", ec == std::errc::no_such_file_or_directory);
}

আউটপুট তিন লাইন: dues.txt 18 bytes, stock.txt 19 bytes, আর old.txt not found? true. "data for dues.txt\n"-এ ১৮টি বাইট — ১৭টি অক্ষর আর একটি newline (Linux-এ; Windows-এর text mode-এ newline দুই বাইট হতো, তখন ১৯)। app.ini তালিকায় নেই, কারণ extension মেলেনি। directory_iterator কোন ক্রমে নাম দেবে, standard তা বলে না — ডিস্ক আর অপারেটিং সিস্টেমভেদে আলাদা — তাই প্রিন্টের আগে sort করা হয়েছে, যাতে আউটপুট সব জায়গায় এক। শেষ অংশে old.txt নেই, তবু প্রোগ্রাম থামেনি: ভুলটি ec-তে এসেছে, আর আমরা সেটি std::errc-এর সাথে মিলিয়ে দেখেছি। প্রোগ্রাম কোথাও পূর্ণ পথ (absolute path) প্রিন্ট করেনি — সেটা একেক কম্পিউটারে একেক রকম।

একটি ফোল্ডারের ভেতরের সব ফোল্ডারসহ হাঁটতে চাইলে recursive_directory_iterator. এটি গাছের মতো কাঠামোয় কীভাবে এগোয়, দেখুন (নামের ক্রম এখানে একটি সম্ভাব্য উদাহরণ মাত্র):

1fs::recursive_directory_iterator it{"shop"};2for (; it != fs::end(it); ++it)3    if (it->is_regular_file())4        found.push_back(it->path());5std::ranges::sort(found);
এই ধাপে
এখন যেখানে
it.depth()
ধরন
found-এ কয়টি
  1. প্রথম জিনিসটি একটি ফোল্ডার। shop/2026 ফাইল নয়, তাই তালিকায় যায় না। কিন্তু recursive iterator পরের ধাপে এর ভেতরে ঢুকবে; depth() বলে শুরুর ফোল্ডার থেকে কত স্তর নিচে আছি।
  2. এক স্তর নিচে। jan.csv, গভীরতা ১. ফাইল, তাই তালিকায় গেল।
  3. ক্রম আপনার ধারণামতো না-ও হতে পারে। এই উদাহরণে iterator একটি ফাইল দেখিয়ে ওপরের স্তরের notes.txt-এ গেল, তারপর আবার 2026-এর আরেকটি ফাইলে। আসলে কোন ক্রম আসবে, তা ফাইল সিস্টেমের ওপর নির্ভর করে; শুধু নিশ্চিত যে প্রতিটি জিনিস একবার করে আসবে (মাঝপথে কেউ ফোল্ডার না বদলালে)।
  4. শেষ ফাইল। feb.csv এল সবার শেষে। ফোল্ডারে যাওয়ার অনুমতি না থাকলে iterator exception ছুড়ত; fs::directory_options::skip_permission_denied দিলে সেই ফোল্ডার বাদ দিয়ে এগিয়ে যায়।
  5. সাজিয়ে নিন। sort-এর পরে তালিকা সব কম্পিউটারে এক। ব্যবহারকারীকে দেখানো, টেস্টে তুলনা, বা দুটি ফোল্ডার মেলানো — সবখানে এই ধাপ জরুরি। আর মনে রাখুন, symlink দিয়ে বানানো চক্রে আটকে না যেতে iterator ডিফল্টে symlink-করা ফোল্ডারের ভেতরে ঢোকে না।

৭ · নিরাপদে সেভ — temp ফাইলে লিখে তারপর rename

ধরুন করিম ভাইয়ের পুরো বছরের খাতা একটি ledger.csv-এ। অ্যাপ প্রতি সন্ধ্যায় নতুন হিসাবসহ পুরো ফাইলটি আবার লেখে: std::ofstream out{"ledger.csv"}; তারপর সব লাইন। ঠিক লেখার মাঝখানে লোডশেডিং হলো, আর UPS নেই। পরের দিন কী পাওয়া যাবে? ধাপে ধাপে দুটি কৌশল পাশাপাশি দেখুন:

শুরুডিস্কে
ledger.csv      300 লাইন, পুরো বছরের সঠিক হিসাব
সরাসরি: std::ofstream out{"ledger.csv"}খোলামাত্র trunc
ledger.csv      0 বাইট   ← পুরোনো হিসাব এখনই শেষ
                         (একটি লাইনও লেখার আগে)
সরাসরি: লেখার মাঝে crashঅর্ধেক ফাইল
ledger.csv      ১৪০ লাইন, শেষ লাইন অর্ধেক কাটা
                পুরোনো ৩০০ লাইন? আর নেই।
নিরাপদ: লিখি ledger.csv.tmp-এআসল ফাইল অক্ষত
ledger.csv      300 লাইন, সঠিক   ← কেউ ছোঁয়নি
ledger.csv.tmp  ১৪০ লাইন... crash!
পরের দিন: পুরোনো খাতা পুরো আছে, .tmp ফেলে দিন
নিরাপদ: লেখা শেষ, close() ও যাচাইদুটি পূর্ণ ফাইল
ledger.csv      300 লাইন (পুরোনো)
ledger.csv.tmp  301 লাইন (নতুন, পুরো লেখা)
out.close(); if (!out) → rename করবেন না
নিরাপদ: fs::rename(tmp, "ledger.csv")এক ধাপে বদল
ledger.csv      301 লাইন (নতুন)
যে কেউ যেকোনো মুহূর্তে খুললে পাবে:
পুরো পুরোনো, অথবা পুরো নতুন — অর্ধেক কখনো নয়
  1. শুরুর অবস্থা। একটি সঠিক, পূর্ণ খাতা। লক্ষ্য: নতুন দিনের হিসাবসহ এটি হালনাগাদ করা, আর কোনো অবস্থাতেই বছরের হিসাব না হারানো।
  2. সরাসরি পদ্ধতির প্রথম আঘাত। ofstream-এর ডিফল্ট mode ফাইল খোলার মুহূর্তেই সেটি শূন্য করে দেয়। এখন থেকে লেখা শেষ হওয়া পর্যন্ত ডিস্কে কোনো পূর্ণ খাতা নেই — এই সময়টুকুই বিপদের জানালা।
  3. জানালার ভেতরে crash. বিদ্যুৎ গেল, বা প্রোগ্রাম একটি বাগে মারা গেল। ফাইলে আছে যতটুকু লেখা ডিস্কে পৌঁছেছিল — শেষ লাইন হয়তো মাঝপথে কাটা। পুরোনো খাতা ফেরত পাওয়ার কোনো উপায় নেই।
  4. নিরাপদ পদ্ধতি: আলাদা ফাইলে লেখা। একই ফোল্ডারে ledger.csv.tmp তৈরি করে সেখানে নতুন খাতা লিখছি। এখন crash হলে শুধু temp ফাইলটি অসম্পূর্ণ; আসল খাতা অক্ষত। পরের বার অ্যাপ চালু হলে পুরোনো .tmp মুছে আবার শুরু করলেই হলো।
  5. শেষ করে যাচাই। সব লেখা শেষে close() ডেকে stream-এর অবস্থা দেখি — ডিস্ক ভরে গিয়ে থাকলে এখানেই ধরা পড়বে, আর আমরা rename করব না। বিদ্যুৎ চলে যাওয়ার বিরুদ্ধেও নিশ্চিত হতে চাইলে এখানে OS-এর fsync দিয়ে temp ফাইলটি ডিস্কে বসিয়ে নিতে হয়।
  6. এক ধাপে বদল। POSIX সিস্টেমে (Linux, macOS) rename একই ফাইল সিস্টেমের ভেতরে atomic: পুরোনো নামটি এক মুহূর্তে নতুন ফাইলের দিকে ঘুরে যায়। কোনো পাঠক অর্ধেক ফাইল দেখে না। ভেতরে আসলে বাইট কপি হয় না, শুধু ফোল্ডারের “নাম → ফাইল” তালিকার একটি ঘর বদলায়।

কয়েকটি শর্ত মনে রাখুন। temp ফাইল একই ফোল্ডারে রাখুন — rename শুধু একই ফাইল সিস্টেমের ভেতরে এক ধাপে কাজ করে; অন্য ডিস্কে (যেমন সিস্টেমের temp ফোল্ডার আলাদা partition-এ থাকলে) সেটা ব্যর্থ হয়, আর তখন কপি করে মোছা আর atomic নয়। temp ফাইলের নাম অনন্য রাখুন, যাতে একই অ্যাপের দুটি কপি একে অপরের temp ফাইল মুছে না দেয়। Windows-এ std::filesystem::rename সাধারণত বিদ্যমান ফাইল প্রতিস্থাপন করে, আর বাস্তবে এক ধাপেই ঘটে, তবে POSIX-এর মতো আনুষ্ঠানিক atomic নিশ্চয়তা নথিতে নেই — আর অন্য প্রোগ্রাম ফাইলটি খুলে রাখলে rename ব্যর্থ হতে পারে। তাই rename-এর ফলও যাচাই করুন।

এই কৌশল কোথায় লাগে না? লগ ফাইলে — সেখানে আপনি শুধু শেষে যোগ করেন (app), পুরোনো লেখা ছোঁয়া হয় না, তাই crash হলে বড়জোর শেষ লাইনটি অসম্পূর্ণ থাকে। আর কোথায় এটা যথেষ্ট নয়? যেখানে প্রতি সেকেন্ডে শত শত ছোট হালনাগাদ, বা একাধিক প্রোগ্রাম একসাথে লেখে — সেখানে পুরো ফাইল বারবার লেখা অসম্ভব রকম ধীর। তখন SQLite-এর মতো একটি ডেটাবেস ব্যবহার করুন, যা journal দিয়ে এই সমস্যার পূর্ণ সমাধান করে রেখেছে।

৮ · বাংলা নামের ফাইল, Unicode আর ব্যবহারকারীর দেওয়া পথ

করিম ভাই চান ফাইলের নাম হোক হিসাব.csv. শুনতে সহজ, কিন্তু এখানে অপারেটিং সিস্টেমগুলোর ভেতরের পার্থক্য সামনে চলে আসে। Linux আর macOS-এ ফাইলের নাম হলো বাইটের সারি, আর প্রচলিত নিয়মে সেগুলো UTF-8: প্রতিটি বাংলা অক্ষর তিন বাইট। Windows-এ ফাইলের নাম রাখা হয় UTF-16-এ — ১৬-বিটের একক — আর তাই সেখানে fs::path-এর ভেতরের অক্ষর-টাইপ wchar_t, Linux-এ char.

  • কীভাবে বানাবেন: C++20 থেকে fs::path p{u8"হিসাব.csv"}; — u8 literal-এর টাইপ char8_t, আর path জানে এটি UTF-8, তাই সব প্ল্যাটফর্মে সঠিক নাম হয়। C++17-এর fs::u8path() একই কাজ করত, কিন্তু C++20-এ deprecated.
  • কীভাবে ফেরত নেবেন: p.string() দেয় প্ল্যাটফর্মের “স্থানীয়” narrow encoding-এ। Linux-এ সাধারণত UTF-8, কিন্তু Windows-এ পুরোনো ANSI code page, যেখানে বাংলা অক্ষর নেই — রূপান্তর ব্যর্থ হয়ে exception বা বিকৃত নাম আসতে পারে। নিশ্চিত UTF-8 চাইলে p.u8string() (C++20-এ এটি std::u8string ফেরত দেয়), আর Windows-এর নিজের API-তে দিতে p.native() বা p.wstring().
  • বাইট গোনা বনাম অক্ষর গোনা: Linux-এ u8"হিসাব.csv"-এর filename().string().size() হয় 19 — “হিসাব”-এর পাঁচটি Unicode অক্ষর (হ, ি, স, া, ব) × ৩ বাইট, যোগ .csv-এর ৪. “নামটি ২০ অক্ষরের বেশি নয়” যাচাই করতে বাইট গুনলে ভুল হবে।

কনসোলে বাংলা প্রিন্ট করাও আলাদা প্রশ্ন — terminal-এর encoding ঠিক না থাকলে সঠিক নামও প্রশ্নবোধক চিহ্ন হয়ে দেখায়। ফাইলের নাম ঠিক আছে কি না যাচাই করতে কনসোলের ছবির ওপর ভরসা না করে fs::exists দিয়ে দেখুন।

ব্যবহারকারীর দেওয়া পথ — বিশ্বাস নয়, যাচাই

ধরুন একটি ওয়েব সার্ভিস ব্যবহারকারীকে রিপোর্ট ডাউনলোড করতে দেয়: reports_dir / user_input. ব্যবহারকারী যদি নাম পাঠান ../config/secret.ini? .. মানে “এক ফোল্ডার ওপরে”, তাই পথটি আপনার রিপোর্ট ফোল্ডার থেকে বেরিয়ে অন্য জায়গায় চলে যায়। একে বলে path traversal. আরও সূক্ষ্ম একটি ফাঁদ fs::path-এর নিজের নিয়মে: ডান দিকের অংশ যদি পূর্ণ পথ (absolute) হয়, / বাঁ দিকটা ফেলে দেয় — fs::path{"data"} / "/etc/passwd"-এর ফল শুধু /etc/passwd.

  • নীতি ১ — যতটা সম্ভব পথ নেবেন না, নাম নেবেন: ব্যবহারকারী একটি আইডি বা নাম দেবেন; আপনি নিজের তালিকা থেকে আসল ফাইল খুঁজবেন। শুধু অক্ষর, সংখ্যা, - আর _ অনুমোদন করা (allowlist) সবচেয়ে সরল প্রতিরক্ষা।
  • নীতি ২ — পথ নিতেই হলে আগে নিয়মিত রূপে আনুন, তারপর সীমা দেখুন: fs::weakly_canonical বা fs::canonical (canonical path) .., . আর symlink মিটিয়ে আসল পূর্ণ পথ দেয়। সেই পথটি আপনার অনুমোদিত ফোল্ডারের ভেতরে কি না, অংশ ধরে মিলিয়ে দেখুন — শুধু string-এর শুরু মিলিয়ে নয়, কারণ /data/reports2-ও /data/reports দিয়ে শুরু হয়।
  • নীতি ৩ — যাচাই আর ব্যবহারের মাঝের সময় মনে রাখুন: যাচাইয়ের পরে কেউ ফাইলের জায়গায় symlink বসিয়ে দিতে পারে। নিরাপত্তা-সংবেদনশীল সার্ভারে প্রোগ্রামকে সীমিত অনুমতিতে চালানো আর অপারেটিং সিস্টেমের সুরক্ষা ব্যবহার করাই শেষ প্রতিরক্ষা।
পরীক্ষা ও ইন্টারভিউতে
  • while (!f.eof()) লুপে সমস্যা কী? — eof() সত্য হয় শেষের পরে পড়ার চেষ্টার পরেই, তাই শেষ পাকে ব্যর্থ পড়া প্রক্রিয়া হয় (শেষ মান দুবার বা খালি লাইন)। ঠিক রূপ: while (std::getline(f, line)) বা while (f >> x).
  • ofstream বন্ধ করতে close() কি বাধ্যতামূলক? — না, destructor নিজেই flush করে বন্ধ করে (RAII). তবে বন্ধের সময়ের ভুল দেখতে চাইলে নিজে close() ডেকে অবস্থা যাচাই করুন।
  • ios::app আর ios::trunc-এর পার্থক্য? — app প্রতিটি লেখা শেষে যোগ করে, পুরোনো লেখা রাখে; trunc খোলার সময়েই ফাইল খালি করে। ofstream-এর ডিফল্ট আচরণ trunc-এর মতো।
  • struct সরাসরি write() করে সেভ করলে সমস্যা কী? — padding, টাইপের মাপ আর endianness প্ল্যাটফর্মভেদে আলাদা, আর pointer বা string-এর ঠিকানা অর্থহীন; তাই ফাইল portable নয়।
  • filesystem-এ exception আর error_code রূপের পার্থক্য? — error_code না দিলে ভুলে filesystem_error ছোড়ে; দিলে exception নেই, ভুল সেই ভ্যারিয়েবলে আসে, যা নিজে দেখতে হয়।

৯ · AI-এর যুগে — ফাইলের কোডে AI-এর চেনা ভুল

ফাইলের কোড AI খুব দ্রুত লেখে, আর প্রথম দেখায় সেটি ঠিকঠাক চলে — কারণ পরীক্ষার সময় ফাইলটি থাকে, ডিস্ক খালি থাকে, কেউ বিদ্যুৎ বন্ধ করে না। ভুলগুলো লুকিয়ে থাকে “অসুখী পথে”: ফাইল নেই, শেষ লাইনে newline নেই, Windows থেকে আসা \r, ভরা ডিস্ক, বাংলা নাম। ইন্টারনেটের পুরোনো উদাহরণ থেকে শেখা AI-এর চেনা ভুলগুলো:

AI যা লেখেসমস্যাযা হওয়া উচিত
while (!f.eof())শেষ মান দুবার, বা একটি খালি লাইনwhile (std::getline(f, line))
খোলার পরে কোনো যাচাই নেইফাইল না থাকলে নীরবে “০ টাকা”if (!in) আর পরিষ্কার বার্তা
write((char*)&s, sizeof s)padding, মাপ, endianness — portable নয়নির্ধারিত text বা binary format, ঘর ধরে লেখা
dir + "\\" + nameLinux-এ ভাঙে; ডান দিক absolute হলে বিপদfs::path আর / অপারেটর
ec দিয়ে ডেকে আর দেখে নাভুল নীরবে হারায়প্রতিটি ডাকের পরে if (ec)
গুরুত্বপূর্ণ ফাইল সরাসরি overwriteমাঝপথে crash হলে সব হারায়temp ফাইল, close() যাচাই, তারপর rename
প্রতি লাইনে std::endlপ্রতিবার flush, বড় ফাইলে খুব ধীর'\n' বা std::println
ব্যবহারকারীর নাম সরাসরি পথে../ দিয়ে ফোল্ডারের বাইরে যাওয়াallowlist, বা canonical করে সীমা যাচাই

AI-কে ফাইলের কোড লিখতে বলার সময় “অসুখী পথগুলো” আগেই বলে দিন। নিচের prompt-টি কপি করে নিজের প্রয়োজনে বদলে নিন:

আমি C++23 শিখছি (g++, -Wall -Wextra). একটি দোকানের দিনের বিক্রির খাতার জন্য দুটি ফাংশন লেখো: save_ledger আর load_ledger. ১. format: প্রথম লাইন "item,qty,price", তারপর প্রতি লাইনে একটি বিক্রি। কোনো ঘরে কমা থাকবে না। ২. পড়ার লুপ হবে getline দিয়ে; while(!eof()) একেবারেই নয়। লাইনের শেষের '\r' ছেঁটে নেবে। ৩. প্রতিটি ভুল লাইন লাইন-নম্বরসহ জানাবে, কিন্তু বাকি লাইন পড়া থামাবে না। সংখ্যা পড়বে std::from_chars দিয়ে। ৪. সেভ করবে একই ফোল্ডারে একটি temp ফাইলে; close() করে অবস্থা যাচাই করবে, তারপর std::filesystem::rename. ৫. পথ জোড়বে শুধু std::filesystem::path আর / দিয়ে; "\\" দিয়ে string জোড়া নয়। struct-এর কাঁচা বাইট ফাইলে লেখা নয়। ৬. filesystem-এর যেখানে error_code নেবে, সেখানে প্রতিবার সেটি যাচাই করবে। কোডের পরে বলো: কোন কোন অবস্থায় ফাংশনগুলো ব্যর্থ হতে পারে, আর বিদ্যুৎ চলে গেলেও ডেটা নিশ্চিত রাখতে এর বাইরে কী লাগবে।

AI-এর লেখা ফাইল-কোড যাচাইয়ের চেকলিস্ট

ভাবনার প্রশ্ন

প্রতিটি প্রশ্ন নিয়ে নিজে কিছুক্ষণ ভাবুন — তারপর “→ উত্তর” চাপুন। এগুলোই আপনাকে কোড-লেখক থেকে সিস্টেম-চিন্তক বানাবে।

প্র ০১iostreams প্রায় চার দশকের পুরোনো নকশা, আর “ধীর” বলে এর দুর্নাম আছে। এটি কীভাবে তৈরি হলো, দুর্নাম কতটা সত্য — আর C++23-এর std::print কেন এল?

C++-এর প্রথম দিকে Bjarne Stroustrup নিজে একটি ছোট “stream” লাইব্রেরি লিখেছিলেন, যা ১৯৮৫ সালের প্রথম বাণিজ্যিক C++ (cfront Release 1.0)-এর সাথে আসে। এর মূল ধারণা ছিল printf-এর দুর্বলতা দূর করা: format string আর আর্গুমেন্টের টাইপ না মিললে printf চুপচাপ আবর্জনা ছাপে বা crash করে, আর ব্যবহারকারীর নিজের টাইপ ছাপার উপায় নেই। << অপারেটর overload করলে টাইপের মিল compiler নিজে দেখে, আর যেকোনো class নিজের operator<< দিয়ে stream-এ ছাপা যায়। পরে Jerry Schwarz লাইব্রেরিটি নতুন করে নকশা ও বাস্তবায়ন করেন — আজকের “iostreams” নামটি, manipulator, আর streambuf ও formatting স্তর আলাদা রাখার নকশা অনেকটাই তাঁর কাজ, যা ১৯৮৯ সালের দিকে cfront 2.0-র সাথে আসে। ১৯৯৮ সালের standard-এ এগুলো template হয় (basic_istream<charT, traits>), আর যোগ হয় locale ব্যবস্থা, যাতে একই কোড অন্য ভাষার অক্ষর আর দশমিক চিহ্ন সামলাতে পারে।

দুর্নামের কারণগুলো বাস্তব। ডিফল্টে cout C-এর stdout-এর সাথে তাল মিলিয়ে চলে (sync_with_stdio), যাতে দুটি মিশিয়ে লিখলেও ক্রম ঠিক থাকে — এর দাম গতি। cin প্রতিটি পড়ার আগে cout flush করে (tie). প্রতিটি সংখ্যা ছাপতে locale-এর facet ডাকা হয়, streambuf-এর ভেতরে virtual ডাক থাকে, আর std::hex-এর মতো manipulator stream-এর অবস্থা স্থায়ীভাবে বদলে দেয় — এক ফাংশনে hex চালু করলে অন্য ফাংশনের আউটপুটও hex হয়ে যায়। তাই কনটেস্ট প্রোগ্রামাররা শুরুতেই sync_with_stdio(false) আর cin.tie(nullptr) লেখেন (পর্ব ৫০)। তবে ফাইল stream-এ এই সমন্বয়ের খরচ নেই, আর ভালোভাবে লেখা iostream কোড প্রায়ই যথেষ্ট দ্রুত; বেশিরভাগ “ধীর” অভিযোগের পেছনে থাকে প্রতি লাইনে endl.

আসল সমস্যা ছিল নকশার: ফরম্যাট ছড়িয়ে থাকে অনেক <<-এর মাঝে, অনুবাদের জন্য বাক্যের ক্রম বদলানো যায় না, আর অবস্থা স্থায়ী। Victor Zverovich-এর মুক্ত-উৎস {fmt} লাইব্রেরি দেখাল যে printf-এর মতো পড়ার সুবিধা আর iostreams-এর টাইপ-নিরাপত্তা একসাথে পাওয়া যায়, compile-এর সময় format string যাচাইসহ, আর প্রায়ই দুটির চেয়েই দ্রুত। এর ভিত্তিতে C++20-এ std::format আর C++23-এ std::print আসে — যা ফাইলেও লেখে।

মূল উপলব্ধি: iostreams-এর “stream” বিমূর্ততা — কনসোল, ফাইল আর string একই ইন্টারফেসে — টিকে আছে, কারণ সেটা ভালো ধারণা। বদলেছে শুধু ফরম্যাট করার উপায়। পুরোনো নকশার কোন অংশ রাখার মতো আর কোন অংশ বদলানোর মতো, তা আলাদা করে দেখতে শিখুন।

প্র ০২C++ ১৯৯৮ সালে standard হলো, অথচ একটি ফোল্ডারের তালিকা নেওয়ার portable উপায় এল ২০১৭ সালে। Boost.Filesystem থেকে std::filesystem-এর যাত্রায় এত সময় লাগল কেন?

ফাইল সিস্টেম দেখতে সরল, কিন্তু অপারেটিং সিস্টেমগুলোর মধ্যে এর পার্থক্য গভীর। Windows-এ ড্রাইভ অক্ষর (C:), দুই রকম বিভাজক, বড়-ছোট হাতের অক্ষরে পার্থক্য নেই, নাম UTF-16-এ; POSIX-এ একটিমাত্র মূল /, নাম বাইটের সারি, বড়-ছোট হাতের অক্ষর আলাদা। symlink, অনুমতি, সময়ের নির্ভুলতা — সবই আলাদা। C-এর standard এসব এড়িয়ে গিয়েছিল; C++98-ও তাই। প্রতিটি প্রজেক্ট নিজের মোড়ক লিখত, বা #ifdef _WIN32 দিয়ে দুই রকম কোড।

Beman Dawes — Boost প্রজেক্টের প্রতিষ্ঠাতাদের একজন, আর বহু বছর C++ কমিটির লাইব্রেরি দলের নেতৃত্বে থাকা মানুষ — ২০০০-এর দশকের শুরুতে Boost.Filesystem লেখেন। নকশার মূল সিদ্ধান্তগুলো সেখানেই পাকা হয়: path একটি আলাদা টাইপ, যা ভেতরে প্ল্যাটফর্মের নিজের রূপে রাখে আর বাইরে portable রূপ দেয়; / দিয়ে জোড়া; আর প্রতিটি ফাংশনের দুটি রূপ — exception ছোড়া আর error_code নেওয়া। শেষেরটি এসেছিল বাস্তব অভিজ্ঞতা থেকে: কেউ চান ভুল হলে প্রোগ্রাম থামুক, কেউ হাজার ফাইল স্ক্যানে প্রতিটি ভুলের জন্য exception চান না। লাইব্রেরিটি কয়েকবার বড় সংস্করণ বদলায় — বিশেষ করে Windows-এ path কোন অক্ষর-টাইপে রাখবে, সেই প্রশ্নে। বাস্তবে এক দশকের বেশি ব্যবহার আর পরিবর্তনের পর এটি standard-এর আগে একটি Technical Specification হিসেবে প্রকাশিত হয় (২০১৫), যাতে compiler-নির্মাতারা std::experimental::filesystem নামে বাস্তবায়ন করে অভিজ্ঞতা জানাতে পারেন। সেই অভিজ্ঞতা নিয়ে C++17-এ এটি পূর্ণ standard হয়।

দেরির আরেকটি কারণ ছিল standard-এর নিজের প্রকৃতি। এমন ব্যবস্থাও আছে যেখানে ফোল্ডারই নেই, বা ফাইল সিস্টেমই নেই (ছোট embedded যন্ত্র)। তাই standard বলে, যেখানে সম্ভব নয়, সেখানে বাস্তবায়ন ভুল জানাতে পারে। আর race-এর সমস্যা: exists(p) সত্য বলার পরের মুহূর্তেই অন্য প্রোগ্রাম ফাইলটি মুছে দিতে পারে — ফাইল সিস্টেম একটি ভাগ করা জিনিস, আর standard এ নিয়ে কোনো প্রতিশ্রুতি দিতে পারে না। ব্যবহারের প্রথম কয়েক বছরে GCC-তে আলাদা লাইব্রেরি (-lstdc++fs) link করতে হতো, যা GCC 9 থেকে আর লাগে না।

মূল উপলব্ধি: standard-এ যা ঢোকে, তা বহু বছরের বাস্তব ব্যবহারে পরীক্ষিত হয়ে ঢোকে — Boost থেকে TS, তারপর standard. আর filesystem-এর “দুই রূপের ভুল” নকশা দেখায়, ভুল সামলানোর একটিমাত্র ঠিক উপায় নেই; প্রেক্ষাপট ঠিক করে কোনটি স্বাভাবিক আর কোনটি ব্যতিক্রম।

প্র ০৩temp ফাইল লিখে rename করলে crash-এ ডেটা বাঁচে কেন? আর “বিদ্যুৎ চলে গেলেও নিশ্চিত” হতে fsync নিয়ে কোন সতর্কতা জানা দরকার?

POSIX standard rename()-এর জন্য একটি বিশেষ প্রতিশ্রুতি দেয়: নতুন নামে আগে থেকে একটি ফাইল থাকলে সেটি এমনভাবে প্রতিস্থাপিত হবে যে অন্য কোনো প্রোগ্রাম কখনো দেখবে না যে নামটি নেই — হয় পুরোনো ফাইল, নয় নতুন। কারণ ফোল্ডার আসলে একটি তালিকা: “এই নাম → ডিস্কের এই ফাইল-রেকর্ড” (Unix-এ inode). rename সেই তালিকার একটি ঘর বদলায়, বাইট কপি করে না। একটি ঘর বদলানো ফাইল সিস্টেম একটিমাত্র কাজ হিসেবে করতে পারে। তাই সম্পাদক, প্যাকেজ ম্যানেজার, ব্রাউজারের সেটিংস — অসংখ্য প্রোগ্রাম এই কৌশলে সেভ করে।

কিন্তু এই প্রতিশ্রুতি প্রোগ্রামের crash-এর বিরুদ্ধে, পুরো কম্পিউটার হঠাৎ বন্ধ হওয়ার বিরুদ্ধে নয়। লেখা প্রথমে থাকে OS-এর cache-এ। ফাইল সিস্টেম ডিস্কে কোনটি আগে পাঠাবে — temp ফাইলের বাইট না rename-এর হিসাব — তা নিজের সুবিধামতো ঠিক করতে পারে। ২০০৯ সালে Linux-এর ext4 ফাইল সিস্টেম নিয়ে একটি আলোচিত ঘটনা আছে: delayed allocation-এর কারণে ডেটা ডিস্কে লেখার আগেই rename ডিস্কে পৌঁছাতে পারত, আর crash-এর পরে কিছু ব্যবহারকারী তাঁদের ডেস্কটপের config ফাইল শূন্য বাইটের পেয়েছিলেন। ext4-এর রক্ষণাবেক্ষণকারীরা পরে এমন rename-এর ক্ষেত্রে ডেটা আগে লেখার একটি সুরক্ষা যোগ করেন, কিন্তু মূল শিক্ষা থেকে যায়: নিশ্চয়তা চাইলে নিজে চাইতে হয়।

পূর্ণ নিরাপদ ক্রম তাই এমন: temp ফাইলে লেখা, fsync দিয়ে সেটি ডিস্কে বসানো, rename, আর তারপর ফোল্ডারটিকেও fsync করা, যাতে নামের বদলটিও স্থায়ী হয়। standard C++-এ fsync নেই — POSIX-এ fsync(), Windows-এ FlushFileBuffers() ডাকতে হয়, আর std::ofstream থেকে নিচের file descriptor পাওয়ার standard উপায়ও নেই, তাই অনেকে এই অংশ সরাসরি OS-এর API দিয়ে লেখেন। আরও কিছু সূক্ষ্মতা: কিছু ডিস্ক নিজের cache-এর কথা OS-কে মিথ্যা বলে; আর fsync ব্যর্থ হলে আবার চেষ্টা করলেই যে ডেটা নিরাপদ, তা নয় — ২০১৮ সালে PostgreSQL-এর ডেভেলপাররা এ নিয়ে বড় আলোচনা করেন। fsync ধীরও — প্রতিটি ডাক কয়েক মিলিসেকেন্ড নিতে পারে, তাই প্রতি লাইনে নয়, গুরুত্বপূর্ণ মুহূর্তে।

মূল উপলব্ধি: “atomic” আর “durable” আলাদা প্রতিশ্রুতি। rename দেয় atomicity — অর্ধেক ফাইল কেউ দেখবে না; fsync দেয় durability — বিদ্যুৎ গেলেও থাকবে। কোনটি কতটুকু দরকার, তা ডেটার মূল্য দেখে ঠিক করুন, আর বেশি দরকার হলে ডেটাবেসকে কাজটি দিন।

প্র ০৪Windows-এ ফাইলের পথ wchar_t-এ কেন, আর Linux-এ সাধারণ char-এ কেন? বাংলা নামের ফাইলের জন্য এই ইতিহাস কী সমস্যা তৈরি করে?

১৯৮০-র দশকের শেষে আর ৯০-এর শুরুতে Unicode তৈরি হচ্ছিল এই ধারণায় যে পৃথিবীর সব অক্ষর ১৬ বিটে (৬৫,৫৩৬টি ঘর) ধরে যাবে। Microsoft তখন Windows NT বানাচ্ছিল (১৯৯৩), আর ভবিষ্যৎমুখী সিদ্ধান্ত নিল: ভেতরের সব লেখা, ফাইলের নামসহ, হবে ১৬-বিটের Unicode-এ। Windows-এর C/C++ compiler-এ wchar_t তাই ১৬ বিট, আর প্রতিটি Win32 API-র দুটি রূপ — A (পুরোনো “ANSI” code page-এর জন্য) আর W (wide, Unicode). পরে দেখা গেল ১৬ বিট যথেষ্ট নয় — ১৯৯৬ সালের Unicode 2.0 থেকে আরও অক্ষরের জন্য surrogate pair আনা হয়, আর ১৬-বিটের এনকোডিং হয়ে যায় UTF-16, যেখানে কিছু অক্ষর দুটি একক নেয়। কিন্তু ততদিনে Windows-এর ভিত্তি তৈরি হয়ে গেছে।

Unix-এর পথ ছিল ভিন্ন। ১৯৯২ সালে Ken Thompson আর Rob Pike UTF-8 নকশা করেন — ASCII অক্ষর এক বাইটে থাকে, আর বাকি অক্ষর কয়েক বাইটে, এমনভাবে যে / বা শূন্য বাইট কখনো কোনো বহু-বাইট অক্ষরের ভেতরে আসে না। তাই পুরোনো Unix কোড, যা ফাইলের নামকে শুধু “/ দিয়ে ভাগ করা বাইট” হিসেবে দেখে, কিছু না বদলেই UTF-8 নাম সামলাতে পারে। Linux kernel আজও নামের encoding জানে না; UTF-8 শুধু একটি প্রচলিত নিয়ম।

C++-এর fs::path তাই প্রতিটি প্ল্যাটফর্মের নিজের রূপ ভেতরে রাখে — Windows-এ wchar_t, POSIX-এ char — আর রূপান্তর করে শুধু যখন আপনি চান। বিপদ আসে string()-এ: Windows-এ এটি পুরোনো ANSI code page-এ রূপান্তর করে, যেখানে বাংলা নেই। এই কারণেই C++20-এ char8_t আর u8string() এল, যাতে UTF-8 লেখা টাইপ দেখেই চেনা যায়। (Windows 10-এর একটি হালনাগাদ থেকে প্রোগ্রাম নিজের code page হিসেবে UTF-8 বেছে নিতে পারে, যা পরিস্থিতি অনেক সহজ করেছে।) বাংলার জন্য আরেকটি সূক্ষ্ম সমস্যা আছে: একই দেখতে নাম দুই রকম কোড-পয়েন্টে লেখা যায়। যেমন “ড়” একটি আলাদা অক্ষর (U+09DC) হিসেবেও লেখা যায়, আবার “ড” + নুক্তা (U+09A1 U+09BC) হিসেবেও। Unicode-এর normalization নিয়মে দুটি সমান, কিন্তু ফাইল সিস্টেমের কাছে বাইট আলাদা — তাই দুটি “একই” নামের ফাইল পাশাপাশি থাকতে পারে, বা খুঁজে পাওয়া যায় না।

মূল উপলব্ধি: লেখা মানে বাইট নয়; বাইট থেকে লেখায় যেতে সবসময় একটি encoding লাগে। ফাইলের নামের ক্ষেত্রে প্রতিটি অপারেটিং সিস্টেমের ইতিহাস সেই encoding ঠিক করে দিয়েছে। portable কোডে fs::path-কে রূপান্তরের দায়িত্ব দিন, আর UTF-8 চাইলে স্পষ্ট করে u8 বলুন।

প্র ০৫read/write ছাড়াও ফাইল পড়ার আরেকটি উপায় আছে — memory-mapped file. এটি কী, কেন কখনো কখনো দ্রুত, আর ডেটাবেসের মতো সিস্টেমে এ নিয়ে বিতর্ক কেন?

সাধারণ পড়ায় বাইটগুলো দুবার নড়ে: ডিস্ক থেকে OS-এর cache-এ, তারপর cache থেকে আপনার buffer-এ, আর প্রতিটি read একটি system call. memory-mapped file-এ (POSIX-এ mmap, Windows-এ CreateFileMapping) আপনি OS-কে বলেন: “এই ফাইলটিকে আমার প্রোগ্রামের ঠিকানার জায়গার একটি অংশ বানিয়ে দাও।” এরপর ফাইলটি দেখতে একটি বিশাল array — data[1000000] পড়লে OS নিজে বুঝে নেয় কোন পৃষ্ঠা (page, সাধারণত ৪ KB) ডিস্ক থেকে আনতে হবে, আর সরাসরি cache-এর পৃষ্ঠাটিই আপনার ঠিকানায় বসিয়ে দেয়। কপি নেই, প্রতিবার system call নেই। একই ফাইল একাধিক প্রোগ্রাম map করলে তারা একই মেমরি ভাগ করে।

এ কারণে এলোমেলো জায়গা থেকে পড়ার কাজে — বড় index, ছবি, compiler-এর object ফাইল, অনুসন্ধান — এটি প্রায়ই দ্রুত আর কোড সরল করে। অপারেটিং সিস্টেম নিজেই প্রোগ্রাম চালু করার সময় executable আর shared library এভাবে map করে। LMDB-এর মতো কিছু ডেটাবেস পুরো নকশাই এর ওপর দাঁড় করিয়েছে, আর MongoDB-র প্রথম দিকের storage engine-ও এর ওপর নির্ভর করত।

দামও আছে। ভুল আর error code হয়ে আসে না: ফাইলটি মাঝপথে অন্য কেউ ছোট করে দিলে, বা নেটওয়ার্ক ডিস্ক হারিয়ে গেলে, array পড়ার মুহূর্তেই প্রোগ্রাম একটি signal (Linux-এ SIGBUS) পেয়ে মারা যেতে পারে। কোন মুহূর্তে ডিস্ক থেকে পড়া হবে বা ডিস্কে লেখা হবে, তার নিয়ন্ত্রণ চলে যায় OS-এর হাতে — ডেটাবেসের জন্য, যাদের নিজেদের ঠিক করতে হয় কোন লেখা কখন স্থায়ী হবে, এটি বড় সমস্যা। ২০২২ সালে কয়েকজন ডেটাবেস-গবেষক (Andrew Pavlo প্রমুখ) একটি আলোচিত গবেষণাপত্রে যুক্তি দেন যে সাধারণ ডেটাবেসে নিজের buffer ব্যবস্থাপনার বদলে mmap-এ ভরসা করা প্রায়ই ভুল সিদ্ধান্ত। ৩২-বিট যন্ত্রে ঠিকানার জায়গা কম থাকায় বড় ফাইল map করাও যেত না। আর standard C++-এ mmap নেই; Boost.Interprocess বা ছোট মুক্ত-উৎস লাইব্রেরি, বা সরাসরি OS-এর API লাগে।

মূল উপলব্ধি: ফাইল পড়ার “সেরা” উপায় নেই, আছে কাজের ধরন। পুরো ফাইল একবার শুরু থেকে শেষ পর্যন্ত পড়তে stream-ই যথেষ্ট; এলোমেলো জায়গা থেকে বারবার পড়তে mmap চমৎকার; আর কখন কী স্থায়ী হবে তার কঠোর নিয়ন্ত্রণ দরকার হলে নিজের হাতে নিয়ন্ত্রণ রাখাই নিরাপদ।

অনুশীলন

বেশিরভাগ কাজে কোড লিখতে হবে না — আগে অনুমান করুন, তারপর উত্তর মিলিয়ে নিন।

  1. আউটপুট অনুমান: একটি ফাইলে আছে a\nb\n (দুটি লাইন, শেষে newline). (ক) while (!f.eof()) { std::getline(f, s); ++n; } শেষে n কত? (খ) while (std::getline(f, s)) ++n; শেষে? (গ) ফাইলটি যদি a\nb হয় (শেষে newline নেই), তাহলে দুটির উত্তর কী?

    (ক) 3 — দুটি সঠিক লাইনের পরে তৃতীয় পাকে getline কিছুই পায় না, s খালি, তবু গোনা হয়। (খ) 2. (গ) দুটিই 2! শেষে newline না থাকলে দ্বিতীয় getline “b” পড়তে গিয়েই ফাইলের শেষে ধাক্কা খায়, তাই eof সঙ্গে সঙ্গে ওঠে আর eof-লুপও ঠিক থামে। এ কারণেই বাগটি বিপজ্জনক: আপনার পরীক্ষার ফাইলে শেষ newline না থাকলে সব ঠিক দেখায়, আর অন্য সম্পাদকে সেভ করা ফাইলে (যেটি শেষে newline দেয়) হঠাৎ একটি বাড়তি খালি লাইন আসে।

  2. বাগ শিকার: একটি AI দোকানের সেটিংস সেভ করতে এই কোড দিয়েছে — std::ofstream f(folder + "\\" + name); f.write((char*)&settings, sizeof settings);, যেখানে settings একটি struct, যার ভেতরে একটি std::string shop_name আর একটি int vat. কয়টি সমস্যা খুঁজে পান?

    অন্তত পাঁচটি: (১) "\\" দিয়ে পথ জোড়া — Linux-এ এটা বিভাজক নয়; fs::path আর / লাগবে। (২) খোলা সফল কি না যাচাই নেই। (৩) binary mode নেই — Windows-এ মান 10-এর বাইট দুই বাইট হয়ে ফাইল নষ্ট। (৪) সবচেয়ে গুরুতর: std::string-এর বাইট লেখা মানে ভেতরের pointer আর আকার লেখা, দোকানের নাম নয়। পড়ে ফেরত আনলে undefined behavior. (৫) padding আর মাপ প্ল্যাটফর্মভেদে আলাদা; লেখার পরে অবস্থা যাচাই নেই; সরাসরি overwrite. ঠিক রূপ: shop_name=... আর vat=15 লাইনসহ একটি text config, temp + rename দিয়ে সেভ।

  3. নকশা ভাবুন: একটি মোবাইল রিচার্জের দোকান প্রতিটি লেনদেন একটি ফাইলে রাখতে চায় — দিনে কয়েকশো লেনদেন, আর মাসের শেষে হিসাব। দুটি নকশা: (ক) প্রতিটি লেনদেনের পরে পুরো তালিকা temp + rename দিয়ে নতুন করে লেখা (খ) প্রতিটি লেনদেন app mode-এ শেষে একটি লাইন যোগ। কোনটি বেছে নেবেন, আর কেন? crash হলে প্রতিটিতে কী হারায়?

    (খ) ভালো। লেনদেন একটি ঘটনার ধারা (log), আর পুরোনো লাইন কখনো বদলায় না — শেষে যোগ করাই স্বাভাবিক। প্রতিটি লেনদেনে পুরো ফাইল লেখা (ক) মাসের শেষে কয়েক হাজার লাইন বারবার লেখা, অকারণে ধীর। crash হলে (খ)-তে বড়জোর শেষ লাইনটি অসম্পূর্ণ থাকে — পড়ার সময় অসম্পূর্ণ শেষ লাইন (newline ছাড়া, বা ঘর কম) চিনে বাদ দিলেই হলো, আর প্রতিটি লেনদেনের পরে flush করলে আগের লাইনগুলো নিরাপদ। (ক)-তে temp + rename থাকায় পুরোনো সংস্করণ বাঁচে, কিন্তু শেষ লেনদেন হারায়। মাসের শেষের হিসাব (একটি সারাংশ ফাইল) বানাতে আবার temp + rename ঠিক আছে — সেটি একবার লেখা হয়।

  4. নিজে লিখুন (কোড): উপরের ledger.cpp-এ পড়ার আগে আরেকটি ব্লক যোগ করুন, যা std::ios::app দিয়ে খুলে "puri,10,5\n" লাইনটি যোগ করে। আউটপুটে কী বদলাবে আগে অনুমান করুন। তারপর app-এর বদলে শুধু ডিফল্ট mode দিয়ে চালান — এবার কী হয়, আর কেন?

    app দিলে শেষে একটি নতুন লাইন puri 10 x 5 = 50 আসবে, আর total = 485 taka. ডিফল্ট mode-এ খোলার মুহূর্তেই ফাইল খালি হয়ে যায়, ফাইলে থাকে শুধু puri,10,5. প্রোগ্রাম সেটিকেই শিরোনাম ভেবে বাদ দেয় (প্রথম getline), তাই কোনো পণ্যই ছাপা হয় না: শুধু total = 0 taka. খেয়াল করুন, কোথাও কোনো error আসেনি — প্রোগ্রাম “সফলভাবে” ভুল উত্তর দিয়েছে। শিরোনাম লাইনটি সত্যিই item,qty,price কি না যাচাই করলে এই ভুল সঙ্গে সঙ্গে ধরা পড়ত।

নিজেকে যাচাই করুন

ভুল হলে কারণসহ ব্যাখ্যা পাবেন, আবার চেষ্টাও করতে পারবেন।

ক্রমC++-এ একটি গুরুত্বপূর্ণ ফাইল নিরাপদে হালনাগাদ করার ধাপগুলো সঠিক ক্রমে সাজান (▲▼ চেপে সরান)

  1. একই ফোল্ডারে একটি অনন্য নামের temp ফাইলের path ঠিক করা
  2. temp ফাইলে ofstream দিয়ে পুরো নতুন ডেটা লেখা
  3. close() ডেকে stream-এর অবস্থা যাচাই করা
  4. প্রয়োজনে OS-এর fsync দিয়ে temp ফাইল ডিস্কে বসানো
  5. fs::rename দিয়ে temp ফাইলকে আসল নামে প্রতিস্থাপন করা

C++-এ একটি ফাইলে 120, 75, 240 আলাদা লাইনে আছে, শেষেও newline. while (!f.eof()) { f >> x; sum += x; } (শুরুতে x = 0, sum = 0) চালানোর পরে sum কত?

  • 435
  • 675
  • 0
  • লুপ কখনো থামে না

C++-এ একটি বিদ্যমান লগ ফাইলের পুরোনো লেখা অক্ষত রেখে শেষে নতুন লাইন যোগ করতে std::ofstream কোন mode-এ খুলবেন?

  • std::ios::out
  • std::ios::trunc
  • std::ios::app
  • std::ios::binary

C++17-এ POSIX সিস্টেমে std::filesystem::path{"data"} / "/etc/passwd"-এর ফল কী?

  • data/etc/passwd
  • data//etc/passwd
  • filesystem_error exception
  • /etc/passwd

C++ প্রোগ্রামে একটি গুরুত্বপূর্ণ ফাইল সরাসরি overwrite না করে আগে temp ফাইলে লিখে তারপর rename করার মূল কারণ কী?

  • লেখার মাঝে crash হলেও ফাইলটি কখনো অর্ধেক-লেখা অবস্থায় দেখা যায় না
  • এতে লেখা অনেক দ্রুত হয়
  • এতে fsync ছাড়াই বিদ্যুৎ চলে গেলেও ডেটা নিশ্চিত থাকে
  • rename না করলে ofstream ফাইল বন্ধ করে না

একটি C++ struct-এর মেমরি write(reinterpret_cast<const char*>(&s), sizeof s) দিয়ে ফাইলে লিখে অন্য প্ল্যাটফর্মে পড়ার সবচেয়ে বড় সমস্যা কী?

  • কোডটি compile হয় না
  • মেমরির বিন্যাস প্ল্যাটফর্মভেদে আলাদা, তাই ফাইলটি অন্য জায়গায় ভুলভাবে পড়া হতে পারে
  • এটি text লেখার চেয়ে অনেক ধীর
  • ফাইলের আকার ৪ KB-র বেশি হতে পারে না

C++17-এ std::error_code ec; auto n = std::filesystem::file_size("missing.txt", ec); — ফাইলটি না থাকলে কী হয়?

  • filesystem_error exception ছোড়া হয়
  • প্রোগ্রাম সঙ্গে সঙ্গে বন্ধ হয়ে যায়
  • exception হয় না; ভুলটি ec-তে আসে, আর তা নিজে যাচাই করতে হয়
  • n হয় 0, আর ec খালি থাকে

শব্দকোষ

লেখার মধ্যে ডটেড দাগ দেওয়া শব্দে মাউস রাখুন বা চাপুন — অর্থ দেখাবে। এই পর্বের সব নতুন শব্দ এখানে একসাথে:

মনে রাখুন

  1. ফাইল = বাইটের সারি + আপনার format-এর চুক্তি। লাইন, CSV, config বা binary — নিয়ম লিখে রাখুন, শিরোনাম বা সংস্করণ দিন।
  2. ফাইল stream মানে cin/cout-এর মতোই। ofstream-এর ডিফল্ট খোলাতেই পুরোনো লেখা মোছে — যোগ করতে app; বন্ধ হয় RAII-তে, কিন্তু গুরুত্বপূর্ণ লেখায় close() করে যাচাই।
  3. পড়ার কাজটিই লুপের শর্ত: while (std::getline(f, line)); প্রতিটি লাইন আলাদা করে পার্স করুন, ভুল লাইন নাম ধরে জানান।
  4. struct-এর কাঁচা বাইট নয়, নির্ধারিত format; লেখা নয় এমন ফাইলে binary mode; পথ জোড়ায় fs::path আর /; error_code দিলে তা দেখুন।
  5. গুরুত্বপূর্ণ ফাইল: temp → close ও যাচাই → (fsync) → rename. ফোল্ডারের তালিকা sort করে দেখান, আর ব্যবহারকারীর দেওয়া পথ যাচাই করুন।

পরের পর্বে: এ পর্যন্ত আপনার প্রোগ্রাম একবারে একটিই কাজ করেছে। এবার একসাথে একাধিক কাজ — std::thread দিয়ে নতুন thread চালানো, mutex দিয়ে ভাগ করা ডেটা পাহারা দেওয়া, আর condition_variable দিয়ে একে অপরের জন্য অপেক্ষা।

আরও পড়ুন · ABCL TECH-এ আপনার পরবর্তী পদক্ষেপ

এই পর্বটি শেয়ার করুন:
এই পর্বে ভুল পেয়েছেন? জানান