C++: محوّلات STL

في الدرس 38 تعلّمنا عن كائنات الدوال.

الآن، سنتعلّم عن محوّلات الدوال — تحويل الدوال الموجودة إلى الشكل الذي تحتاجه.

مثل لبنات LEGO، استخدام القطع الصغيرة لبناء شيء أكبر.


1. نظرة عامة على المحوّلات

(1) 1.1 ما هي المحوّلات؟

المحوّلات هي قوالب تُعدّل سلوك كائنات الدوال.

المحوّلات الشائعة:



2. std::bind — ربط المعاملات

(1) 2.1 الاستخدام الأساسي

يُستخدم std::bind لربط معاملات الدوال، مما ينشئ كائنات دوال جديدة.

مثال: ربط المعاملات (الصعوبة ⭐⭐)

▶ مثال 2: مثال برمجي (الصعوبة ⭐)

TEXT 📖 للعرض فقط
#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;
}

المخرجات:

TEXT 📖 للعرض فقط
add10(5) = 15
add10(20) = 30

💡 نصيحة:


(2) 2.2 إعادة ترتيب المعاملات

مثال: تبديل ترتيب المعاملات (الصعوبة ⭐⭐)

CPP
#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 (الصعوبة ⭐⭐)

TEXT 📖 للعرض فقط
#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 لنفي القيمة المُرجعة من كائن دالة.

مثال: نفي مسند (الصعوبة ⭐⭐)

CPP
#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 يُبسّطها.

مثال: استدعاء دوال العضو (الصعوبة ⭐⭐⭐)

TEXT 📖 للعرض فقط
#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: معالج درجات مرن (الصعوبة ⭐⭐⭐)

CPP
#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;
}
▶ جرّب الكود

المخرجات:

TEXT 📖 للعرض فقط
عدد الدرجات >= : 

❓ أسئلة شائعة

س: هل لا يزال std::bind ذا صلة في C++11؟ ج: نعم، لكن Lambda مُوصى بها أكثر. Lambda أكثر إيجازًا وأداءً أفضل.

TEXT 📖 للعرض فقط
// باستخدام std::bind
auto f1 = std::bind(add, 10, std::placeholders::_1);

// باستخدام Lambda (مُوصى به)
auto f2 = [](int x) { return add(10, x); };

س هل يمكن لـ stack وqueue استخدام حاويات أخرى كتنفيذ أساسي؟
ج نعم! تحدّد المحوّلات الحاوية الأساسية عبر معاملات القوالب: stack<int, vector<int>> تستخدم vector؛ queue<int, list<int>> تستخدم list. افتراضيًا، يستخدم stack بنية deque، وكذلك queue.

▶ مثال 3: طابور (الصعوبة ⭐)

CPP
#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;
}
▶ جرّب الكود

المخرجات:

TEXT 📖 للعرض فقط
الأمام: 
💡 نصيحة: الطابور هو بنية أول-dاخل-أول-خارج (FIFO). استخدم push() للإدخال، وfront() للوصول للعنصر الأمامي، وpop() للحذف.


📖 ملخص


📝 تمارين

  1. أساسي (الصعوبة ⭐): أنشئ stack<int>، أدخل 1، 2، 3 بالترتيب، ثم كرّر وأخرج جميع العناصر. لاحظ ترتيب الإخراج.

  2. متوسط (الصعوبة ⭐⭐): استخدم queue لتطبيق "طابور مهام طباعة" — حاكِ معالجة مهام طباعة متعددة بالترتيب، وأخرج طول الطابور المتبقي بعد كل مهمة.

  3. متقدم (الصعوبة ⭐⭐⭐): استخدم priority_queue لتطبيق "جدول مهام" — كل مهمة لها أولوية (1-10)، يعالج الطابور المهام من أعلى إلى أدنى أولوية، مع كسر التعادل بترتيب الإدراج.



الدرس التالي: معالجة الاستثناءات (#40)

Web-Tutorial.com

فريق Web-Tutorial التقني

منصة دروس برمجية يديرها عدة مطورين. كل درس يتم كتابته ومراجعته بواسطة مطورين متخصصين في المجال. نعمل على ضمان دقة وموثوقية المحتوى — إذا لاحظت أي مشكلة، فيرجى إخبارنا.

100%