مقدمة: لماذا هذا مهم؟
\n\nهل تساءلت يومًا لماذا يوجد في C++ طريقتان لإضافة عنصر إلى نهاية المتجه (vector)؟ يبدو الأمر وكأنه إعادة اختراع العجلة، خاصة مع وجود push_back المُجرب؟ الإجابة تكمن في الأداء والمرونة في لغة C++ الحديثة.
على موقع Stack Overflow، جمع هذا السؤال آلاف المشاهدات ومئات التعليقات. وهذا ليس من قبيل الصدفة: فهم الفرق بين push_back و emplace_back ليس مجرد معرفة أكاديمية، بل مهارة عملية يمكنها تسريع كودك بمقدار 2-3 مرات. دعنا نوضح الأمر مرة واحدة وإلى الأبد.
الفرق الأساسي: ماذا تقول التوثيقات؟
\n\nلنبدأ بالأمر البسيط. كلا الطريقتين تضيفان عنصرًا إلى نهاية الحاوية (مثل المتجه، القائمة، الطابور). لكنهما تفعلان ذلك بطرق مختلفة:
\n\n- \n
push_back— تستقبل كائنًا جاهزًا (بقيمة، مرجع، أو مرجع rvalue) و تنسخ/تنقل هذا الكائن إلى داخل الحاوية. \n emplace_back— تستقبل وسائط (arguments) لمُنشئ (constructor) الكائن و تنشئ الكائن مباشرة داخل الحاوية (in-place construction). \n
يبدو الأمر مجردًا؟ دعنا نلقي نظرة على الكود.
\n\nمثال عملي: إنشاء كلاس (class)
\n\nلنفترض أن لدينا كلاسًا بسيطًا اسمه Person:
#include <iostream>\n#include <vector>\n#include <string>\n\nclass Person {\npublic:\n std::string name;\n int age;\n\n // المُنشئ\n Person(const std::string& n, int a) \n : name(n), age(a) {\n std::cout << "تم استدعاء المُنشئ لـ " << name << std::endl;\n }\n\n // مُنشئ النسخ\n Person(const Person& other) \n : name(other.name), age(other.age) {\n std::cout << "مُنشئ النسخ لـ " << name << std::endl;\n }\n\n // مُنشئ النقل\n Person(Person&& other) noexcept\n : name(std::move(other.name)), age(other.age) {\n std::cout << "مُنشئ النقل لـ " << name << std::endl;\n }\n};\n\nالآن دعنا نرى ما سيحدث عند استخدام الطرق المختلفة.
\n\nالخيار 1: push_back مع كائن مؤقت
\n\nint main() {\n std::vector<Person> people;\n people.push_back(Person("Alice", 25));\n return 0;\n}\n\nالمخرجات:
\nتم استدعاء المُنشئ لـ Alice\nمُنشئ النقل لـ Alice\n\nماذا حدث؟ أولاً، يتم إنشاء كائن Person مؤقت (أول استدعاء للمُنشئ). ثم يتم نقل هذا الكائن إلى المتجه (مُنشئ النقل). استدعاءان — عمل مضاعف.
الخيار 2: push_back مع كائن جاهز
\n\nint main() {\n std::vector<Person> people;\n Person alice("Alice", 25);\n people.push_back(alice); // alice هو lvalue\n return 0;\n}\n\nالمخرجات:
\nتم استدعاء المُنشئ لـ Alice\nمُنشئ النسخ لـ Alice\n\nهنا نرى نسخًا، وليس نقلًا. إذا كان الكائن كبيرًا (يحتوي، على سبيل المثال، على مصفوفات ديناميكية)، فقد يكون النسخ مكلفًا.
\n\nالخيار 3: emplace_back
\n\nint main() {\n std::vector<Person> people;\n people.emplace_back("Bob", 30); // نمرر وسائط المُنشئ\n return 0;\n}\n\nالمخرجات:
\nتم استدعاء المُنشئ لـ Bob\n\nاستدعاء واحد فقط للمُنشئ! يتم إنشاء كائن Person مباشرة داخل ذاكرة المتجه، دون أي نسخ أو نقل وسيط.
الأداء: متى يكون الفرق حاسمًا؟
\n\nبالنسبة للأنواع البسيطة (int, double, المؤشرات)، الفرق غير ملحوظ — يقوم المُجمّع (compiler) بتحسين كل شيء. لكن بالنسبة للكائنات المعقدة، يمكن أن يكون الفرق كبيرًا:
\n\n- \n
- الكائنات ذات مُنشئ النسخ الثقيل (مثل
std::string، الحاويات