هل يجب على المرء أن يفضل خوارزميات STL على الحلقات المدرفلة يدويًا؟

StackOverflow https://stackoverflow.com/questions/135129

  •  02-07-2019
  •  | 
  •  

سؤال

يبدو أنني أرى حلقات "for" أكثر من التكرارات في الأسئلة والأجوبة هنا أكثر مما أرى for_each() وtransform() وما شابه.يقترح سكوت مايرز ذلك يفضل استخدام خوارزميات stl, أو على الأقل فعل ذلك في عام 2001.بالطبع، استخدامها غالبًا ما يعني نقل جسم الحلقة إلى دالة أو كائن وظيفي.قد يشعر البعض أن هذا تعقيد غير مقبول، بينما قد يشعر آخرون أنه يحل المشكلة بشكل أفضل.

لذا...هل ينبغي تفضيل خوارزميات STL على الحلقات المدرفلة يدويًا؟

هل كانت مفيدة؟

المحلول

يعتمد ذلك على:

  • ما إذا كان الأداء العالي مطلوبًا
  • سهولة قراءة الحلقة
  • ما إذا كانت الخوارزمية معقدة

إذا لم تكن الحلقة هي عنق الزجاجة، وكانت الخوارزمية بسيطة (مثل for_each)، فبالنسبة لمعيار C++ الحالي، أفضل حلقة ملفوفة يدويًا لسهولة القراءة.(محلية المنطق هي المفتاح.)

ومع ذلك، الآن بعد أن تم دعم C++0x/C++11 من قبل بعض المترجمين الرئيسيين، أود أن أقول استخدام خوارزميات STL لأنها تسمح الآن بتعبيرات لامدا - وبالتالي محلية المنطق.

نصائح أخرى

سأخالف التيار هنا وأؤيد أن استخدام خوارزميات STL مع الوظائف يجعل فهم التعليمات البرمجية وصيانتها أسهل بكثير، ولكن عليك القيام بذلك بشكل صحيح.عليك أن تولي المزيد من الاهتمام لسهولة القراءة والوضوح.على وجه الخصوص، عليك أن تحصل على التسمية الصحيحة.ولكن عندما تفعل ذلك، يمكن أن ينتهي بك الأمر إلى الحصول على تعليمات برمجية أكثر وضوحًا ووضوحًا، وتحول نموذجي إلى تقنيات ترميز أكثر قوة.

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

for (vector<Child>::iterator iter = children.begin();
    iter != children.end();
    ++iter)
{
    iter->setFooCount(n);
}

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

for_each(children.begin(), children.end(), SetFooCount(n));

واو، هذا يقول بالضبط ما نحتاجه.ليس عليك أن تكتشف ذلك؛ستعرف على الفور أنه يقوم بتعيين "Foo Count" لكل طفل.(سيكون الأمر أكثر وضوحًا إذا لم نكن بحاجة إلى .begin() / .end()، لكن لا يمكنك الحصول على كل شيء، ولم يستشيروني عند إنشاء المحكمة الخاصة بلبنان.)

من المؤكد أنك بحاجة إلى تحديد هذا العامل السحري، SetFooCount, ، لكن تعريفه نموذجي جدًا:

class SetFooCount
{
public:
    SetFooCount(int n) : fooCount(n) {}

    void operator () (Child& child)
    {
        child.setFooCount(fooCount);
    }

private:
    int fooCount;
};

في المجمل، إنه المزيد من التعليمات البرمجية، وعليك أن تنظر إلى مكان آخر لمعرفة ما هو بالضبط SetFooCount هو فعل.ولكن نظرًا لأننا أطلقنا عليها اسمًا جيدًا، فإننا لا نضطر في 99% من الحالات إلى إلقاء نظرة على الكود SetFooCount.نحن نفترض أنه يفعل ما يقوله، وما علينا إلا أن ننظر إلى for_each خط.

ما يعجبني حقًا هو أن استخدام الخوارزميات يؤدي إلى نقلة نوعية.بدلاً من التفكير في القائمة كمجموعة من الكائنات، والقيام بالأشياء لكل عنصر في القائمة، فإنك تفكر في القائمة ككيان من الدرجة الأولى، وتعمل مباشرة على القائمة نفسها.تتكرر حلقة for عبر القائمة، وتستدعي دالة عضو في كل عنصر لتعيين عدد Foo.بدلاً من ذلك، أقوم بتنفيذ أمر واحد، والذي يقوم بتعيين Foo Count لكل عنصر في القائمة.إنه أمر خفي، ولكن عندما تنظر إلى الغابة بدلاً من الأشجار، فإنك تكتسب المزيد من القوة.

لذلك، مع القليل من التفكير والتسمية الدقيقة، يمكننا استخدام خوارزميات STL لإنشاء تعليمات برمجية أكثر وضوحًا ووضوحًا، والبدء في التفكير على مستوى أقل تفصيلاً.

ال std::foreach هو نوع الكود الذي جعلني ألعن المحكمة الخاصة بلبنان منذ سنوات.

لا أستطيع أن أقول ما إذا كان ذلك أفضل، لكني أحب أكثر أن أحصل على رمز حلقتي تحت ديباجة الحلقة.بالنسبة لي، هو شرط قوي.و ال std::foreach لن يسمح لي البناء بذلك (الغريب أن إصدارات foreach من Java أو C# رائعة، بقدر ما أشعر بالقلق ...لذا أعتقد أن هذا يؤكد أن موقع جسم الحلقة مهم جدًا بالنسبة لي).

لذلك سأستخدم foreach فقط إذا كان هناك بالفعل خوارزمية قابلة للقراءة/الفهم يمكن استخدامها معها.إذا لم يكن الأمر كذلك، لا، لن أفعل.ولكن أعتقد أن هذه مسألة ذوق، إذ ربما ينبغي لي أن أبذل قصارى جهدي لفهم كل هذا الشيء وتعلم تحليله...

لاحظ أن الأشخاص الذين شاركوا في التعزيز شعروا على ما يبدو بنفس الطريقة إلى حد ما، لأنهم كتبوا BOOST_FOREACH:

#include <string>
#include <iostream>
#include <boost/foreach.hpp>

int main()
{
    std::string hello( "Hello, world!" );

    BOOST_FOREACH( char ch, hello )
    {
        std::cout << ch;
    }

    return 0;
}

يرى : http://www.boost.org/doc/libs/1_35_0/doc/html/foreach.html

هذا هو الشيء الوحيد الذي أخطأ فيه سكوت مايرز.

إذا كانت هناك خوارزمية فعلية تتطابق مع ما عليك القيام به، فاستخدم الخوارزمية بالطبع.

ولكن إذا كان كل ما عليك فعله هو التكرار عبر مجموعة والقيام بشيء ما لكل عنصر، فما عليك سوى إجراء الحلقة العادية بدلاً من محاولة فصل التعليمات البرمجية إلى عامل مختلف، مما يؤدي في النهاية إلى تقطيع التعليمات البرمجية إلى أجزاء دون أي مكاسب حقيقية.

هناك بعض الخيارات الأخرى مثل Boost::bind أو Boost::lambda، ولكن هذه أشياء معقدة حقًا في البرمجة الوصفية للقالب، فهي لا تعمل بشكل جيد مع تصحيح الأخطاء والتنقل خلال التعليمات البرمجية لذا يجب تجنبها بشكل عام.

كما ذكر آخرون، سيتغير كل هذا عندما تصبح تعبيرات لامدا مواطنًا من الدرجة الأولى.

ال for الحلقة أمر حتمي، والخوارزميات تصريحية.عندما تكتب std::max_element, ، من الواضح أن ما تحتاجه، عندما تستخدم حلقة لتحقيق نفس الشيء، ليس بالضرورة أن يكون الأمر كذلك.

يمكن أن تتمتع الخوارزميات أيضًا بميزة أداء طفيفة.على سبيل المثال، عند اجتياز std::deque, ، يمكن للخوارزمية المتخصصة تجنب التحقق بشكل متكرر مما إذا كانت الزيادة المحددة تحرك المؤشر فوق حدود القطعة.

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

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

بالنسبة للخوارزميات التي تأخذ وظائف، بشكل عام لا، حتى يمكن استخدام C++ 0x lambdas.إذا كان العامل صغيرًا وكانت الخوارزمية معقدة (معظمها ليس كذلك)، فقد يكون من الأفضل الاستمرار في استخدام الخوارزمية القياسية.

أنا معجب كبير بخوارزميات STL من حيث المبدأ ولكن في الممارسة العملية فهي مجرد وسيلة مرهقة للغاية.بحلول الوقت الذي تحدد فيه فئات المسند/المسند الخاصة بك، يمكن أن يتحول سطران للحلقة إلى أكثر من 40 سطرًا من التعليمات البرمجية التي يصعب اكتشافها فجأة بمقدار 10 مرات.

والحمد لله، الأمور تسير على ما يرام عليه أسهل في C++0x مع وظائف لامدا، auto و الجديد for بناء الجملة.ألق نظرة على هذا نظرة عامة على C++0x على ويكيبيديا.

لن أستخدم قاعدة صارمة وسريعة لذلك.هناك العديد من العوامل التي يجب أخذها في الاعتبار، مثل إجراء عملية معينة في التعليمات البرمجية الخاصة بك في كثير من الأحيان، أو أنها مجرد حلقة أو خوارزمية "فعلية"، هل تعتمد الخوارزمية على الكثير من السياق الذي قد يتعين عليك نقله إلى وظيفتك؟

على سبيل المثال لن أضع شيئا من هذا القبيل

for (int i = 0; i < some_vector.size(); i++)
    if (some_vector[i] == NULL) some_other_vector[i]++;

في خوارزمية لأنها ستؤدي إلى نسبة أكبر بكثير من التعليمات البرمجية وسيتعين علي التعامل مع تعريف بعض_المتجهات_الأخرى للخوارزمية بطريقة ما.

هناك الكثير من الأمثلة الأخرى حيث يكون استخدام خوارزميات STL منطقيًا جدًا، ولكن عليك أن تقرر على أساس كل حالة على حدة.

أعتقد أن واجهة خوارزمية STL هي دون المستوى الأمثل ويجب تجنبها بسبب استخدام مجموعة أدوات STL مباشرة (للخوارزميات) قد تعطي مكاسب صغيرة جدًا في الأداء، ولكنها ستكلف بالتأكيد سهولة القراءة وقابلية الصيانة وحتى القليل من إمكانية الكتابة عندما تتعلم كيفية استخدام الأدوات.

ما مدى كفاءة معيار الحلقة فوق المتجه:

int weighted_sum = 0;
for (int i = 0; i < a_vector.size(); ++i) {
  weighted_sum += (i + 1) * a_vector[i];  // Just writing something a little nontrivial.
}

من استخدام بنية for_each، أو محاولة دمج ذلك في استدعاء للتراكم؟

يمكنك القول بأن عملية التكرار أقل كفاءة، ولكن for _ Each يقدم أيضًا استدعاء دالة في كل خطوة (والذي قد يمكن تخفيفها من خلال محاولة تضمين الوظيفة، ولكن تذكر أن "inline" هو مجرد اقتراح للمترجم - قد يتجاهله).

وعلى أية حال، فإن الفرق صغير.في تجربتي، أكثر من 90٪ من الكود الذي تكتبه هو لا الأداء حاسم، ولكن يكون وقت المبرمج حرج.من خلال الحفاظ على حلقة STL الخاصة بك مضمنة حرفيًا، فهي قابلة للقراءة للغاية.هناك قدر أقل من المراوغة التي يمكن أن تتعثر بها، لنفسك أو للمشرفين في المستقبل.إذا كان ذلك مدرجًا في دليل الأسلوب الخاص بك، فأنت توفر بعض الوقت التعليمي للمبرمجين لديك (اعترف بذلك، تعلم استخدام STL بشكل صحيح في المرة الأولى يتضمن بضع لحظات مسكتك).هذا الجزء الأخير هو ما أعنيه بتكلفة قابلية الكتابة.

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

IMO، يجب تجنب الكثير من خوارزميات المكتبات القياسية مثل std::for_each - بشكل أساسي بسبب مشكلات عدم وجود lambda التي ذكرها الآخرون، ولكن أيضًا بسبب وجود شيء مثل إخفاء التفاصيل بشكل غير مناسب.

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

OTOH، يجدر بنا أن نأخذ في الاعتبار حقيقة أن الكثير من المبرمجين يستخدمون C++ (وقبل ذلك، C وPascal وما إلى ذلك) لفترة طويلة.العادات القديمة تموت بصعوبة، وهناك هذا الشيء الذي يسمى التنافر المعرفي مما يؤدي في كثير من الأحيان إلى الأعذار والمبررات.ومع ذلك، لا تقفز إلى الاستنتاجات - فمن المحتمل على الأقل أن يكون رجال المعايير مذنبين بالتنافر بعد اتخاذ القرار.

أعتقد أن العامل الكبير هو مستوى راحة المطور.

ربما يكون صحيحًا أن استخدام التحويل أو for_each هو الشيء الصحيح الذي يجب القيام به، لكنه ليس أكثر كفاءة، والحلقات المكتوبة بخط اليد ليست خطيرة بطبيعتها.إذا كان الأمر سيستغرق نصف ساعة للمطور لكتابة حلقة بسيطة، مقابل نصف يوم للحصول على بناء جملة التحويل أو for_each بشكل صحيح، ونقل الكود المقدم إلى دالة أو كائن دالة.ومن ثم سيحتاج المطورون الآخرون إلى معرفة ما يحدث.

ربما يكون من الأفضل للمطور الجديد أن يتعلم استخدام التحويل وfor_each بدلاً من الحلقات اليدوية، لأنه سيكون قادرًا على استخدامها باستمرار دون أخطاء.بالنسبة لبقيتنا الذين يعتبرون حلقات الكتابة طبيعة ثانية، ربما يكون من الأفضل الالتزام بما نعرفه، والتعرف أكثر على الخوارزميات في أوقات فراغنا.

ضع الأمر على هذا النحو -- إذا أخبرت مديري أنني قضيت اليوم في تحويل الحلقات اليدوية إلى for_each وتحويل المكالمات، أشك في أنه سيكون سعيدًا جدًا.

مرخصة بموجب: CC-BY-SA مع الإسناد
لا تنتمي إلى StackOverflow
scroll top