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

প্রিপ্রসেসর ও ম্যাক্রো — compile-এর আগে কোডের লেখা বদল

The C Preprocessor — #include, #define, Macro Pitfalls & Conditional Compilation

Taught in Bangla, this lesson explains the C preprocessor: #include and include guards, #define constants compared with const and enum, function-like macro pitfalls, the do-while(0) idiom, conditional compilation, __FILE__ and __LINE__, assert, and # and ##.

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

  • Preprocessor কেন “লেখা-বদলের যন্ত্র”, আর এই একটি সত্য থেকেই কীভাবে তার সব শক্তি ও সব ফাঁদ আসে
  • #include-এর <> আর ""-এর পার্থক্য, include guard ও #pragma once
  • #define, const আর enum — কোন ধ্রুবকের জন্য কোনটি
  • Function-like macro-র ফাঁদ, do { } while (0) কৌশল, আর কেন আজ static inline ভালো
  • #ifdef দিয়ে debug/release ও Windows/Linux-এর আলাদা কোড, আর __FILE__, __LINE__, assert দিয়ে ভুল ধরা

১ · Compile-এর আগে একজন লেখা-বদলকারী

পর্ব ০৩-এ আপনি toolchain-এর চার ধাপ দেখেছিলেন। প্রথম ধাপের নাম ছিল preprocessor — সে #include <stdio.h> দেখে পুরো ফাইলটি এনে বসিয়ে দেয়। gcc -E দিয়ে তার কাজের ফলও দেখেছিলেন। এই পর্বে আমরা সেই ধাপটিকেই কাছ থেকে দেখব, কারণ এখানে এমন কিছু ভুল জন্মায় যা পরে compiler বা debugger দিয়ে খুঁজে পাওয়া খুব কঠিন।

একটি তুলনা দিয়ে শুরু করি। Microsoft Word-এ “Find & Replace” আছে — “ঢাকা” খুঁজে সব জায়গায় “Dhaka” বসিয়ে দেয়। Word কিন্তু বোঝে না শব্দটা কোথায় নাম, কোথায় বাক্যের অংশ; সে শুধু অক্ষর মিলিয়ে বদলায়। Preprocessor ঠিক এই কাজটিই করে — আপনার .c ফাইলের ওপর, compiler পড়ার আগে।

ধরুন একটি কোচিং সেন্টারে প্রশ্নপত্র ছাপা হবে। ছাপাখানায় পাঠানোর আগে অফিসের একজন কর্মী খসড়াটি হাতে নেন। খসড়ায় লেখা আছে “[এখানে গত বছরের ৫টি প্রশ্ন বসাও]”, “[পরীক্ষার তারিখ]”, “[শুধু বিজ্ঞান বিভাগের জন্য: …]”। কর্মী প্রশ্নগুলো কপি করে বসান, তারিখ লিখে দেন, আর মানবিক বিভাগের কপি থেকে বিজ্ঞানের অংশ কেটে দেন। তিনি প্রশ্নের বিষয় বোঝেন না, বোঝার দরকারও নেই — তিনি শুধু নির্দেশ মেনে লেখা বদলান। ছাপাখানা (compiler) পায় চূড়ান্ত, পরিষ্কার একটি কপি। Preprocessor হলো সেই কর্মী।

Preprocessor-কে দেওয়া প্রতিটি নির্দেশকে বলে directive. চেনার উপায় সহজ — লাইনের প্রথম অক্ষর (ফাঁকা জায়গা বাদে) #. মূল directive-গুলো হলো:

Directiveকাজপ্রশ্নপত্রের তুলনায়
#includeঅন্য ফাইলের পুরো লেখা এনে এখানে বসাও“গত বছরের প্রশ্ন এখানে বসাও”
#define / #undefএকটি নামকে অন্য লেখা দিয়ে বদলানোর নিয়ম বানাও / মুছে ফেলো“[তারিখ] যেখানে আছে, ১৫ মার্চ লেখো”
#if, #ifdef, #ifndef, #elif, #else, #endifশর্ত মিললে অংশটি রাখো, না মিললে পুরো অংশটি ফেলে দাও“শুধু বিজ্ঞান বিভাগের কপিতে”
#errorএখানে পৌঁছালে build থামিয়ে দাও, এই বার্তা দেখাও“বিভাগ না জানালে ছাপা বন্ধ”
#pragmaCompiler-কে বিশেষ নির্দেশ (compiler ভেদে আলাদা)ছাপাখানার জন্য আলাদা চিরকুট
মূল ধারণা

Preprocessor চলে compiler-এর আগে, আর সে C ভাষা বোঝে না। int কী, কোন ভ্যারিয়েবল কোন ফাংশনের ভেতরে, কোন অপারেটর আগে হিসাব হবে — এসব তার কাছে অর্থহীন। সে দেখে শুধু শব্দের টুকরো (token) আর লেখা। এই পর্বের প্রতিটি ফাঁদ এই একটি বাক্য থেকেই এসেছে। মনে রাখলে বাকিটা প্রায় নিজেই বুঝে যাবেন।

Preprocessor কাজ শেষ করলে যে একটি বড় লেখা তৈরি হয় — আপনার .c ফাইল আর তার ভেতরে ঢোকা সব header মিলিয়ে — compiler সেটিকেই একটি একক হিসেবে compile করে। এই এককের নাম translation unit. পরের অংশগুলোতে শব্দটি কাজে লাগবে।

C-এর শুরুর দিকে preprocessor ছিল না। Dennis Ritchie নিজের লেখা C-এর ইতিহাসে জানিয়েছেন, ১৯৭২-৭৩ সালের দিকে এটি যোগ হয়, আর প্রথম সংস্করণে ছিল মাত্র দুটি জিনিস — ফাইল #include করা আর parameter ছাড়া সাধারণ #define. Argument নেওয়া macro আর conditional compilation যোগ হয় এর কিছু পরে। মজার ব্যাপার হলো, শুরুতে একে ভাষার একটি ঐচ্ছিক সংযোজন হিসেবে দেখা হতো — বেশ কিছুদিন ফাইলের শুরুতে বিশেষ চিহ্ন না থাকলে preprocessor চালানোই হতো না। আজ এটি C স্ট্যান্ডার্ডের অবিচ্ছেদ্য অংশ।

২ · #include — অন্য ফাইল এনে বসানো, আর দুবার বসলে বিপদ

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

#include <stdio.h>

কোণা বন্ধনী মানে “সিস্টেমের header”. Compiler তার নিজের জানা ফোল্ডারগুলোতে খোঁজে — যেমন Linux-এ /usr/include. Standard library ও ইনস্টল করা library-র জন্য এটি।

লাইব্রেরির তাক থেকে বই আনা

#include "student.h"

উদ্ধৃতি চিহ্ন মানে “আমার প্রজেক্টের header”. GCC-সহ প্রচলিত compiler আগে যে ফাইলে #include লেখা, তার নিজের ফোল্ডারে খোঁজে; না পেলে সিস্টেমের ফোল্ডারগুলোতে যায়।

নিজের ব্যাগ থেকে খাতা বের করা

অন্য কোনো ফোল্ডারে header রাখলে compiler-কে -I flag দিয়ে ঠিকানা বলে দিতে হয়, যেমন gcc -Iinclude main.c. এই খোঁজার তালিকাকে বলে include path. Header না পেলে কী হয়, পর্ব ০৩-এ দেখেছেন — preprocessor থেমে যায়, No such file or directory.

একই header দুবার ঢুকলে কী হয়

সমস্যাটা আসে বড় প্রজেক্টে। একটি ছাত্র-ফলাফল সফটওয়্যারের কথা ভাবুন। date.h-তে তারিখের একটি struct আছে। student.h-এর দরকার জন্মতারিখ, তাই সে date.h include করে। result.h-এর দরকার পরীক্ষার তারিখ, তাই সে-ও date.h include করে। আর main.c দুটো header-ই include করে। কেউ কোনো ভুল করেনি — তবু build ভেঙে যায়। নিচের ডায়াগ্রামে দেখুন কেন:

main.cদুটো header-ই চায়
student.hজন্মতারিখ লাগবে
result.hপরীক্ষার তারিখ লাগবে
date.hপ্রথমবার
date.hদ্বিতীয়বার
চারটি ফাইলপ্রত্যেকটি আলাদাভাবে ঠিক আছে
/* date.h */
struct Date { int day, month, year; };

/* student.h */
#include "date.h"
struct Student { char name[40]; struct Date birth; };

/* result.h */
#include "date.h"
struct Result { int marks; struct Date exam_day; };

/* main.c */
#include "student.h"
#include "result.h"
main.i (তৈরি হচ্ছে)student.h খোলা হলো
/* student.h থেকে → তার ভেতরের date.h থেকে: */
struct Date { int day, month, year; };
struct Student { char name[40]; struct Date birth; };
main.i (তৈরি হচ্ছে)এবার result.h খোলা হলো
struct Date { int day, month, year; };
struct Student { char name[40]; struct Date birth; };
/* result.h থেকে → তার ভেতরের date.h থেকে আবার: */
struct Date { int day, month, year; };
struct Result { int marks; struct Date exam_day; };
TerminalCompiler একই struct দুবার দেখল
$ gcc main.c -o result
In file included from result.h:1,
                 from main.c:2:
date.h:1:8: error: redefinition of 'struct Date'
date.h (guard সহ)প্রথমবার: DATE_H এখনো নেই
#ifndef DATE_H        /* DATE_H সংজ্ঞায়িত নয়? → হ্যাঁ, ভেতরে ঢোকো */
#define DATE_H        /* এখন থেকে DATE_H আছে */

struct Date { int day, month, year; };   /* রাখা হলো */

#endif
main.i (চূড়ান্ত)দ্বিতীয়বার পুরো ফাইল বাদ
struct Date { int day, month, year; };
struct Student { char name[40]; struct Date birth; };
/* result.h → date.h: DATE_H আগেই আছে, তাই #ifndef মিথ্যা — কিছুই বসল না */
struct Result { int marks; struct Date exam_day; };
/* struct Date মাত্র একবার — build সফল */
  1. চারটি নির্দোষ ফাইল। প্রতিটি header নিজের দরকারি জিনিস নিজে include করছে — এটাই ভালো অভ্যাস। কিন্তু খেয়াল করুন, date.h-তে পৌঁছানোর দুটি পথ আছে।
  2. প্রথম পথ: student.h. Preprocessor main.c-এর প্রথম লাইনে student.h খুলল। তার ভেতরে #include "date.h" — তাই struct Date বসে গেল। এ পর্যন্ত সব ঠিক।
  3. দ্বিতীয় পথ: result.h. এবার result.h, যার ভেতরেও #include "date.h". Preprocessor-এর কোনো স্মৃতি নেই যে ফাইলটি আগে বসানো হয়েছে — সে আবার পুরোটা কপি করল।
  4. Compiler-এর আপত্তি। Compiler একই translation unit-এ struct Date-এর সংজ্ঞা দুবার দেখল — redefinition error. লক্ষ করুন, error দেখাচ্ছে date.h-এ, অথচ ভুলটা আসলে include-এর গঠনে। নতুনরা এখানে প্রায়ই বিভ্রান্ত হন।
  5. সমাধান: include guard. date.h-এর পুরো লেখা একটি #ifndef-এর ভেতরে রাখা হলো। প্রথমবার DATE_H নামটি সংজ্ঞায়িত নেই, তাই ভেতরে ঢুকে প্রথমেই DATE_H সংজ্ঞায়িত করা হলো, তারপর struct রাখা হলো।
  6. দ্বিতীয়বার কিছুই ঢুকল না। result.h আবার date.h খুলল, কিন্তু এবার DATE_H আগে থেকেই আছে — #ifndef মিথ্যা, তাই #endif পর্যন্ত সবকিছু বাদ। struct Date মাত্র একবার, build সফল। প্রতিটি header-এর প্রথম আর শেষ লাইন এভাবেই লেখা উচিত।

এই কৌশলের নাম include guard. নামটি সাধারণত ফাইলের নাম বড় হাতের অক্ষরে, .-এর জায়গায় _ দিয়ে — student.h হলে STUDENT_H. বড় প্রজেক্টে নামের সংঘাত এড়াতে সামনে প্রজেক্টের নামও যোগ করা হয়, যেমন RESULTAPP_STUDENT_H.

আরেকটি ছোট উপায় আছে — header-এর প্রথম লাইনে #pragma once. এর অর্থ “এই ফাইল একটি translation unit-এ একবারই বসাও”. GCC, Clang আর Microsoft-এর compiler (MSVC) — তিনটিই এটি বোঝে, তাই বাস্তবে খুব প্রচলিত। তবে #pragma once C স্ট্যান্ডার্ডের অংশ নয়। একই ফাইল যদি দুটি ভিন্ন পথে পৌঁছানো যায় (যেমন link করা ফোল্ডার বা নেটওয়ার্ক drive), কিছু পরিস্থিতিতে compiler বুঝতে পারে না দুটো আসলে একই ফাইল। তাই যেখানে সর্বোচ্চ portability দরকার, সেখানে মানুষ এখনো guard লেখে, কেউ কেউ দুটোই।

Include guard শুধু একটি translation unit-এর ভেতরে দ্বিতীয়বার ঢোকা আটকায়। main.c আর report.c — দুটি আলাদা ফাইল আলাদাভাবে compile হয়, তাই দুটোতেই date.h একবার করে ঢুকবে। Header-এ শুধু ঘোষণা থাকলে এতে সমস্যা নেই। কিন্তু header-এ কোনো ফাংশনের পুরো সংজ্ঞা বা ভ্যারিয়েবল রাখলে দুটি .o ফাইলে একই নাম দুবার আসে, আর linker বলে multiple definition. Guard সেটা আটকাতে পারে না। কোন জিনিস header-এ যায় আর কোনটি .c-তে, তা নিয়ে বিস্তারিত পর্ব ২৯-এ।

৩ · #define দিয়ে ধ্রুবক — const আর enum-এর সাথে তুলনা

#define-এর সবচেয়ে সরল রূপ একটি নামকে একটি লেখা দিয়ে বদলায়:

#define MAX_STUDENTS 60
#define PASS_MARK    33

int marks[MAX_STUDENTS];          /* preprocessor-এর পরে: int marks[60]; */

এ ধরনের নির্দেশকে বলে macro, আর যেহেতু এটি দেখতে একটি সাধারণ নামের মতো, তাই এর পুরো নাম object-like macro. Preprocessor ফাইলের যেখানেই MAX_STUDENTS শব্দটি পায়, সেখানে 60 লিখে দেয়। একে বলে macro expansion. পর্ব ০৬-এ দেখেছেন, এভাবে magic number-কে নাম দিলে কোড পড়া সহজ হয়, আর নিয়ম বদলালে এক জায়গায় বদলালেই চলে।

কিন্তু পর্ব ০৬-এ একই কাজের জন্য const শিখেছিলেন, আর পর্ব ২২-এ enum. তাহলে কোনটি কখন? তিনটির স্বভাব পাশাপাশি দেখুন:

প্রশ্ন#define N 60const int N = 60;enum { N = 60 };
কে দেখে?শুধু preprocessor; compiler দেখে শুধু 60Compiler — এটি টাইপসহ একটি আসল ভ্যারিয়েবলCompiler — টাইপ int-এর একটি নামযুক্ত ধ্রুবক
টাইপ যাচাইনেই, শুধু লেখাআছেআছে (সবসময় int)
Scope মানে?না — লেখার লাইন থেকে ফাইলের শেষ পর্যন্ত সব জায়গায়হ্যাঁ, ফাংশন বা ব্লকের ভেতরে সীমিত রাখা যায়হ্যাঁ
ফাইল-স্তরের array-র মাপ int a[N];চলেC-তে চলে না — C-এর নিয়মে const ভ্যারিয়েবল “constant expression” নয়চলে
switch-এর case N:চলেচলে না, একই কারণেচলে
Debugger-এ নাম দেখা যায়?সাধারণত না — compile-এর আগেই নাম মুছে গেছেহ্যাঁহ্যাঁ
double বা string ধ্রুবকচলেচলেনা, শুধু পূর্ণসংখ্যা

C++ জানা থাকলে চতুর্থ সারিটি অবাক করবে — C++-এ const int দিয়ে array-র মাপ দেওয়া যায়, C-তে যায় না। ফাংশনের ভেতরে int a[N]; লিখলে C99 থেকে কোডটি compile হয় ঠিকই, কিন্তু তখন সেটি আর সাধারণ array নয় — একটি “variable length array”, যাকে = {0} দিয়ে শুরুর মানও দেওয়া যায় না। আর পুরোনো C89-এ এটি সরাসরি error. সর্বশেষ C23 স্ট্যান্ডার্ডে constexpr যোগ হয়ে এই ফাঁক কিছুটা পূরণ হয়েছে, তবে সব compiler-এ এখনো সমানভাবে নেই।

অভিজ্ঞ C প্রোগ্রামাররা তাই মোটামুটি এই নিয়ম মানেন:

  • পূর্ণসংখ্যার ধ্রুবক, বিশেষ করে array-র মাপ বা case: enum — টাইপ আছে, scope আছে, debugger-এ দেখা যায়।
  • দশমিক বা অন্য টাইপের ধ্রুবক: static const double VAT_RATE = 0.15;
  • #define: যখন মানটি preprocessor-এরই লাগবে (যেমন #if MAX_STUDENTS > 100), অথবা compile করার সময় বাইরে থেকে দেওয়া হবে (gcc -DMAX_STUDENTS=80).

Object-like macro-তেও বন্ধনী ভুলবেন না। #define TOTAL 40 + 20 লিখে TOTAL * 2 হিসাব করলে compiler পায় 40 + 20 * 2 — উত্তর ৮০, ১২০ নয়। লেখা উচিত #define TOTAL (40 + 20). আর #define PASS_MARK 33;-এর মতো শেষে ; দেবেন না — if (m >= PASS_MARK) তখন হয়ে যায় if (m >= 33;), আর compiler এমন জায়গায় error দেখায় যেখানে চোখে কোনো ভুল নেই।

৪ · Function-like macro — দেখতে ফাংশন, আসলে লেখা-বদল

Macro argument-ও নিতে পারে। নামের ঠিক পরেই (মাঝে কোনো ফাঁকা জায়গা ছাড়া) বন্ধনী দিলে তৈরি হয় function-like macro:

#define SQUARE_BAD(x)  x * x

দেখতে একটি ফাংশনের মতো — SQUARE_BAD(5) দিলে 5 * 5, অর্থাৎ ২৫. কিন্তু মনে রাখুন, preprocessor হিসাব করে না, শুধু x-এর জায়গায় আপনি যা লিখেছেন হুবহু সেই লেখাটি বসায়। Argument-এ একটি যোগ চিহ্ন থাকলেই গল্প বদলে যায়। পর্ব ০৭-এর precedence মনে আছে তো? নিচে ধাপে ধাপে দেখুন compiler আসলে কী পায়:

  1. আপনার লেখাSQUARE_BAD(a + 1)আশা: ১৬
  2. Preprocessora + 1 * a + 1শুধু লেখা বসাল
  3. Compiler3 + 3 + 1ফল: ৭
square.cআপনি যা লিখেছেন
#define SQUARE_BAD(x)  x * x

int a = 3;
int r = SQUARE_BAD(a + 1);   /* আশা: (3+1)*(3+1) = 16 */
gcc -E square.cx-এর জায়গায় হুবহু "a + 1"
int a = 3;
int r = a + 1 * a + 1;
/* x  *  x
   ↓     ↓
 a + 1 * a + 1   — বন্ধনী কোথাও নেই! */
Compiler-এর হিসাবগুণ আগে, যোগ পরে
a + 1 * a + 1
= a + (1 * a) + 1
= 3 + 3 + 1
= 7          /* 16 নয়! কোনো error নেই, কোনো warning নেই */
সংশোধিত macroপ্রতিটি x আর পুরোটা — দুই স্তরে বন্ধনী
#define SQUARE(x)  ((x) * (x))

SQUARE(a + 1)   →   ((a + 1) * (a + 1))   = 16
10 / SQUARE(2)  →   10 / ((2) * (2))      = 2
/* বাইরের বন্ধনী না থাকলে 10 / (2) * (2) = 10 হতো */
নতুন ফাঁদবন্ধনী এখানে বাঁচাতে পারে না
int i = 5;
int r = SQUARE(i++);
/* preprocessor-এর পরে: */
int r = ((i++) * (i++));
/* i একই expression-এ দুবার বাড়ছে — undefined behavior.
   ফল 30? 25? 36? কোনো নিশ্চয়তা নেই। */
static inline ফাংশনargument একবারই হিসাব হয়
static inline int square(int x) { return x * x; }

square(a + 1)  →  x = 4, ফল 16
square(i++)    →  x = 5, ফল 25, i হলো 6  /* ঠিক একবার */
square("hi")   →  compiler error: টাইপ মেলে না — ভালো!
  1. আপনার লেখা। a-এর মান ৩, তাই SQUARE_BAD(a + 1) থেকে আশা করছেন ৪ × ৪ = ১৬. যে কেউ কোডটা পড়ে এটাই ভাববেন।
  2. Preprocessor লেখা বসাল। সংজ্ঞায় আছে x * x. দুটি x-এর জায়গাতেই বসল “a + 1” লেখাটি — চারপাশে কোনো বন্ধনী ছাড়া, কারণ preprocessor বন্ধনী যোগ করে না। এই ধাপকেই বলে macro expansion.
  3. Compiler নিয়ম মেনে হিসাব করল। Compiler SQUARE_BAD নামটি কখনো দেখেইনি; সে পেয়েছে a + 1 * a + 1. Precedence অনুযায়ী গুণ আগে, তাই 1 * a = ৩, তারপর ৩ + ৩ + ১ = ৭. কোনো error বা warning নেই — প্রোগ্রাম চুপচাপ ভুল উত্তর দেয়।
  4. সমাধান: দুই স্তরে বন্ধনী। প্রতিটি x-কে আলাদা বন্ধনীতে, আর পুরো সংজ্ঞাকেও একটি বাইরের বন্ধনীতে। ভেতরের বন্ধনী argument-এর যোগ-বিয়োগ রক্ষা করে, বাইরেরটি চারপাশের ভাগ-গুণ থেকে রক্ষা করে।
  5. দ্বিতীয় ফাঁদ: দুবার হিসাব। বন্ধনী থাকলেও SQUARE(i++) লিখলে লেখাটি দুবার বসে, ফলে i++ দুবার চলে। একই expression-এ একটি ভ্যারিয়েবল দুবার বদলানো C-তে undefined behavior — ফল compiler ও optimization ভেদে যা খুশি হতে পারে। একে বলে double evaluation.
  6. আধুনিক উত্তর: static inline ফাংশন। আসল ফাংশনে argument ঠিক একবার হিসাব হয়, টাইপ যাচাই হয়, debugger-এ নাম দেখা যায়। আর optimization চালু থাকলে compiler ফাংশন-ডাকার খরচও তুলে দেয় — তাই গতিতে macro-র সাথে সাধারণত কোনো পার্থক্য থাকে না।

এবার নিজে চালিয়ে দেখুন। নিচের কোডে একই কাজ macro আর inline function — দুইভাবে করা হয়েছে। MAX macro-টি দুই স্তরে বন্ধনীসহ “সঠিকভাবে” লেখা, তবু একটি অবাক করা ফল দেয়:

#include <stdio.h>

#define SQUARE_BAD(x) x * x
#define SQUARE(x)     ((x) * (x))
#define MAX(a, b)     ((a) > (b) ? (a) : (b))

static inline int max_int(int a, int b) { return a > b ? a : b; }

int main(void) {
    int a = 3;
    printf("SQUARE_BAD(a + 1) = %d\n", SQUARE_BAD(a + 1));
    printf("SQUARE(a + 1)     = %d\n", SQUARE(a + 1));

    int i = 5, j = 5;
    int m1 = MAX(i++, 0);       /* macro */
    int m2 = max_int(j++, 0);   /* function */
    printf("macro : m1 = %d, i = %d\n", m1, i);
    printf("inline: m2 = %d, j = %d\n", m2, j);
    return 0;
}

প্রথম দুই লাইনে আসবে 7 আর 16 — বন্ধনীর পার্থক্য। আসল চমক শেষ দুই লাইনে। MAX(i++, 0) হয়ে যায় ((i++) > (0) ? (i++) : (0)). শর্ত যাচাইয়ে একবার i++ (৫ > ০ সত্য, i হলো ৬), তারপর উত্তর নিতে গিয়ে আবার i++ — তাই m1 = ৬ আর i = ৭. অথচ লিখেছিলেন একবার বাড়াতে! এখানে ?:-এর মাঝে একটি sequence point আছে বলে ফলটি অন্তত নির্দিষ্ট, কিন্তু ভুল। ফাংশনের বেলায় m2 = ৫, j = ৬ — ঠিক যা আশা করেছিলেন।

নিয়ম

Macro-কে argument দেওয়ার সময় কখনো side effect আছে এমন কিছু দেবেন না — i++, getchar(), rand(), কোনো ফাংশন যা ফাইল পড়ে বা প্রিন্ট করে। এই কারণেই C জগতে প্রথা হলো macro-র নাম সব বড় হাতের অক্ষরে লেখা (MAX, SQUARE) — যাতে নাম দেখেই পাঠক সাবধান হন, “এটি ফাংশন নয়”.

৫ · কয়েক লাইনের macro আর do { } while (0) কৌশল

কখনো macro দিয়ে একাধিক কাজ করাতে ইচ্ছে হয় — ধরুন একটি বিন্দুর দুটি স্থানাঙ্ক শূন্য করা। লম্বা macro কয়েক লাইনে লিখতে চাইলে প্রতিটি লাইনের শেষে \ দিতে হয়, কারণ preprocessor-এর কাছে একটি directive মানে একটি লাইন। এখন প্রশ্ন হলো, ভেতরের কয়েকটি statement-কে কীভাবে মোড়াবেন? তিনটি চেষ্টা ধাপে ধাপে দেখুন:

চেষ্টা ১: কোনো মোড়ক নেইif-এর পরে বন্ধনী নেই
#define RESET(p)  (p).x = 0; (p).y = 0

if (out_of_screen)
    RESET(pt);

/* preprocessor-এর পরে: */
if (out_of_screen)
    pt.x = 0;
pt.y = 0;     /* ← if-এর বাইরে! সবসময় চলে */
চেষ্টা ২: { } দিয়ে মোড়ানোশুধু if হলে কাজ করে
#define RESET(p)  { (p).x = 0; (p).y = 0; }

if (out_of_screen)
    RESET(pt);

/* preprocessor-এর পরে: */
if (out_of_screen)
    { pt.x = 0; pt.y = 0; };   /* ঠিক আছে… আপাতত */
চেষ্টা ২, সাথে elseবাড়তি ; সব ভেঙে দিল
if (out_of_screen)
    RESET(pt);
else
    moves++;

/* preprocessor-এর পরে: */
if (out_of_screen)
    { pt.x = 0; pt.y = 0; };   /* ← এই ; দিয়ে if শেষ */
else                          /* error: 'else' without a previous 'if' */
    moves++;
চেষ্টা ৩: do { } while (0)সব জায়গায় একটি সাধারণ statement
#define RESET(p)            \
    do {                    \
        (p).x = 0;          \
        (p).y = 0;          \
    } while (0)

if (out_of_screen)
    do { pt.x = 0; pt.y = 0; } while (0);   /* ; এখানে লাগবেই */
else
    moves++;                                /* ঠিকঠাক compile হয় */
  1. মোড়ক ছাড়া — নিঃশব্দ বাগ। if-এর পরে { } না থাকলে শুধু পরের একটি statement শর্তের অধীনে থাকে। Macro খুলে দুটি statement হলো, তাই দ্বিতীয়টি (pt.y = 0;) সবসময় চলে। Compiler কোনো আপত্তি করে না, কারণ ব্যাকরণে কোনো ভুল নেই।
  2. { } দিয়ে মোড়ালে আপাতত চলে। এখন দুটি কাজই এক ব্লকে। শেষে একটি বাড়তি ; থেকে গেল — সেটি একটি খালি statement, এখানে ক্ষতিহীন।
  3. কিন্তু else এলেই ভাঙে। প্রোগ্রামাররা ফাংশনের মতো করে RESET(pt); লেখেন — শেষে ; দিয়ে। ব্লকের পরের সেই ; if-কে শেষ করে দেয়, ফলে else তার জোড়া খুঁজে পায় না। Compiler error দেয় এমন লাইনে, যেখানে চোখে কোনো ভুল নেই।
  4. do { … } while (0) — একবার চলা লুপ। শর্ত 0, তাই ভেতরের কাজ ঠিক একবার চলে। কিন্তু ব্যাকরণে পুরোটা একটি statement, যার শেষে ; বাধ্যতামূলক — ঠিক একটি ফাংশন-ডাকার মতো। if, else, লুপ — সব জায়গায় নিরাপদ। Optimizer এই লুপটি পুরোপুরি মুছে দেয়, তাই কোনো খরচ নেই। Linux kernel-সহ অসংখ্য বড় C প্রজেক্টে কয়েক লাইনের macro এভাবেই লেখা হয়।

তাহলে macro কেন, যখন static inline আছে?

C99 থেকে C-তে inline keyword আছে। ছোট ফাংশনের সামনে static inline লিখে header-এ রাখলে compiler চাইলে ফাংশন-ডাকার জায়গায় সরাসরি ফাংশনের কোড বসিয়ে দেয় — পর্ব ০৩-এ যাকে inlining বলেছিলাম। এতে macro-র গতির সুবিধা পাওয়া যায়, কিন্তু তার কোনো ফাঁদ থাকে না:

বৈশিষ্ট্যFunction-like macrostatic inline ফাংশন
Argument কতবার হিসাব হয়যতবার সংজ্ঞায় লেখা আছেঠিক একবার
বন্ধনীর ঝামেলাপ্রতিটি জায়গায় নিজেকে দিতে হয়নেই — এটি আসল expression
টাইপ যাচাইনেইআছে, ভুল টাইপে compiler সতর্ক করে
Debugger-এ ধাপে ধাপে চলাপ্রায় অসম্ভবসম্ভব (optimization বন্ধ থাকলে)
Error messageখোলা লেখার ভেতরে, প্রায়ই বিভ্রান্তিকরআপনার লেখা লাইনেই
যেকোনো টাইপে একই কোডপারে (MAX int, double দুটোতেই চলে)প্রতিটি টাইপের জন্য আলাদা ফাংশন লাগে

শেষ সারিটিই macro-র একটি আসল সুবিধা — টাইপ না জেনেই কাজ করা। তবে বেশিরভাগ বাস্তব কোডে দু-একটি টাইপই লাগে। Linux kernel-এর coding style নির্দেশিকাতেও পরিষ্কার লেখা আছে: ফাংশনের মতো দেখতে macro-র চেয়ে inline ফাংশন সাধারণত ভালো। Macro রাখুন সেই কাজগুলোর জন্য যা ফাংশন পারেই না — পরের দুটি অংশে সেগুলো দেখব।

৬ · Conditional compilation — এক কোড, আলাদা প্রোগ্রাম

Preprocessor-এর সবচেয়ে কাজের ক্ষমতা সম্ভবত এটি: শর্ত অনুযায়ী কোডের একটি অংশ রাখা বা পুরোপুরি মুছে ফেলা। একে বলে conditional compilation. সাধারণ if চলে প্রোগ্রাম চলার সময়; কিন্তু #if কাজ করে compile-এর আগে, আর যে অংশ বাদ পড়ে, সেটি compiler কখনো দেখেই না — চূড়ান্ত প্রোগ্রামে তার একটি বাইটও থাকে না।

লেখাঅর্থ
#ifdef DEBUGDEBUG নামের macro সংজ্ঞায়িত থাকলে (মান যা-ই হোক)
#ifndef DATE_Hসংজ্ঞায়িত না থাকলে — include guard এটাই ব্যবহার করে
#if MAX_STUDENTS > 100পূর্ণসংখ্যার হিসাব সত্য হলে। অজানা নাম এখানে ০ ধরা হয়
#if defined(_WIN32) || defined(__linux__)কয়েকটি শর্ত একসাথে মেলাতে
#elif, #else, #endifনাহলে-যদি, নাহলে, শেষ — প্রতিটি #if-এর একটি #endif লাগবেই

শর্তে ব্যবহৃত নামগুলো আসে দুই জায়গা থেকে। কিছু compiler নিজেই সংজ্ঞায়িত করে রাখে — যেমন Windows-এ (MSVC বা MinGW, ৩২ বা ৬৪ বিট যা-ই হোক) _WIN32, Linux-এ __linux__, macOS-এ __APPLE__. আর কিছু আপনি build কমান্ডে দেন: gcc -DDEBUG app.c মানে “ফাইলের একদম শুরুতে #define DEBUG 1 লেখা আছে ধরে নাও”. একই app.c থেকে কী কী আলাদা প্রোগ্রাম তৈরি হয়, দেখুন:

app.cএকটিই ফাইল
#include <stdio.h>

#if defined(_WIN32)
    #define CLEAR_CMD "cls"
#elif defined(__linux__) || defined(__APPLE__)
    #define CLEAR_CMD "clear"
#else
    #error "Unknown platform: please define CLEAR_CMD"
#endif

int main(void) {
#ifdef DEBUG
    printf("[debug] build date: %s\n", __DATE__);
#endif
    printf("Screen clear command: %s\n", CLEAR_CMD);
    return 0;
}
Linux · gcc app.cRelease: compiler যা পায়
/* stdio.h-এর শত শত লাইন … */

int main(void) {
    printf("Screen clear command: %s\n", "clear");
    return 0;
}
/* debug লাইনটির কোনো চিহ্নই নেই */
Linux · gcc -DDEBUG app.cDebug: একটি লাইন বেশি
/* stdio.h-এর শত শত লাইন … */

int main(void) {
    printf("[debug] build date: %s\n", "Oct 16 2026");
    printf("Screen clear command: %s\n", "clear");
    return 0;
}
Windows · gcc app.c (MinGW)_WIN32 আগে থেকেই সংজ্ঞায়িত
/* stdio.h-এর শত শত লাইন … */

int main(void) {
    printf("Screen clear command: %s\n", "cls");
    return 0;
}
অজানা সিস্টেম#error — build থামল
$ gcc app.c
app.c:8:6: error: #error "Unknown platform: please define CLEAR_CMD"
    8 |     #error "Unknown platform: please define CLEAR_CMD"
      |      ^~~~~
  1. একটিই source ফাইল। এতে দুই রকম শর্ত আছে — কোন অপারেটিং সিস্টেম (_WIN32, __linux__), আর debug build কি না (DEBUG). __DATE__ একটি তৈরি-করা macro, যা compile-এর তারিখ বসায় — অংশ ৭-এ বিস্তারিত।
  2. Linux-এ সাধারণ build. __linux__ সংজ্ঞায়িত, তাই CLEAR_CMD হলো "clear". DEBUG নেই, তাই #ifdef DEBUG থেকে #endif পর্যন্ত সব মুছে গেল। ব্যবহারকারীর হাতে যাওয়া প্রোগ্রামে debug কোডের অস্তিত্বই নেই।
  3. একই কম্পিউটার, শুধু -DDEBUG যোগ। একটি flag-এর জন্য compiler এবার একটি বাড়তি printf পেল। Source-এ এক অক্ষরও বদলাতে হলো না। __DATE__-এর জায়গায় বসেছে compile-এর দিনের তারিখ।
  4. Windows-এ build. একই ফাইল, কিন্তু এবার _WIN32 সংজ্ঞায়িত — তাই "cls". এভাবেই একটি কোডবেস থেকে Windows, Linux, Mac আর মোবাইলের জন্য আলাদা প্রোগ্রাম তৈরি হয়।
  5. #error — নিরাপদে থেমে যাওয়া। কোনো শর্তই না মিললে preprocessor নিজেই build থামিয়ে পরিষ্কার বার্তা দেয়। চুপচাপ ভুল প্রোগ্রাম তৈরির চেয়ে এটি অনেক ভালো — সমস্যাটা ধরা পড়ে প্রোগ্রামারের টেবিলে, ব্যবহারকারীর হাতে নয়।

কোড সাময়িকভাবে “বন্ধ” রাখতে অনেকে /* … */ দিয়ে comment করেন। কিন্তু ভেতরে আগে থেকে /* */ থাকলে প্রথম */-এই comment শেষ হয়ে যায়। নিরাপদ উপায় হলো #if 0 … #endif — ভেতরে যা-ই থাকুক, পুরোটা বাদ পড়ে।

Conditional compilation-এর একটি লুকানো দাম আছে। আপনি প্রতিদিন যে build চালান, শুধু সেই শাখার কোডই compiler যাচাই করে। #ifdef _WIN32-এর ভেতরের কোডে ভুল থাকলে Linux-এ কাজ করা প্রোগ্রামার কখনো জানবেন না — যতদিন না কেউ Windows-এ build করেন। তাই পেশাদার প্রজেক্টে CI প্রতিটি platform আর প্রতিটি build ধরনে আলাদাভাবে compile করে দেখে। আর #ifdef যত বেশি, পরীক্ষা করার মতো সংস্করণও তত বেশি — তাই এটি যত কম, তত ভালো।

৭ · প্রোগ্রাম নিজের ঠিকানা বলে — __FILE__, __LINE__, __func__ ও assert

কিছু macro আপনাকে লিখতে হয় না, compiler নিজেই প্রস্তুত রাখে। এদের বলে predefined macro. সবচেয়ে কাজের কয়েকটি:

নামযা দেয়উদাহরণ
__FILE__বর্তমান source ফাইলের নাম (string)"result.c"
__LINE__বর্তমান লাইন নম্বর (পূর্ণসংখ্যা)42
__DATE__, __TIME__Compile-এর তারিখ ও সময় (string)"Oct 16 2026", "10:30:05"
__STDC_VERSION__কোন C স্ট্যান্ডার্ডে compile হচ্ছেC99-এ 199901L, C11-এ 201112L, C17-এ 201710L
__func__বর্তমান ফাংশনের নাম (string) — macro নয়, নিচে দেখুন"average"

__func__ নিয়ে একটি সূক্ষ্ম কিন্তু সত্যিকারের পার্থক্য আছে। এটি C99-এ যোগ হওয়া একটি predefined identifier — macro নয়। কারণটা যৌক্তিক: preprocessor ফাংশন কী তা-ই জানে না, তাই সে ফাংশনের নাম বসাতে পারে না। কাজটি করে compiler নিজে, যেন প্রতিটি ফাংশনের শুরুতে লুকিয়ে static const char __func__[] = "average"; লেখা আছে। ব্যবহারে পার্থক্য সামান্য, কিন্তু একটি জিনিস মনে রাখুন: #ifdef __func__ লিখলে সেটি সবসময় মিথ্যা হবে।

__LINE__ আর __FILE__-এর আসল জাদু দেখা যায় macro-র ভেতরে ব্যবহার করলে। Macro খোলা হয় যেখানে ডাকা হয়েছে সেখানে, তাই __LINE__ বসায় ডাকার জায়গার লাইন নম্বর — একটি ফাংশন কখনো এটি পারত না। নিচের প্রোগ্রামে একটি ছোট LOG macro আছে, যা শুধু debug build-এ কাজ করে:

#include <stdio.h>

#define DEBUG   /* delete this line for a release build */

#ifdef DEBUG
#define LOG(msg) printf("[%s:%d %s] %s\n", __FILE__, __LINE__, __func__, msg)
#else
#define LOG(msg) do { } while (0)
#endif
#define SHOW(expr) printf(#expr " = %d\n", (expr))

static int average(int total, int count) {
    LOG("average() called");
    if (count == 0) { LOG("count is zero, returning 0"); return 0; }
    return total / count;
}

int main(void) {
    SHOW(average(250, 5));
    SHOW(average(10, 0));
    return 0;
}

আউটপুটে প্রতিটি log-এর সামনে ফাইলের নাম, লাইন নম্বর আর ফাংশনের নাম আসবে, যেমন [prog.c:13 average] average() called. অনলাইন compiler ফাইলটির নাম দেয় prog.c; নিজের কম্পিউটারে দেখবেন আপনার ফাইলের নাম। লক্ষ করুন, লাইন নম্বর ১৩ আর ১৪ — LOG যেখানে ডাকা হয়েছে সেখানকার, সংজ্ঞার লাইন ৬ নয়। এবার তৃতীয় লাইনটি মুছে আবার চালান: সব log উধাও, শুধু ফল থাকে। SHOW-এর #expr অংশটি argument-এর লেখাকে হুবহু string বানায় — সেটি অংশ ৮-এ। (বাস্তব প্রোগ্রামে log সাধারণত stderr-এ পাঠানো হয়; এখানে আউটপুটের ক্রম সহজ রাখতে printf ব্যবহার করেছি।)

assert — “এটা কখনো মিথ্যা হওয়ার কথা নয়”

Standard library-র <assert.h>-এ আছে assert, যা আসলে এই কৌশলেরই একটি পেশাদার রূপ। assert(count > 0); লিখলে প্রোগ্রাম চলার সময় শর্তটি যাচাই হয়। মিথ্যা হলে প্রোগ্রাম সঙ্গে সঙ্গে থামে, আর stderr-এ এমন কিছু ছাপে:

app: result.c:27: average: Assertion `count > 0' failed.
Aborted

শর্তের হুবহু লেখা, ফাইল, লাইন, ফাংশন — সবই এসেছে #, __FILE__, __LINE__ আর __func__ থেকে। আর release build-এ -DNDEBUG দিলে NDEBUG সংজ্ঞায়িত হয়, তখন প্রতিটি assert খুলে কিছুই হয় না — সম্পূর্ণ বাদ, কোনো খরচ নেই। CMake-এর মতো build system-এর Release মোড সাধারণত নিজে থেকেই -DNDEBUG যোগ করে।

ঠিক এই কারণে assert-এর ভেতরে কখনো আসল কাজ রাখবেন না। assert(fclose(fp) == 0); debug build-এ ঠিকঠাক চলবে, কিন্তু release build-এ পুরো লাইনটি মুছে যাবে — ফাইল আর বন্ধই হবে না। আর assert হলো প্রোগ্রামারের ভুল ধরার জন্য (“এই ফাংশনে কখনো NULL আসার কথা নয়”)। ব্যবহারকারীর ভুল ইনপুট বা ফাইল না পাওয়ার মতো স্বাভাবিক সমস্যা সামলাতে হয় সাধারণ if দিয়ে। এই পার্থক্য নিয়ে পুরো আলোচনা পর্ব ৩০-এ, এরর হ্যান্ডলিংয়ে।

৮ · # আর ## — লেখাকে string বানানো, শব্দ জোড়া দেওয়া

Macro-র ভেতরে দুটি বিশেষ অপারেটর আছে, যা শুধু preprocessor বোঝে। দুটোই “লেখা নিয়ে কাজ” — আর ঠিক এ ধরনের কাজ ফাংশন দিয়ে করা অসম্ভব।

# — stringize

Argument-এর লেখাটিকে হুবহু একটি string-এ বদলায়। SHOW(average(250, 5))-এ #expr হয়ে যায় "average(250, 5)". পাশাপাশি দুটি string C নিজেই জুড়ে দেয়, তাই #expr " = %d\n" একটি format string হয়ে যায়।

ব্যবহার: debug print, assert-এর বার্তা

## — token pasting

দুই পাশের দুটি শব্দকে জোড়া দিয়ে একটি নতুন নাম বানায়। #define COUNTER(name) static int count_##name = 0 লিখে COUNTER(login); দিলে তৈরি হয় static int count_login = 0;

ব্যবহার: একই ধাঁচের অনেক নাম তৈরি

একটি বাস্তব উদাহরণ: একটি দোকানের হিসাবের প্রোগ্রামে অর্ডারের অবস্থা enum-এ রাখা আছে — PENDING, PAID, DELIVERED. Log-এ সংখ্যা 1 না দেখিয়ে নাম "PAID" দেখাতে চাইলে প্রতিটির জন্য হাতে string লিখতে হয়, আর নতুন অবস্থা যোগ করলে দুই জায়গায় বদলাতে হয়। # দিয়ে একটি তালিকা থেকেই enum আর নামের string — দুটোই তৈরি করা যায়, ফলে দুটো কখনো অমিল হয় না। এই কৌশলকে বলে “X-macro”; বড় C প্রজেক্টে বহুল ব্যবহৃত।

একটি সূক্ষ্ম নিয়ম: # বা ##-এর পাশে বসা argument আগে macro হিসেবে খোলা হয় না। তাই #define STR(x) #x দিয়ে STR(__LINE__) লিখলে পাবেন "__LINE__", লাইন নম্বর নয়। সমাধান হলো দুই ধাপের macro (#define XSTR(x) STR(x)) — প্রথম ধাপে argument খুলে যায়, দ্বিতীয় ধাপে string হয়। এখনই মুখস্থ করার দরকার নেই; শুধু জেনে রাখুন, এমন অদ্ভুত আচরণ দেখলে কারণ এটাই।

৯ · AI-এর যুগে — macro কোডে AI যেসব ভুল করে

Macro এমন একটি জায়গা যেখানে AI-এর লেখা কোড “দেখতে ঠিক” লাগে, কিন্তু বিপদ লুকিয়ে থাকে লেখা খোলার পরে। কারণ AI কোডটি একটি লেখা হিসেবে দেখে — আপনার build কমান্ডে কোন -D flag আছে, কোন header কোন পথে দুবার ঢুকছে, তা সে জানে না। পুরোনো টিউটোরিয়ালে ভরা ইন্টারনেট থেকে শেখায় AI প্রায়ই সেই যুগের অভ্যাসও নিয়ে আসে।

AI-এর ভুলকী হয়কীভাবে ধরবেন
বন্ধনী ছাড়া macro: #define AREA(r) 3.14 * r * rAREA(x + 1) ভুল ফল দেয়, কোনো warning ছাড়াইপ্রতিটি parameter আর পুরো সংজ্ঞা বন্ধনীতে আছে কি না দেখুন
MAX(a[i++], b) বা SQUARE(rand())-এর মতো ডাকDouble evaluation — মান দুবার বাড়ে বা দুবার আলাদা random সংখ্যাMacro-র argument-এ ++, --, ফাংশন-ডাক খুঁজুন
২০-৩০ লাইনের বিশাল macro “ফাংশন”Error message অবোধ্য, debugger অচল, পড়া কঠিনAI-কে বলুন static inline ফাংশনে বদলাতে
Header-এ include guard নেইপ্রজেক্ট বড় হলে হঠাৎ redefinition errorপ্রতিটি .h-এর প্রথম ও শেষ লাইন দেখুন
কয়েক লাইনের macro { } দিয়ে, do-while(0) ছাড়াif/else-এর ভেতরে compile error বা নিঃশব্দ বাগ\ দিয়ে লেখা প্রতিটি macro-র মোড়ক দেখুন
ধ্রুবক #define-এ, শেষে ; সহঅদ্ভুত জায়গায় errorMacro-র শেষে ; থাকলে মুছে দিন, সম্ভব হলে enum ব্যবহার করুন

AI-এর লেখা কোডে macro দেখলে নিচের prompt ব্যবহার করুন। শেষ অনুরোধটি গুরুত্বপূর্ণ — AI-কে দিয়ে expansion হাতে লিখিয়ে নিলে সে নিজেই প্রায়ই নিজের ভুল ধরে ফেলে, আর আপনি সেটি gcc -E দিয়ে মিলিয়ে নিতে পারেন।

নিচের C কোডে যত #define macro আছে, প্রতিটি পর্যালোচনা করো: [এখানে কোড পেস্ট করুন] ১. প্রতিটি function-like macro-র parameter আর পুরো সংজ্ঞা বন্ধনীতে আছে কি না দেখাও। ২. কোনো macro-তে argument একাধিকবার ব্যবহার হলে (double evaluation) সেটি চিহ্নিত করো, আর কোড কোথাও সেখানে i++ বা ফাংশন-ডাক দিচ্ছে কি না বলো। ৩. যেসব macro static inline ফাংশন বা enum দিয়ে বদলানো যায়, সেগুলো বদলে দাও এবং কেন বদলালে বলো। ৪. প্রতিটি header-এ include guard আছে কি না দেখো। ৫. অন্তত দুটি macro ডাকার জন্য preprocessor-এর পরে কোডটি ঠিক কেমন হবে (gcc -E-এর ফল), হুবহু লিখে দেখাও।

Macro-ওয়ালা কোড হাতে পেলে — যাচাইয়ের চেকলিস্ট

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

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

প্র ০১C কেন compiler-এর ভেতরেই এসব না রেখে আলাদা একটি “লেখা-বদলের” ধাপ রাখল?

উত্তরটা লুকিয়ে আছে ১৯৭০-এর দশকের বাস্তবতায়। C তৈরি হচ্ছিল খুব সীমিত মেমরির কম্পিউটারে, যেখানে একটি বড় compiler পুরোটা একসাথে মেমরিতে রাখাও কঠিন ছিল। তাই কাজ ছোট ছোট আলাদা প্রোগ্রামে ভাগ করা ছিল স্বাভাবিক — Unix-এর দর্শনই ছিল “একটি প্রোগ্রাম, একটি কাজ, ভালোভাবে”.

  • আলাদা ধাপ মানে সরল compiler: ফাইল জোড়া দেওয়া আর নাম বদলানোর কাজ একটি সরল লেখা-প্রক্রিয়াকারীকে দিলে compiler-কে শুধু খাঁটি C নিয়েই ভাবতে হয়। Dennis Ritchie নিজেও লিখেছেন, preprocessor শুরুতে ছিল ভাষার বাইরের একটি ঐচ্ছিক সংযোজন — #include আর সাধারণ #define নিয়ে, অনেকটা অন্য ভাষার file-inclusion সুবিধার আদলে।
  • ভাষা না বদলে ক্ষমতা বাড়ানো: Platform-ভেদে আলাদা কোড, ধ্রুবকের নাম, debug সুবিধা — এগুলো ভাষার ব্যাকরণে না ঢুকিয়েই পাওয়া গেল। পরে নতুন চাহিদা এলে preprocessor-এ নতুন directive যোগ করলেই চলত।
  • দাম: যেহেতু preprocessor C বোঝে না, তাই টাইপ, scope, precedence — কোনো নিয়মই তাকে বাঁধে না। আজকের পর্বের প্রতিটি ফাঁদ এই নকশারই প্রত্যক্ষ ফল।

পরে const, enum, inline এসে preprocessor-এর অনেক কাজ নিরাপদভাবে compiler-এর ভেতরে নিয়ে গেছে। কিন্তু পুরোনো কোটি কোটি লাইন কোড চালু রাখতে হয় বলে preprocessor সরানো আর সম্ভব নয় — একটি ভাষার নকশা-সিদ্ধান্ত প্রায় চিরকাল থেকে যায়।

মূল উপলব্ধি: সীমিত সম্পদে সরল ও আলাদা ধাপ একটি বুদ্ধিমান সিদ্ধান্ত ছিল; কিন্তু সেই সরলতার দাম দিতে হয় দশকের পর দশক। প্রকৌশলে আজকের সুবিধাজনক সমাধান কাল দায় হয়ে উঠতে পারে — তাই নকশার সময়েই ভাবতে হয় সেটি কত দিন টিকবে।

প্র ০২Java, Go-এর মতো আধুনিক ভাষা macro বাদ দিল, আর Rust রাখল ভিন্ন রূপে। কেন?

C-এর macro-র মূল সমস্যা হলো তা “অন্ধ” — কোডের গঠন না বুঝে লেখা বদলায়। পরের প্রজন্মের ভাষাগুলো এর দুই রকম উত্তর দিয়েছে।

  • পুরোপুরি বাদ (Java, Go): এদের নকশাকাররা মনে করেছেন, macro কোডকে এমন রূপ দেয় যা পড়ে বোঝা যায় না — SQUARE(x) দেখে বোঝার উপায় নেই এটি ফাংশন নাকি লেখা-বদল। তাই ধ্রুবকের জন্য টাইপসহ const, ছোট ফাংশনের জন্য compiler-এর নিজস্ব inlining, আর platform-ভেদের জন্য আলাদা ফাইল (Go-তে _windows.go, _linux.go নামের ফাইল) দেওয়া হয়েছে।
  • নিরাপদ macro (Rust, Lisp পরিবার): এসব ভাষার macro লেখা নয়, কোডের গঠন নিয়ে কাজ করে। Rust-এর macro_rules! argument-কে একটি সম্পূর্ণ expression হিসেবে দেখে, তাই a + 1-এর চারপাশে আলাদা বন্ধনী লাগে না। আর macro-র ভেতরে বানানো ভ্যারিয়েবল বাইরের একই নামের ভ্যারিয়েবলের সাথে গুলিয়ে যায় না — এই গুণকে বলে “hygiene”. Scheme ভাষায় এই ধারণাটি অনেক আগেই প্রতিষ্ঠিত হয়েছিল।
  • অন্য পথ (C++, Zig): C++ preprocessor রেখেছে C-এর সাথে মিল রাখতে, কিন্তু template, constexpr আর inline দিয়ে বেশিরভাগ কাজের নিরাপদ বিকল্প দিয়েছে। Zig compile-এর সময়েই সাধারণ কোড চালানোর ব্যবস্থা (comptime) রেখে macro-র প্রয়োজনই কমিয়েছে।

লক্ষ করুন, কোনো ভাষাই macro-র চাহিদা অস্বীকার করেনি — platform বাছাই, কোড তৈরি, compile-এর সময় হিসাব লাগবেই। পার্থক্য শুধু সেই ক্ষমতা কতটা নিরাপদ মোড়কে দেওয়া হয়েছে।

মূল উপলব্ধি: একটি শক্তিশালী কিন্তু বিপজ্জনক সুবিধার মুখোমুখি হলে ভাষার নকশাকাররা হয় সেটি সরিয়ে বিকল্প দেন, নয়তো এমন মোড়ক দেন যাতে ভুল করা কঠিন হয়। AI-এর যুগে দ্বিতীয় পথটি আরও মূল্যবান — যে কোড ভুলভাবে লেখাই কঠিন, তা AI-ও ভুলভাবে কম লেখে।

প্র ০৩বড় C/C++ প্রজেক্ট build হতে এত সময় লাগে কেন, আর header-এর সাথে এর সম্পর্ক কী?

মনে করুন, একটি প্রজেক্টে ১,০০০টি .c ফাইল, আর প্রায় সবাই একই বড় header include করে — যার ভেতরে আরও header, সব মিলিয়ে হাজার হাজার লাইন। প্রতিটি .c একটি আলাদা translation unit, আর include guard শুধু একটি unit-এর ভেতরে পুনরাবৃত্তি আটকায়। তাই সেই একই হাজার হাজার লাইন ১,০০০ বার পড়া, preprocess আর parse হয়। অনেক বড় প্রজেক্টে compiler-এর সময়ের বড় অংশ যায় আপনার কোডে নয়, header-এ।

  • ঢেউয়ের মতো rebuild: একটি জনপ্রিয় header-এ একটি comment বদলালেও, যে সব ফাইল সেটি সরাসরি বা ঘুরিয়ে include করে, সবাইকে নতুন করে compile হতে হয়। Build system জানে না পরিবর্তনটি অর্থহীন ছিল।
  • প্রচলিত প্রতিকার: শুধু যা লাগে তাই include করা (“include what you use”); header-এ পুরো struct-এর বদলে শুধু নাম ঘোষণা (struct Student;) দেওয়া, যখন শুধু pointer লাগে; আর precompiled header — যেখানে বড় header একবার প্রক্রিয়া করে ফলটি সংরক্ষণ করা হয়।
  • মূল সমাধানের চেষ্টা — C++20 modules: Module একবার compile হয়ে একটি প্রস্তুত, বাইনারি রূপে থাকে। যে ফাইল সেটি import করে, সে লেখা কপি না করে সেই প্রস্তুত তথ্য পড়ে। আর macro module-এর বাইরে ছড়িয়ে পড়ে না, ফলে এক header-এর #define অন্যের কোড নষ্ট করতে পারে না। C ভাষায় এখনো modules নেই, তবে C++-এর অভিজ্ঞতা থেকে বোঝা যায় সমস্যাটি কতটা গুরুতর।

মূল উপলব্ধি: “কপি করে বসাও” — একটি সরল নকশা ছোট প্রজেক্টে নিখুঁত, কিন্তু আকার বাড়লে খরচ গুণিতক হারে বাড়ে। সিস্টেম নকশায় সবসময় জিজ্ঞেস করুন: এটি ১০ গুণ, ১,০০০ গুণ বড় হলে কী হবে?

প্র ০৪এত সমালোচনার পরও কোথায় macro-ই সবচেয়ে সঠিক হাতিয়ার?

Macro-র সমালোচনা মানে এই নয় যে এটি অচল। কিছু কাজ আছে যা C-তে শুধু preprocessor পারে, কারণ সেগুলো compile-এর আগের বা কোডের লেখা নিয়ে কাজ।

  • Platform ও build-ভেদ: Windows-এ এক API, Linux-এ আরেক — #ifdef _WIN32 ছাড়া উপায় নেই। Debug build-এ বাড়তি যাচাই, release-এ শূন্য খরচ — এটিও শুধু conditional compilation পারে।
  • ডাকার জায়গার তথ্য: __FILE__, __LINE__ সেই জায়গার মান দেয় যেখানে লেখা আছে। একটি ফাংশন নিজের লাইন জানে, ডাকার লাইন নয়। তাই assert আর logging সিস্টেম প্রায় সবসময় macro-র ওপর দাঁড়ানো।
  • কোডের লেখা থেকে string: #expr দিয়ে শর্তের হুবহু লেখা দেখানো — ফাংশন সেটি কখনো জানতে পারে না।
  • পুনরাবৃত্তি দূর করা: X-macro দিয়ে একটি তালিকা থেকে enum, নামের string আর টেবিল — একসাথে তৈরি, যাতে কোনোটি অমিল না হয়।
  • ছোট, প্রমাণিত সহায়ক: যেমন #define ARRAY_SIZE(a) (sizeof(a) / sizeof((a)[0])) — যেকোনো টাইপের array-র উপাদান-সংখ্যা। (সাবধান: pointer দিলে ভুল ফল দেয়, পর্ব ১৮-এর array decay মনে করুন।)

লক্ষ করুন, তালিকার প্রায় সবকিছুই হয় compile-এর আগের সিদ্ধান্ত, নয়তো কোডের লেখা নিয়ে কাজ। সাধারণ হিসাব-নিকাশ এখানে নেই — সেটি ফাংশনের কাজ।

মূল উপলব্ধি: ভালো প্রকৌশলী কোনো টুলকে “ভালো” বা “খারাপ” বলেন না; তিনি জানেন কোন টুল কোন সমস্যার জন্য একমাত্র সমাধান, আর কোথায় তা অপ্রয়োজনীয় ঝুঁকি। Macro ব্যবহারের আগে নিজেকে জিজ্ঞেস করুন: “এটা কি ফাংশন, enum বা const দিয়ে করা যায়?” উত্তর “হ্যাঁ” হলে সেটাই করুন।

প্র ০৫Macro থাকলে debugging আর code review — মানুষ বা AI, দুজনের জন্যই কেন কঠিন হয়?

মূল কারণ: আপনি যে কোড পড়ছেন আর compiler যে কোড পাচ্ছে, দুটি আলাদা। এই ফাঁকের মধ্যেই সব সমস্যা।

  • Debugger-এর অন্ধত্ব: Debugger compile হওয়া কোড দেখে। MAX_STUDENTS নামটি সেখানে নেই, শুধু 60. একটি বড় macro এক লাইনে খোলে, তাই সেটির ভেতরে ধাপে ধাপে চলা যায় না। (GCC-তে -g3 দিলে macro-র সংজ্ঞা debug তথ্যে রাখা হয়, আর GDB-তে macro expand দিয়ে দেখা যায় — কিন্তু খুব কম মানুষ এটি জানেন।)
  • বিভ্রান্তিকর error: Error আসে খোলা লেখার ভেতর থেকে। আধুনিক GCC ও Clang “in expansion of macro …” লিখে সাহায্য করে, তবু লম্বা macro-তে বার্তাটি প্রায়ই অবোধ্য।
  • অদৃশ্য কোড: #ifdef-এর যে শাখা আপনার build-এ বাদ পড়ে, সেটি compiler কখনো যাচাই করে না। Reviewer যে কোড পড়ছেন, তার কতটুকু আসলে চলবে — তা নির্ভর করে build কমান্ডের -D flag-এর ওপর, যা কোডে লেখা নেই।
  • AI-এর বিশেষ সমস্যা: AI সাধারণত একটি বা কয়েকটি ফাইল দেখে। Macro-র সংজ্ঞা অন্য header-এ থাকলে, বা build system কোন flag দেয় তা না জানলে, AI ধরে নেয় — আর সেই ধারণা ভুল হতে পারে। MAX(a, b) দেখে সে ফাংশন ভেবে MAX(i++, j) লিখে ফেলতে পারে।

প্রতিকার: সন্দেহ হলে gcc -E দিয়ে আসল লেখা দেখুন, আর AI-কে সেই খোলা কোডটিই দিন; macro যত সম্ভব কম ও ছোট রাখুন; CI-তে সব build ধরনে compile করুন।

মূল উপলব্ধি: যে কোড পড়া আর যে কোড চলে — দুটো যত কাছাকাছি, যাচাই তত সহজ। Macro এই দূরত্ব বাড়ায়। AI-এর যুগে যখন বেশিরভাগ কোড অন্য কেউ (বা কিছু) লিখছে, তখন “যা দেখছি তাই চলছে” — এই নিশ্চয়তা আগের চেয়ে অনেক বেশি মূল্যবান।

অনুশীলন

বেশিরভাগ কাজ হাতে-কলমে হিসাব করা আর preprocessor-এর ফল চোখে দেখা।

  1. Preprocessor-এর কাজ দেখুন: উপরের macro_trap.c কোডটি নিজের কম্পিউটারে রেখে চালান gcc -E macro_trap.c | tail -n 15. main-এর ভেতরে SQUARE_BAD, SQUARE, MAX নামগুলো খুঁজে পান কি?

    পাবেন না। কোনো macro-র নামই থাকে না — তাদের জায়গায় আছে খোলা লেখা: a + 1 * a + 1, ((a + 1) * (a + 1)) আর ((i++) > (0) ? (i++) : (0)). #define লাইনগুলোও উধাও। এটাই প্রমাণ যে compiler macro সম্পর্কে কিছুই জানে না। আরও লক্ষ করুন, max_int ফাংশনটি নামসহ রয়ে গেছে — সেটি আসল C কোড। Windows-এ tail না থাকলে আউটপুট একটি ফাইলে রেখে (gcc -E macro_trap.c -o out.i) ফাইলের শেষ অংশ দেখুন।

  2. হাতে expansion করুন: #define HALF(x) x / 2 আর #define DOUBLE(x) (x) + (x). int n = 10; হলে (ক) HALF(n + 4) (খ) 3 * DOUBLE(n) (গ) HALF(DOUBLE(n)) — প্রতিটির মান কত? আগে খাতায় হিসাব করুন।

    (ক) n + 4 / 2 = ১০ + ২ = ১২ (আশা ছিল ৭)।

    (খ) 3 * (n) + (n) = ৩০ + ১০ = ৪০ (আশা ছিল ৬০) — ভেতরের বন্ধনী আছে, কিন্তু বাইরের বন্ধনী নেই।

    (গ) ভেতর থেকে খোলে: (n) + (n) / 2 = ১০ + ৫ = ১৫ (আশা ছিল ১০)।

    তিনটিতেই কোনো warning আসবে না। সঠিক রূপ: #define HALF(x) ((x) / 2) আর #define DOUBLE(x) ((x) + (x)) — অথবা আরও ভালো, দুটি static inline ফাংশন।

  3. Header গোয়েন্দা: AI আপনাকে config.h দিল, যাতে আছে #define BUFFER 256;, struct Config { int port; }; আর int request_count = 0;. Include guard নেই। এতে কয়টি সমস্যা দেখছেন, আর কোনটি guard দিলেও থেকে যাবে?

    তিনটি সমস্যা। (১) BUFFER-এর শেষে ; — char buf[BUFFER]; হয়ে যাবে char buf[256;];, অদ্ভুত error. (২) Guard না থাকায় দুই পথে include হলে struct Config-এর redefinition error. (৩) Header-এ ভ্যারিয়েবলের সংজ্ঞা int request_count = 0; — দুটি .c ফাইল এটি include করলে linker multiple definition বলবে।

    Guard দিলে দ্বিতীয়টি ঠিক হয়, কিন্তু তৃতীয়টি থেকে যায়, কারণ guard শুধু একটি translation unit-এর ভেতরে কাজ করে। সমাধান: header-এ extern int request_count; (শুধু ঘোষণা), আর একটি .c ফাইলে আসল সংজ্ঞা — পর্ব ২৯-এ বিস্তারিত।

  4. Macro থেকে ফাংশনে (কোড লিখুন): নিচের macro-টিকে একটি static inline ফাংশনে বদলান, তারপর CLAMP(i++, 0, 100) আর আপনার ফাংশনের ফল তুলনা করুন: #define CLAMP(v, lo, hi) ((v) < (lo) ? (lo) : (v) > (hi) ? (hi) : (v))

    একটি সম্ভাব্য সমাধান:

    static inline int clamp_int(int v, int lo, int hi) {
        if (v < lo) return lo;
        if (v > hi) return hi;
        return v;
    }

    i = 50 হলে macro-তে v তিনবার পর্যন্ত লেখা আছে: প্রথম তুলনায় i ৫১, দ্বিতীয় তুলনায় ৫২, আর ফল নিতে গিয়ে আরেকবার — ফল ৫২, i হয়ে যায় ৫৩. ফাংশনে ফল ৫০, i ৫১. Macro-তে একটি argument যত বেশিবার ব্যবহার হয়, double (এখানে triple!) evaluation-এর ঝুঁকি তত বেশি।

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

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

ক্রমএকই header দুবার include হলে guard-এর কাজ ঠিক ক্রমে সাজান (▲▼ চেপে সরান)

  1. প্রথম #include "date.h" — preprocessor #ifndef DATE_H যাচাই করে
  2. DATE_H সংজ্ঞায়িত নেই, তাই ভেতরে ঢোকে
  3. #define DATE_H — নামটি এখন সংজ্ঞায়িত
  4. struct Date translation unit-এ বসে
  5. দ্বিতীয় #include "date.h" — #ifndef মিথ্যা, #endif পর্যন্ত সব বাদ

Preprocessor সম্পর্কে কোন কথাটি সঠিক?

  • সে ভ্যারিয়েবলের টাইপ দেখে macro খোলে
  • সে compiler-এর আগে চলে, আর শুধু লেখা বদলায়
  • সে প্রোগ্রাম চলার সময় macro খোলে
  • ফাংশনের ভেতরে লেখা #define শুধু সেই ফাংশনেই কাজ করে

#define DOUBLE(x) x + x হলে 3 * DOUBLE(2)-এর মান কত?

  • ১২
  • ১০
  • ৮
  • ৬

Header-এর শুরুতে #ifndef STUDENT_H / #define STUDENT_H আর শেষে #endif লেখার উদ্দেশ্য কী?

  • একই .c ফাইলে header দুবার ঢুকে redefinition error হওয়া আটকানো
  • সব .c ফাইল মিলিয়ে linker-এর multiple definition error আটকানো
  • প্রোগ্রামকে দ্রুত চালানো
  • Header ফাইল না পেলে error লুকানো

#define MAX(a, b) ((a) > (b) ? (a) : (b)) — বন্ধনী সবই আছে। তবু MAX(n++, 10) লেখা বিপজ্জনক কেন?

  • Precedence-এর কারণে তুলনা ভুল হয়
  • n++ int নয়, তাই টাইপ মেলে না
  • Compiler এটি compile করতে দেয় না
  • n++ দুবার চলতে পারে, ফলে n ও ফল দুটোই ভুল হয়

কোডে #ifdef DEBUG … #endif-এর ভেতরে কয়েকটি printf আছে। gcc app.c (কোনো -D ছাড়া) দিয়ে build করলে কী হয়?

  • printf-গুলো প্রোগ্রামে থাকে, কিন্তু চলার সময় এড়িয়ে যায়
  • ওই অংশটি compile-এর আগেই মুছে যায়, প্রোগ্রামে থাকেই না
  • DEBUG সংজ্ঞায়িত নেই বলে compile error হয়
  • printf-গুলো সবসময় চলে

Release build-এ -DNDEBUG দেওয়া হলো। assert(save_file(db) == 0); লাইনটির কী হবে?

  • ফাইল সংরক্ষণ ব্যর্থ হলে প্রোগ্রাম থামবে
  • শুধু যাচাই বাদ পড়বে, save_file তবু ডাকা হবে
  • পুরো লাইনটি বাদ পড়বে — save_file ডাকাই হবে না
  • Compile error হবে

শব্দকোষ

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

মনে রাখুন

  1. Preprocessor compiler-এর আগে চলে আর শুধু লেখা বদলায় — টাইপ, scope, precedence কিছুই বোঝে না।
  2. প্রতিটি header-এ include guard (বা #pragma once); guard শুধু একটি translation unit-এর ভেতরে কাজ করে।
  3. Function-like macro-তে দুই স্তরে বন্ধনী, argument-এ কখনো side effect নয়, কয়েক লাইনের হলে do { } while (0).
  4. ধ্রুবকে enum/const, ছোট হিসাবে static inline; macro শুধু যেখানে অন্য কিছু পারে না — platform বাছাই, debug log, __FILE__/__LINE__.
  5. assert প্রোগ্রামারের ভুল ধরে, NDEBUG-এ উধাও হয়; সন্দেহ হলে gcc -E দিয়ে আসল লেখা দেখুন।

পরের পর্বে: আজ দেখলেন macro দিয়ে “যেকোনো কাজ” করাতে গেলে কত ঝুঁকি। পরের পর্বে দেখব C-এর নিরাপদ উপায় — ফাংশনকেও একটি মানের মতো ভ্যারিয়েবলে রাখা আর অন্য ফাংশনে পাঠানো। qsort কীভাবে যেকোনো ধরনের ডেটা সাজায়, আর একটি বাটন চাপলে কোন কাজ হবে তা কীভাবে পরে ঠিক করা যায় — function pointer ও callback.

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

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