C++: محوّلات STL
في الدرس 38 تعلّمنا عن كائنات الدوال.
الآن، سنتعلّم عن محوّلات الدوال — تحويل الدوال الموجودة إلى الشكل الذي تحتاجه.
مثل لبنات LEGO، استخدام القطع الصغيرة لبناء شيء أكبر.
1. نظرة عامة على المحوّلات
(1) 1.1 ما هي المحوّلات؟
المحوّلات هي قوالب تُعدّل سلوك كائنات الدوال.
المحوّلات الشائعة:
std::bind(ربط المعاملات)std::ref(التمرير بالمرجع)std::negate(النفي)std::mem_fn(مؤشر دالة العضو)
2. std::bind — ربط المعاملات
(1) 2.1 الاستخدام الأساسي
يُستخدم std::bind لربط معاملات الدوال، مما ينشئ كائنات دوال جديدة.
مثال: ربط المعاملات (الصعوبة ⭐⭐)
▶ مثال 2: مثال برمجي (الصعوبة ⭐)
#include <iostream>
#include <functional>
int add(int a, int b) {
return a + b;
}
int main() {
// ربط المعامل الأول لـ add بـ 10
auto add10 = std::bind(add, 10, std::placeholders::_1);
std::cout << "add10(5) = " << add10(5) << std::endl; // المخرج: 15
std::cout << "add10(20) = " << add10(20) << std::endl; // المخرج: 30
return 0;
}
المخرجات:
add10(5) = 15
add10(20) = 30
💡 نصيحة:
std::placeholders::_1يعني "اترك المعامل الأول ليتم ملؤه لاحقًا"
(2) 2.2 إعادة ترتيب المعاملات
مثال: تبديل ترتيب المعاملات (الصعوبة ⭐⭐)
#include <iostream>
#include <functional>
int subtract(int a, int b) {
return a - b;
}
int main() {
// تبديل ترتيب المعاملات
auto reverse_subtract = std::bind(subtract,
std::placeholders::_2,
std::placeholders::_1);
std::cout << "subtract(10, 3) = " << subtract(10, 3) << std::endl; // 7
std::cout << "reverse(10, 3) = " << reverse_subtract(10, 3) << std::endl; // -7
return 0;
}
3. std::ref — تغليف المرجع
(1) 3.1 المشكلة: التمرير بالقيمة
افتراضيًا، تمرّر خوارزميات STL كائنات الدوال بالقيمة، مما يعني أنه لا يمكن مشاركة الحالة.
مثال: الحل باستخدام std::ref (الصعوبة ⭐⭐)
#include <iostream>
#include <algorithm>
#include <vector>
#include <functional>
struct Counter {
int count = 0;
void operator()(int) { count++; }
};
int main() {
std::vector`<int>` v = {1, 2, 3, 4, 5};
Counter counter;
// ❌ خطأ! التمرير بالقيمة، يُستدعى نسخة من counter
std::for_each(v.begin(), v.end(), counter);
std::cout << "العدد: " << counter.count << std::endl; // المخرج: 0
// ✅ صحيح! استخدم std::ref للتمرير بالمرجع
std::for_each(v.begin(), v.end(), std::ref(counter));
std::cout << "العدد: " << counter.count << std::endl; // المخرج: 5
return 0;
}
4. std::not_fn — النفي
(1) 4.1 الاستخدام الأساسي
يُستخدم std::not_fn لنفي القيمة المُرجعة من كائن دالة.
مثال: نفي مسند (الصعوبة ⭐⭐)
#include <iostream>
#include <vector>
#include <algorithm>
#include <functional>
int main() {
std::vector`<int>` v = {1, 2, 3, 4, 5};
// إيجاد أول عدد زوجي
auto it1 = std::find_if(v.begin(), v.end(),
[](int x) { return x % 2 == 0; });
std::cout << "أول زوجي: " << *it1 << std::endl; // 2
// إيجاد أول عدد فردي (النفي)
auto it2 = std::find_if(v.begin(), v.end(),
std::not_fn([](int x) { return x % 2 == 0; }));
std::cout << "أول فردي: " << *it2 << std::endl; // 1
return 0;
}
5. std::mem_fn — مؤشر دالة العضو
(1) 5.1 المشكلة: مؤشرات دوال العضو صعبة الاستخدام
صياغة مؤشر دالة العضو معقدة؛ std::mem_fn يُبسّطها.
مثال: استدعاء دوال العضو (الصعوبة ⭐⭐⭐)
#include <iostream>
#include <vector>
#include <algorithm>
#include <memory>
struct Student {
std::string name;
void display() const {
std::cout << "طالب: " << name << std::endl;
}
};
int main() {
std::vector`<Student>` students = {{"Zhang San"}, {"Li Si"}};
// استخدام std::mem_fn لاستدعاء دالة العضو
std::for_each(students.begin(), students.end(),
std::mem_fn(&Student::display));
return 0;
}
6. مثال شامل
▶ مثال 1: معالج درجات مرن (الصعوبة ⭐⭐⭐)
#include <iostream>
#include <vector>
#include <algorithm>
#include <functional>
int main() {
std::vector`<int>` scores = {85, 92, 78, 90, 88};
int threshold = 90;
// عدّ عدد الدرجات التي لا تقل عن الحد
int count = std::count_if(scores.begin(), scores.end(),
std::bind(std::greater_equal`<int>`(),
std::placeholders::_1,
threshold));
std::cout << "عدد الدرجات >= " << threshold << ": " << count << std::endl;
return 0;
}
المخرجات:
عدد الدرجات >= :
❓ أسئلة شائعة
س: هل لا يزال std::bind ذا صلة في C++11؟ ج: نعم، لكن Lambda مُوصى بها أكثر. Lambda أكثر إيجازًا وأداءً أفضل.
// باستخدام std::bind
auto f1 = std::bind(add, 10, std::placeholders::_1);
// باستخدام Lambda (مُوصى به)
auto f2 = [](int x) { return add(10, x); };
stack<int, vector<int>> تستخدم vector؛ queue<int, list<int>> تستخدم list. افتراضيًا، يستخدم stack بنية deque، وكذلك queue.▶ مثال 3: طابور (الصعوبة ⭐)
#include <iostream>
#include <queue>
int main() {
std::queue`<int>` q;
q.push(10);
q.push(20);
q.push(30);
while (!q.empty()) {
std::cout << "الأمام: " << q.front() << std::endl;
q.pop();
}
return 0;
}
المخرجات:
الأمام:
push() للإدخال، وfront() للوصول للعنصر الأمامي، وpop() للحذف.
📖 ملخص
std::stack: محوّل مكدس، أول-dاخل-أول-خارج أخيرًا (LIFO)std::queue: محوّل طابور، أول-dاخل-أول-خارج (FIFO)std::priority_queue: طابور أولوية، يحذف حسب الأولويةstd::bind: ربط معاملات الدوال
📝 تمارين
-
أساسي (الصعوبة ⭐): أنشئ
stack<int>، أدخل 1، 2، 3 بالترتيب، ثم كرّر وأخرج جميع العناصر. لاحظ ترتيب الإخراج. -
متوسط (الصعوبة ⭐⭐): استخدم
queueلتطبيق "طابور مهام طباعة" — حاكِ معالجة مهام طباعة متعددة بالترتيب، وأخرج طول الطابور المتبقي بعد كل مهمة. -
متقدم (الصعوبة ⭐⭐⭐): استخدم
priority_queueلتطبيق "جدول مهام" — كل مهمة لها أولوية (1-10)، يعالج الطابور المهام من أعلى إلى أدنى أولوية، مع كسر التعادل بترتيب الإدراج.
- المحوّلات: stack/queue/priority_queue تغلّف حاويات أساسية
- stack LIFO: push/pop/top
- queue FIFO: push/pop/front/back
- priority_queue طابور أولوية: كومة عليا
- تحدّد المحوّلات الحاوية الأساسية عبر معاملات القوالب
الدرس التالي: معالجة الاستثناءات (#40)