هل من الأفضل في C++ التمرير بالقيمة أو التمرير بالمرجع الثابت؟

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

سؤال

هل من الأفضل في C++ التمرير بالقيمة أو التمرير بالمرجع الثابت؟

أنا أتساءل ما هي الممارسة الأفضل.أدرك أن المرور بالمرجع الثابت يجب أن يوفر أداءً أفضل في البرنامج لأنك لا تقوم بعمل نسخة من المتغير.

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

المحلول

لقد كان من المعتاد التوصية بأفضل الممارسات بشكل عام1 ل استخدم pass by const ref لـ كل الانواع, ، باستثناء الأنواع المضمنة (char, int, double, ، وما إلى ذلك)، للمكررات والكائنات الوظيفية (لامدا، الطبقات المشتقة من std::*_function).

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

مع C++ 11، اكتسبنا نقل الدلالات.باختصار، تسمح دلالات النقل، في بعض الحالات، بتمرير كائن "حسب القيمة" دون نسخه.على وجه الخصوص، هذا هو الحال عندما يكون الكائن الذي تقوم بتمريره هو com.rvalue.

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

في هذه المواقف لدينا المقايضة التالية (المبسطة):

  1. يمكننا تمرير الكائن حسب المرجع، ثم نسخه داخليًا.
  2. يمكننا تمرير الكائن بالقيمة.

لا يزال "التمرير حسب القيمة" يؤدي إلى نسخ الكائن، إلا إذا كان الكائن عبارة عن قيمة r.في حالة قيمة r، يمكن نقل الكائن بدلاً من ذلك، بحيث لا تصبح الحالة الثانية فجأة "نسخ، ثم نقل" بل "نقل، ثم (من المحتمل) نقل مرة أخرى".

بالنسبة للكائنات الكبيرة التي تنفذ مُنشئات الحركة المناسبة (مثل المتجهات والسلاسل ...)، فإن الحالة الثانية هي إذن إلى حد كبير أكثر كفاءة من الأول.ولذلك، فمن المستحسن أن استخدم التمرير حسب القيمة إذا كانت الوظيفة تمتلك ملكية الوسيطة، وإذا كان نوع الكائن يدعم النقل الفعال.


ملاحظة تاريخية:

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

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

لكن بشكل عام لا يستطيع المترجم تحديد ذلك، كما أن ظهور دلالات الحركة في لغة C++ جعل هذا التحسين أقل أهمية بكثير.


1 على سبيل المثالفي سكوت مايرز، فعالة C ++.

2 ينطبق هذا غالبًا بشكل خاص على منشئي الكائنات، الذين قد يأخذون الوسائط ويخزنونها داخليًا لتكون جزءًا من حالة الكائن المُنشأ.

نصائح أخرى

يحرر: مقال جديد بقلم ديف أبراهامز على cpp-next:

تريد السرعة؟تمر بالقيمة.


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

foo * f;

void bar(foo g) {
    g.i = 10;
    f->i = 2;
    g.i += 5;
}

يمكن للمترجم تحسينه

g.i = 15;
f->i = 2;

لأنه يعلم أن f وg لا يشتركان في نفس الموقع.إذا كان g مرجعًا (foo &)، فلا يمكن للمترجم أن يفترض ذلك.نظرًا لأنه يمكن بعد ذلك تسمية g.i باسم مستعار بواسطة f->i ويجب أن تكون قيمته 7.لذلك سيتعين على المترجم إعادة جلب القيمة الجديدة لـ g.i من الذاكرة.

لمزيد من القواعد العملية، إليك مجموعة جيدة من القواعد الموجودة في نقل البنائين مقال (ينصح بقراءته بشدة).

  • إذا كانت الدالة تنوي تغيير الوسيطة كأثر جانبي، فاعتبرها بمرجع غير ثابت.
  • إذا لم تقم الدالة بتعديل وسيطتها وكانت الوسيطة من النوع البدائي، فاعتبرها حسب القيمة.
  • بخلاف ذلك، اعتبرها مرجعًا ثابتًا، إلا في الحالات التالية
    • إذا كانت الوظيفة ستحتاج بعد ذلك إلى عمل نسخة من مرجع const على أي حال، فخذها حسب القيمة.

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

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

my::string uppercase(my::string s) { /* change s and return it */ }

ومع ذلك، إذا لم تكن بحاجة إلى تغيير المعلمة على أي حال، فارجع إلى const:

bool all_uppercase(my::string const& s) { 
    /* check to see whether any character is uppercase */
}

ومع ذلك، إذا كان الغرض من المعلمة هو كتابة شيء ما في الوسيطة، فقم بتمريره بمرجع غير ثابت

bool try_parse(T text, my::string &out) {
    /* try to parse, write result into out */
}

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

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

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

إذا كنت تفعل البرمجة القالب، كنت اضطر عادة لتمرير دائما المرجع CONST منذ كنت لا تعرف أنواع يتم تمريرها في اجتياز العقوبات لتمرير شيئا سيئا من حيث القيمة هي أسوأ بكثير من العقوبات من تمرير بنيت نوع -in التي كتبها المرجع CONST.

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

هذا ما أعمل به عادةً عند تصميم واجهة وظيفة غير القالب:

  1. تمرير بالقيمة إذا كانت الوظيفة لا ترغب في تعديل المعلمة والقيمة رخيصة للنسخ (int ، double ، float ، char ، bool ، إلخ ...لاحظ أن std::string وstd::vector وبقية الحاويات الموجودة في المكتبة القياسية ليست كذلك)

  2. تمرير بواسطة Const Pointer إذا كانت القيمة مكلفة للنسخ ولا ترغب الوظيفة في تعديل القيمة التي تم توجيهها إلى الفرق هي قيمة تعالجها الوظيفة.

  3. تمرير بواسطة المؤشر غير المؤشر إذا كانت القيمة باهظة الثمن للنسخ وتريد الوظيفة تعديل القيمة التي تم توجيهها إلى الفرق هي قيمة تعالجها الوظيفة.

  4. قم بالتمرير حسب مرجع const عندما يكون نسخ القيمة مكلفًا ولا تريد الدالة تعديل القيمة المشار إليها ولن تكون NULL قيمة صالحة إذا تم استخدام مؤشر بدلاً من ذلك.

  5. قم بالتمرير بمرجع غير ثابت عندما تكون القيمة مكلفة للنسخ وتريد الوظيفة تعديل القيمة المشار إليها ولن تكون NULL قيمة صالحة إذا تم استخدام مؤشر بدلاً من ذلك.

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

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

وممر من حيث القيمة لأنواع صغيرة.

وتمرير بالإشارة CONST لأنواع كبيرة (تعريف كبيرة يمكن أن تختلف بين الأجهزة) ولكن، في C ++ 11، وتمرير من حيث القيمة إذا كنت تسير على استهلاك البيانات، حيث يمكنك استغلال دلالات الخطوة. على سبيل المثال:

class Person {
 public:
  Person(std::string name) : name_(std::move(name)) {}
 private:
  std::string name_;
};

والآن سوف رمز الدعوة القيام به:

Person p(std::string("Albert"));

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

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

مثال void amount(int account , int deposit , int total )

معلمة الإدخال:الحساب ، Paramteter إخراج الإيداع:المجموع

الإدخال والإخراج مختلفان في استخدام المكالمة بواسطة vaule

  1. void amount(int total , int deposit )

إجمالي إخراج الإيداع إجمالي الإيداع

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