push_back مقابل emplace_back: ما الفرق وماذا يختار المبتدئ؟

مدرب بايثون أونلاين للمبتدئين

تعلم Python بسهولة دون إرهاق نظري. حل مهام عملية مع التحقق التلقائي واكتب الكود مباشرة في المتصفح.

ابدأ الدورة

مقدمة: لماذا هذا مهم؟

\n\n

هل تساءلت يومًا لماذا يوجد في C++ طريقتان لإضافة عنصر إلى نهاية المتجه (vector)؟ يبدو الأمر وكأنه إعادة اختراع العجلة، خاصة مع وجود push_back المُجرب؟ الإجابة تكمن في الأداء والمرونة في لغة C++ الحديثة.

\n\n

على موقع Stack Overflow، جمع هذا السؤال آلاف المشاهدات ومئات التعليقات. وهذا ليس من قبيل الصدفة: فهم الفرق بين push_back و emplace_back ليس مجرد معرفة أكاديمية، بل مهارة عملية يمكنها تسريع كودك بمقدار 2-3 مرات. دعنا نوضح الأمر مرة واحدة وإلى الأبد.

\n\n

الفرق الأساسي: ماذا تقول التوثيقات؟

\n\n

لنبدأ بالأمر البسيط. كلا الطريقتين تضيفان عنصرًا إلى نهاية الحاوية (مثل المتجه، القائمة، الطابور). لكنهما تفعلان ذلك بطرق مختلفة:

\n\n
    \n
  • push_back — تستقبل كائنًا جاهزًا (بقيمة، مرجع، أو مرجع rvalue) و تنسخ/تنقل هذا الكائن إلى داخل الحاوية.
  • \n
  • emplace_back — تستقبل وسائط (arguments) لمُنشئ (constructor) الكائن و تنشئ الكائن مباشرة داخل الحاوية (in-place construction).
  • \n
\n\n

يبدو الأمر مجردًا؟ دعنا نلقي نظرة على الكود.

\n\n

مثال عملي: إنشاء كلاس (class)

\n\n

لنفترض أن لدينا كلاسًا بسيطًا اسمه Person:

\n\n
#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\n
int 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 مؤقت (أول استدعاء للمُنشئ). ثم يتم نقل هذا الكائن إلى المتجه (مُنشئ النقل). استدعاءان — عمل مضاعف.

\n\n

الخيار 2: push_back مع كائن جاهز

\n\n
int 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\n
int 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

الأداء: متى يكون الفرق حاسمًا؟

\n\n

بالنسبة للأنواع البسيطة (int, double, المؤشرات)، الفرق غير ملحوظ — يقوم المُجمّع (compiler) بتحسين كل شيء. لكن بالنسبة للكائنات المعقدة، يمكن أن يكون الفرق كبيرًا:

\n\n
    \n
  • الكائنات ذات مُنشئ النسخ الثقيل (مثل std::string، الحاويات

المدونات

توصيات الكتب