مقدمة: ما تخفيه التوثيقات
تخيل: أنت تكتب خادمًا عالي التحميل أو خوارزمية معالجة بيانات. لديك اثنان std::vector<int>، وتحتاج إلى دمجهما في واحد. قد يبدو الأمر بسيطًا؟ تأخذ وتضيف. لكن C++ لا يغفر النهج السطحي. طريقة واحدة خاطئة - وستحصل على نسخ زائدة، انخفاض في الأداء، أو حتى سلوك غير محدد.
سؤال «Appending a vector to a vector» على Stack Overflow حصد آلاف المشاهدات ليس من قبيل الصدفة. حتى المطورون ذوو الخبرة يخلطون أحيانًا بين push_back و insert أو ينسون move semantics. في هذه المقالة، سننظر تحت غطاء المترجم ونتعرف على كيفية عمل إضافة متجه إلى آخر في الواقع. ستتعرف على لماذا قد يكون std::copy أسرع من insert، ومتى تنقذك move semantics في مشروعك.
الطريقة الأكثر وضوحًا (والأخطر): push_back في حلقة
غالبًا ما يكتب المبتدئون هكذا:
std::vector<int> dest = {1, 2, 3};std::vector<int> src = {4, 5, 6};
for (size_t i = 0; i < src.size(); ++i) { dest.push_back(src[i]);}ماذا يحدث هنا؟ في كل تكرار، يتحقق push_back مما إذا كانت الذاكرة المخصصة كافية. إذا لم تكن كافية - يحدث إعادة تخصيص: يتم نسخ المصفوفة القديمة إلى منطقة ذاكرة جديدة، ويتم حذف القديمة. مع ثلاثة عناصر، قد يكون هناك 2-3 عمليات إعادة تخصيص. بالنسبة للمتجهات الصغيرة، هذا غير ملحوظ، ولكن مع آلاف العناصر - كارثة.
المشكلة: O(n) من الفحوصات والنسخ المحتمل للمصفوفة بأكملها. في C++17 والإصدارات الأحدث، قد يحسن المترجم ذلك، لكن الاعتماد على هذا أمر محفوف بالمخاطر.
ماذا يحدث تحت غطاء push_back؟
- التحقق من
size() < capacity(). - إذا لم يكن كذلك - استدعاء
reallocate(new_capacity). - نسخ (أو نقل) العناصر الموجودة.
- حذف المخزن المؤقت القديم.
- نسخ العنصر الجديد.
الاستنتاج: لا تستخدم أبدًا حلقة لإضافة متجه كامل، إلا إذا كنت تفعل ذلك عن قصد لعدد صغير من العناصر.
الطريقة الصحيحة: std::vector::insert
توفر المكتبة القياسية حلاً أنيقًا:
std::vector<int> dest = {1, 2, 3};std::vector<int> src = {4, 5, 6};
dest.insert(dest.end(), src.begin(), src.end());سطر واحد! لكن ما يختبئ خلف هذا الاستدعاء؟ يقوم insert بما يلي:
- يحسب مقدار الذاكرة المطلوبة:
dest.size() + src.size(). - إذا كانت السعة الحالية غير كافية - يخصص بالضبط ما هو مطلوب (أو باحتياطي، حسب التنفيذ).
- ينقل العناصر الموجودة (إذا كان الإدراج ليس في النهاية).
- ينسخ (أو ينقل) العناصر الجديدة من
src.
مهم: يقوم insert بعملية إعادة تخصيص واحدة (أو لا شيء إذا كانت المساحة كافية). هذا أفضل من الحلقة، ولكن لا يزال هناك نسخ للعناصر من src.
أداء insert
إذا لم تعد src مطلوبة بعد الإدراج، يمكنك نقل عناصرها باستخدام std::make_move_iterator:
dest.insert(dest.end(), std::make_move_iterator(src.begin()), std::make_move_iterator(src.end()));هذا يحول النسخ إلى نقل. بالنسبة لـ std::vector<int> لا فرق (int هو نوع بدائي)، ولكن بالنسبة لـ std::vector<std::string> أو الكائنات المعقدة، النقل هو أرخص بكثير مقارنة بالنسخ.
البديل: std::copy مع back_inserter
بعض المطورين يفضلون خوارزميات STL:
المدونات
توصيات الكتب
اقرأ أيضًا