Lesson ১৫পড়তে ৭ মিনিট লাগবে

ফাংশন পয়েন্টার ও কলব্যাকসমূহ (Function Pointers & Callbacks)

ধরুন আপনার কাছে একটি রিমোট কন্ট্রোল বা remote control আছে যার প্রতিটি বোতামকে মূলত যেকোনো কিছু করার জন্য রিপ্রোগ্রাম (reprogrammed) বা নতুনভাবে সেট করা যায় — এটিই হলো একটি ফাংশন পয়েন্টার (function pointer)

প্রোগ্রামেবল রিমোট কন্ট্রোল (The Programmable Remote Control)

বেশিরভাগ সি (C) কোডেই, আপনি সাধারণত যেকোনো ফাংশনকে সরাসরি তাদের নাম ধরেই ডাকেন বা কল (call) করেন: যেমন printf(), strlen(), main()। কিন্তু যদি এমন হতো যে আপনি চাইলে যেকোনো একটি ফাংশনকে একটি ভ্যারিয়েবলের (variable) ভেতর সেভ করে (store) রাখতে পারতেন, সেটিকে অন্য কোনো ফাংশনের (function) কাছে পাস (pass) করে দিতে পারতেন, বা রানটাইমের (runtime) সময় ঠিক কোন ফাংশনটিকে ডাকতে বা কল (call) করতে হবে তা নিজে থেকেই বেছে নিতে পারতেন? তবে তখন কেমন হতো?

আর মূলত ঠিক এই কাজটিই ফাংশন পয়েন্টারগুলো (function pointers) করে থাকে। ঠিক যেমনভাবে একটি সাধারণ পয়েন্টার (regular pointer) কোনো ভ্যারিয়েবলের (variable) ঠিকানা বা অ্যাড্রেসকে (address) ধারণ করে রাখে, তেমনি একটি ফাংশন পয়েন্টারও মূলত একটি ফাংশনের ঠিকানা (address of a function) ধারণ করে রাখে। এটি অনেকটা এমন একটি রিমোট কন্ট্রোলের (remote control) মতো যার প্রতিটি বোতাম আলাদা আলাদা কাজের (action) ঠিকানা (address) সেভ বা ধারণ করে রাখে — এবং আপনি চাইলে যেকোনো সময় এই বোতামগুলোকে রিপ্রোগ্রাম (reprogram) বা নতুনভাবে সেট করে নিতে পারেন।

এর সিনট্যাক্স (The Syntax) (হ্যাঁ, এটি দেখতে বেশ কুৎসিত বা Ugly)

একটি ফাংশন পয়েন্টারের (function pointer) ডিক্লেয়ারেশন (declaration) বা এর দেখতে মূলত কিছুটা এমন হয়:

return_type (*pointer_name)(param_types);

এখানে এই *pointer_name-এর চারপাশের ব্র্যাকেটগুলো বা প্যারেনথেসিসগুলো (parentheses) অনেক বেশি গুরুত্বপূর্ণ (critical)। এগুলো ছাড়া, আপনি হয়তো এমন একটি ফাংশনকে ডিক্লেয়ার করে ফেলবেন যেটি কিনা মূলত একটি পয়েন্টার (pointer) রিটার্ন (returns) করে — আর সেটি কিন্তু সম্পূর্ণ ভিন্ন বা আলাদা একটি জিনিস হয়ে দাঁড়াবে।

বেসিক ফাংশন পয়েন্টার (Basic Function Pointer)

#include <stdio.h>
int add(int a, int b) { return a + b; }
int subtract(int a, int b) { return a - b; }
int multiply(int a, int b) { return a * b; }
int main() {
// একটি ফাংশন পয়েন্টার (function pointer) ডিক্লেয়ার (Declare) করুন
int (*operation)(int, int);
// এটিকে বিভিন্ন বা ডিফারেন্ট (different) ফাংশনগুলোর (functions) দিকে পয়েন্ট (Point) করুন
operation = add;
printf("add(3, 4) = %d\n", operation(3, 4));
operation = subtract;
printf("subtract(3, 4) = %d\n", operation(3, 4));
operation = multiply;
printf("multiply(3, 4) = %d\n", operation(3, 4));
return 0;
}
Output
add(3, 4)      = 7
subtract(3, 4) = -1
multiply(3, 4) = 12

উদ্ধারকারী বা সেভিয়ার হিসেবে typedef (typedef to the Rescue)

ফাংশন পয়েন্টারের (function pointers) এই র সিনট্যাক্সটি (raw syntax) পড়া যে কতটা কঠিন তা আমরা সবাই জানি। এই typedef মূলত আপনাকে এই ফাংশন পয়েন্টার টাইপগুলোর (function pointer type) জন্য একটি সুন্দর, পরিষ্কার এবং রিডেবল নাম (readable name) তৈরি করতে সাহায্য করে। একবার আপনি এর টাইপটিকে (type) ডিফাইন বা সংজ্ঞায়িত (define) করে নিলে, আপনি এর সাহায্যে যেকোনো ভ্যারিয়েবল (variables), ফাংশন প্যারামিটার (function parameters) এবং অ্যারেগুলোকে (arrays) ঠিক এর অন্যান্য সাধারণ টাইপের (other type) মতোই করে ডিক্লেয়ার বা ঘোষণা করতে পারবেন।

ফাংশন পয়েন্টারের জন্য typedef (typedef for Function Pointers)

#include <stdio.h>
// "এমন একটি ফাংশন যেটি ইনপুট হিসেবে দুটি int নেয় এবং আউটপুট হিসেবে একটি int রিটার্ন করে" এর জন্য একটি নতুন টাইপ (type) ডিফাইন (Define) করুন
typedef int (*MathOp)(int, int);
int add(int a, int b) { return a + b; }
int subtract(int a, int b) { return a - b; }
// এর ফলে এখন ফাংশন পয়েন্টারগুলোও (function pointers) প্যারামিটার হিসেবে (as parameters) অনেক বেশি পরিষ্কার বা ক্লিন (clean) হয়ে গেছে!
void compute(MathOp op, int x, int y, const char *label) {
printf("%s(%d, %d) = %d\n", label, x, y, op(x, y));
}
int main() {
compute(add, 10, 3, "add");
compute(subtract, 10, 3, "subtract");
return 0;
}
Output
add(10, 3) = 13
subtract(10, 3) = 7
Note: ফাংশন পয়েন্টারের (function pointers) সিনট্যাক্স (syntax) যে কতটা কঠিন এবং দেখতে কুৎসিত (ugly) হয় তা আমরা সবাই জানি। তবে typedef মূলত এর এই পুরো ব্যাপারটিকে অনেকটাই রিডেবল (readable) বা পাঠঅযোগ্য করে তোলে। তাই ফাংশন পয়েন্টার টাইপগুলোর (function pointer types) জন্য সবসময় এই typedef ব্যবহার করার চেষ্টা করবেন — এতে করে আপনার ভবিষ্যৎ সত্তা (future self) (এবং আপনার টিমের অন্যান্য সবাই) আপনার এই কাজের জন্য বিশেষভাবে ধন্যবাদ দেবে।

কলব্যাক — আর্গুমেন্ট হিসেবে ফাংশন পাস করা (Callbacks — Passing Functions as Arguments)

কলব্যাক বা callback হলো এমন একটি ফাংশন (function) যাকে আপনি অন্য আরেকটি ফাংশনে এই বলে পাস (pass) করে দেন যে: "যখন তোমার প্রয়োজন পড়বে তখন তুমি এটিকে ডেকে নিও (Call this when you need to)।" এটি মূলত সি (C) প্রোগ্রামিংয়ের সবচেয়ে শক্তিশালী (powerful) নিদর্শন বা প্যাটার্নগুলোর (patterns) অন্যতম একটি অংশ। স্টান্ডার্ড লাইব্রেরিও (standard library) সব জায়গায় এটির ব্যবহার করে থাকে — যেমন qsort, bsearch, সিগন্যাল হ্যান্ডলার (signal handlers), এবং অন্যান্য অনেক কিছুতেই।

ব্যাপারটিকে কোনো কন্ট্রাক্টর (contractor) বা ঠিকাদারকে ভাড়া করার সময় তাকে একটি ফোন নম্বর (phone number) ধরিয়ে দিয়ে বলার মতো কল্পনা করে নিতে পারেন যে: "তোমার কাজ শেষ হয়ে গেলে এই নম্বরটিতে কল দিও।" এখানকার ওই কন্ট্রাক্টর বা ঠিকাদার কিন্তু কখনোই জানবে না যে সে আসলে কাকে কল দিচ্ছে — সে শুধু আপনার দেওয়া ওই নম্বরটিকে ডায়াল (dial) করে যাবে।

কলব্যাক প্যাটার্ন (Callback Pattern)

#include <stdio.h>
typedef int (*Predicate)(int);
int is_even(int n) { return n % 2 == 0; }
int is_positive(int n) { return n > 0; }
int is_large(int n) { return n > 50; }
// জেনেরিক ফিল্টার (Generic filter): টেস্টে (test) ঠিক কতগুলো আইটেম পাস (pass) হয়েছে তা কাউন্ট করে বা গুনে রাখে (counts)
int count_matching(int arr[], int size, Predicate test) {
int count = 0;
for (int i = 0; i < size; i++) {
if (test(arr[i])) count++;
}
return count;
}
int main() {
int data[] = {-3, 12, 7, 84, -15, 50, 63, 2};
int n = 8;
printf("Even numbers: %d\n", count_matching(data, n, is_even));
printf("Positive numbers: %d\n", count_matching(data, n, is_positive));
printf("Large numbers: %d\n", count_matching(data, n, is_large));
return 0;
}
Output
Even numbers:     4
Positive numbers: 5
Large numbers:    2

রিয়েল-ওয়ার্ল্ড বা বাস্তব জীবনের উদাহরণ: qsort (Real-World Example: qsort)

সি (C) স্টান্ডার্ড লাইব্রেরির (standard library) এই qsort ফাংশনটি মূলত যেকোনো অ্যারেকেই (array) সাজাতে বা সর্ট (sorts) করতে পারে — তা সে যেকোনো পূর্ণসংখ্যা (integers), স্ট্রিং (strings), স্ট্রাক্ট (structs), বা অন্য যাই হোক না কেন। কিন্তু কীভাবে (How)? এটি মূলত এর নিজস্ব একটি কম্প্যারেটর কলব্যাক (comparator callback) ব্যবহার করে যা এটিকে বলে দেয় যে, কীভাবে এর যেকোনো দুটি উপাদানকে (two elements) একে অপরের সাথে তুলনা (compare) করে দেখতে হয়। আপনি শুধু এর লজিকটি (logic) বা যুক্তিটি একে দিয়ে দেবেন; আর qsort নিজে থেকেই এর বাকি সর্টিং অ্যালগরিদম টুকু (sorting algorithm) করে নেবে।

এই কম্প্যারেটরটিকে (comparator) অবশ্যই যেকোনো একটি অবস্থা বা ভ্যালু রিটার্ন (return) করতে হবে:

  • নেগেটিভ (Negative) যদি প্রথম উপাদানটি দ্বিতীয় উপাদানটির আগে আসে
  • জিরো (Zero) বা শুন্য যদি তারা একে অপরের সমান (equal) হয়
  • পজেটিভ (Positive) যদি প্রথম উপাদানটি দ্বিতীয় উপাদানটির পরে আসে

কাস্টম কম্প্যারেটর (Custom Comparator) দিয়ে qsort-এর ব্যবহার (Using qsort with a Custom Comparator)

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
// ছোট থেকে বড় বা অ্যাসেন্ডিং অর্ডারে (ascending order) সাজানোর জন্য কম্প্যারেটর (Comparator)
int compare_asc(const void *a, const void *b) {
return (*(int *)a - *(int *)b);
}
// বড় থেকে ছোট বা ডিসেন্ডিং অর্ডারে (descending order) সাজানোর জন্য কম্প্যারেটর (Comparator)
int compare_desc(const void *a, const void *b) {
return (*(int *)b - *(int *)a);
}
void print_array(int arr[], int n, const char *label) {
printf("%s: ", label);
for (int i = 0; i < n; i++) printf("%d ", arr[i]);
printf("\n");
}
int main() {
int nums[] = {42, 17, 93, 8, 55, 31};
int n = 6;
print_array(nums, n, "Original ");
qsort(nums, n, sizeof(int), compare_asc);
print_array(nums, n, "Ascending ");
qsort(nums, n, sizeof(int), compare_desc);
print_array(nums, n, "Descending");
return 0;
}
Output
Original  : 42 17 93 8 55 31
Ascending : 8 17 31 42 55 93
Descending: 93 55 42 31 17 8

ডিসপ্যাচ টেবিল — ফাংশন পয়েন্টারের অ্যারে (Dispatch Table — Array of Function Pointers)

যেকোনো ফাংশন পয়েন্টারের অ্যারেকে (array of function pointers) মূলত ডিসপ্যাচ টেবিল (dispatch table) বলা হয়। এখানে বড় কোনো switch স্টেটমেন্ট (statement) লেখার বদলে, আপনি মূলত একটি অ্যারের (array) ইনডেক্স ব্যবহার করতে পারেন এবং ওই পজিশনের (position) বা অবস্থানের যেকোনো ফাংশনকে সরাসরি কল বা ডাকতে (call) পারেন। ইন্টারপ্রেটার (interpreters), কমান্ড প্রসেসর (command processors) এবং স্টেট মেশিনগুলোতে (state machines) সাধারণত এই প্যাটার্নটিই বা pattern ব্যবহৃত হয়ে থাকে।

সাধারণ ডিসপ্যাচ টেবিল (ক্যালকুলেটর) (Simple Dispatch Table (Calculator))

#include <stdio.h>
typedef double (*MathFunc)(double, double);
double op_add(double a, double b) { return a + b; }
double op_sub(double a, double b) { return a - b; }
double op_mul(double a, double b) { return a * b; }
double op_div(double a, double b) { return b != 0 ? a / b : 0; }
int main() {
// ডিসপ্যাচ টেবিল (Dispatch table): ফাংশন পয়েন্টারের অ্যারে (array of function pointers)
MathFunc operations[] = { op_add, op_sub, op_mul, op_div };
const char *symbols[] = { "+", "-", "*", "/" };
double a = 20.0, b = 6.0;
for (int i = 0; i < 4; i++) {
double result = operations[i](a, b);
printf("%.0f %s %.0f = %.2f\n", a, symbols[i], b, result);
}
return 0;
}
Output
20 + 6 = 26.00
20 - 6 = 14.00
20 * 6 = 120.00
20 / 6 = 3.33
Note: ফাংশন পয়েন্টারের সিনট্যাক্স চিট শিট (Function pointer syntax cheat sheet): কোনো typedef ব্যবহার না করে (Without typedef): int (*fp)(int, int) = add; কোনো typedef ব্যবহার করে (With typedef): typedef int (*MathOp)(int, int); MathOp fp = add; এর উভয় পদ্ধতিই একে অপরের সাথে আইডেন্টিকাল বা হুবহু এক (identical) — এখানকার typedef মূলত শুধুমাত্র এর টাইপটিকে (type) একটি নতুন নাম দিয়ে থাকে। তবে কোনো কিছু নিয়ে দ্বিমত বা সন্দেহ (doubt) হলে, সবসময় এই typedef কেই ব্যবহার করবেন।
চ্যালেঞ্জ

ছোট কুইজ

একটি ফাংশন পয়েন্টার (function pointer) মূলত তার ভেতরে কী সেভ বা স্টোর (store) করে রাখে?
Preprocessor & Macros