c++ - هي أيام اجتياز const std:: string & كمعلمة انتهت؟




c++11 (10)

لقد سمعت حديثًا حديثًا لـ Herb Sutter الذي اقترح أن أسباب اجتياز std::vector و std::string by const & ؛ اقترح أن كتابة وظيفة مثل ما يلي هو الأفضل الآن:

std::string do_something ( std::string inval )
{
   std::string return_val;
   // ... do stuff ...
   return return_val;
}

أفهم أن return_val سيكون عبارة عن rvalue عند النقطة التي ترجعها الدالة ، وبالتالي يمكن إعادتها باستخدام دلالات النقل ، وهي رخيصة جدًا. ومع ذلك ، لا يزال inval أكبر من حجم المرجع (الذي يتم تنفيذه عادة كمؤشر). هذا لأن std::string له مكونات متنوعة بما في ذلك مؤشر إلى كومة char[] عضو char[] أجل تحسين سلسلة قصيرة. لذا يبدو لي أن تمرير المرجع لا يزال فكرة جيدة.

هل يمكن لأي شخص أن يفسر لماذا كان هيرب قد قال هذا؟


هي أيام اجتياز const std :: string & كمعلمة انتهت؟

لا يأخذ العديد من الأشخاص هذه النصيحة (بما في ذلك Dave Abrahams) خارج النطاق الذي ينطبق عليه ، وتبسيطه للتطبيق على جميع معلمات std::string - دائمًا تمرير std::string بقيمة لا يعد "أفضل ممارسة" لأي وكل المعلمات والتطبيقات العشوائية لأن التحسينات التي تركز عليها هذه المحادثات / المقالات تنطبق فقط على مجموعة محدودة من الحالات .

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

كما هو الحال دائمًا ، يؤدي تمرير المرجع const إلى حفظ الكثير من النسخ عندما لا تحتاج إلى نسخة .

الآن إلى المثال المحدد:

ومع ذلك ، لا يزال الهدف أكبر بكثير من حجم المرجع (الذي يتم تنفيذه عادة كمؤشر). هذا لأن سلسلة std :: له مكونات متنوعة بما في ذلك مؤشر إلى كومة وشريحة عضو [] من أجل تحسين سلسلة قصيرة. لذا يبدو لي أن تمرير المرجع لا يزال فكرة جيدة. هل يمكن لأي شخص أن يفسر لماذا كان هيرب قد قال هذا؟

إذا كان حجم الرصة مصدر قلق (وافتراضًا أن هذا غير مضمَّن / محسّن) ، return_val + inval > return_val - IOW ، استخدام قمة الكومة بمرور القيمة هنا (ملاحظة: التبسيط المفرط لـ ABIs). في هذه الأثناء ، يمكن تمرير المرجع const مرجع تعطيل التحسينات. السبب الرئيسي هنا هو عدم تجنب نمو المداخن ، ولكن لضمان إمكانية إجراء التحسين حيثما كان ذلك ممكنًا .

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


IMO باستخدام مرجع C ++ لـ std::string هو تحسين محلي سريع وقصير ، بينما استخدام تمرير القيمة قد يكون (أو لا) تحسينًا عالميًا أفضل.

لذا فإن الجواب هو: ذلك يعتمد على الظروف:

  1. إذا قمت بكتابة كافة التعليمات البرمجية من خارج إلى الدالات الداخلية ، فأنت تعرف ما يفعله الرمز ، يمكنك استخدام المرجع const std::string & .
  2. إذا قمت بكتابة التعليمات البرمجية للمكتبة أو استخدام رمز المكتبة بشكل كبير حيث يتم تمرير السلاسل ، فمن المحتمل أن تكتسب المزيد من الناحية العالمية من خلال الثقة في سلوك منشئ نسخة std::string .

المشكلة هي أن "const" هو مؤهل غير محبب. ما هو المقصود عادةً بـ "المرجع سلسلة المرجع" هو "عدم تعديل هذه السلسلة" ، وليس "عدم تعديل عدد الإحالات". هناك ببساطة أي وسيلة ، في C ++ ، ليقول أي أعضاء هم "const". إما أنهم جميعا ، أو لا شيء منهم.

من أجل الإمساك بمشكلة اللغة هذه ، يمكن لـ STL أن تسمح لـ "C ()" في المثال الخاص بك بعمل نسخة نقل دلالية ، على أي حال ، وتجاهل "const" بصرف النظر عن العد المرجعي (وبالتالي افترض أنه لم يكن أعلن const لأنه كان mem-mapped أو nano-thready أو أيا كان). وطالما كانت محددة بشكل جيد ، فسيكون هذا أمرًا جيدًا.

بما أن STL لا ، لدي نسخة من سلسلة تثبتها <> بعيدا عن العداد المرجعي ، و - وها - ويمكنك أن تقوم بحرية بتمرير cmstring كمراجع const ، وأن تصنع نسخًا منها في وظائف عميقة ، طوال اليوم ، بدون تسرب أو مشاكل.

منذ C ++ لا يقدم دقة التباين هنا ، كتابة مواصفات جيدة وجعل كائن جديد لامعة "ثورة متحركة سلسلة" (سمسترينج) هو أفضل حل رأيته.


انظر "Herb Sutter" العودة إلى الأساسيات Essentials of Modern C ++ Style " . من بين موضوعات أخرى ، فإنه يراجع المعلمة التي تعبر عن النصيحة التي قدمت في الماضي ، والأفكار الجديدة التي تأتي مع C ++ 11 وتنظر بالتحديد إلى فكرة تمرير السلاسل حسب القيمة.

تظهر المعايير أن تمرير std::string s بقيمة ، في الحالات التي ستنسخ الدالة فيها في أي حال ، يمكن أن يكون أبطأ بكثير!

هذا لأنك تجبره على عمل نسخة كاملة (ومن ثم الانتقال إلى مكانه) ، بينما يقوم const& version بتحديث السلسلة القديمة التي قد تعيد استخدام المخزن المؤقت المخصص بالفعل.

انظر الشريحة 27: بالنسبة للوظائف "المحددة" ، يكون الخيار 1 هو نفسه كما كان دائمًا. يضيف الخيار 2 تحميل زائد لمرجع rvalue ، ولكن هذا يعطي اندماج تجسيمي إذا كانت هناك معلمات متعددة.

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

إذا كنت تريد أن ترى مدى عمقك في القلق بشأن هذا ، شاهد العرض التقديمي لـ Nicolai Josuttis والحظ الجيد مع ذلك ( "Perfect - Done!" n بعد العثور على خطأ في الإصدار السابق.


سبب عشب قال ما قاله بسبب حالات كهذه.

لنفترض أن لدي الدالة A التي تستدعي الدالة B ، والتي تستدعي الدالة C يمرر A سلسلة خلال B و C لا يعرف أو يهتم C ؛ كل A يعرف عن B هذا يعني أن C عبارة عن تفاصيل تنفيذ B

لنفترض أن A تم تعريفه على النحو التالي:

void A()
{
  B("value");
}

إذا كان B و C قد أخذوا السلسلة بـ const& ، فحينئذٍ يبدو الأمر كالتالي:

void B(const std::string &str)
{
  C(str);
}

void C(const std::string &str)
{
  //Do something with `str`. Does not store it.
}

كل شيء جيد وجيد. كنت مجرد تمرير مؤشرات حولها ، لا نسخ ، لا تتحرك ، الجميع سعيد. يأخذ C const& لأنه لا يخزن السلسلة. انها ببساطة تستخدم ذلك.

الآن ، أريد إجراء تغيير بسيط واحد: يحتاج C إلى تخزين السلسلة في مكان ما.

void C(const std::string &str)
{
  //Do something with `str`.
  m_str = str;
}

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

إلا أنه لا يمكن. لأنه يأخذ const& .

إذا قمت بتغيير C لأخذ معلمتها بالقيمة ، فهذا يتسبب فقط في B للقيام بالنسخة في هذا المعامل ؛ لا أكسب شيء.

لذلك إذا كنت قد تجاوزت للتو قيمة str من خلال كل الوظائف ، معتمدين على std::move لخلط البيانات حولها ، فلن يكون لدينا هذه المشكلة. إذا أراد أحد الاحتفاظ بها ، فيمكنه ذلك. إذا لم يفعلوا ذلك ، حسنًا.

هل هو أكثر تكلفة؟ نعم فعلا؛ الانتقال إلى قيمة أكثر تكلفة من استخدام المراجع. هل هو أقل تكلفة من النسخة؟ ليس للأوتار الصغيرة مع SSO. هل يستحق القيام به؟

ذلك يعتمد على حالة الاستخدام الخاصة بك. كم تكره تخصيصات الذاكرة؟


لا وجود للرصاص الفضي. كما هو الحال دائمًا ، يعتمد الأمر على حالة الاستخدام.

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

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


لقد قمت بنسخ / لصق الإجابة من هذا السؤال هنا ، وقمت بتغيير الأسماء والهجاء لتناسب هذا السؤال.

إليك رمز لقياس ما يتم طرحه:

#include <iostream>

struct string
{
    string() {}
    string(const string&) {std::cout << "string(const string&)\n";}
    string& operator=(const string&) {std::cout << "string& operator=(const string&)\n";return *this;}
#if (__has_feature(cxx_rvalue_references))
    string(string&&) {std::cout << "string(string&&)\n";}
    string& operator=(string&&) {std::cout << "string& operator=(string&&)\n";return *this;}
#endif

};

#if PROCESS == 1

string
do_something(string inval)
{
    // do stuff
    return inval;
}

#elif PROCESS == 2

string
do_something(const string& inval)
{
    string return_val = inval;
    // do stuff
    return return_val; 
}

#if (__has_feature(cxx_rvalue_references))

string
do_something(string&& inval)
{
    // do stuff
    return std::move(inval);
}

#endif

#endif

string source() {return string();}

int main()
{
    std::cout << "do_something with lvalue:\n\n";
    string x;
    string t = do_something(x);
#if (__has_feature(cxx_rvalue_references))
    std::cout << "\ndo_something with xvalue:\n\n";
    string u = do_something(std::move(x));
#endif
    std::cout << "\ndo_something with prvalue:\n\n";
    string v = do_something(source());
}

بالنسبة لي هذه المخرجات:

$ clang++ -std=c++11 -stdlib=libc++ -DPROCESS=1 test.cpp
$ a.out
do_something with lvalue:

string(const string&)
string(string&&)

do_something with xvalue:

string(string&&)
string(string&&)

do_something with prvalue:

string(string&&)
$ clang++ -std=c++11 -stdlib=libc++ -DPROCESS=2 test.cpp
$ a.out
do_something with lvalue:

string(const string&)

do_something with xvalue:

string(string&&)

do_something with prvalue:

string(string&&)

يلخص الجدول أدناه نتائجي (باستخدام clang -std = c ++ 11). الرقم الأول هو عدد منشآت النسخ والرقم الثاني هو عدد منشآت النقل:

+----+--------+--------+---------+
|    | lvalue | xvalue | prvalue |
+----+--------+--------+---------+
| p1 |  1/1   |  0/2   |   0/1   |
+----+--------+--------+---------+
| p2 |  1/0   |  0/1   |   0/1   |
+----+--------+--------+---------+

يتطلب حل تمرير القيمة الواحدة زيادة حمولة واحدة فقط ولكنه يكلف بناء تحرك إضافي عند اجتياز lvalues ​​و xvalues. هذا قد يكون أو لا يكون مقبولا في أي حالة معينة. كل الحلول لها مزايا وعيوب.


ما لم تكن في حاجة فعلا إلى نسخة ، فمن المعقول أن تأخذ const & . فمثلا:

bool isprint(std::string const &s) {
    return all_of(begin(s),end(s),(bool(*)(char))isprint);
}

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

إذا كنت بحاجة إلى نسخة ، فعادةً ما يكون تمرير القيمة والعودة حسب القيمة (دائمًا؟) هو الخيار الأفضل. في الحقيقة أنا لا أقلق حول هذا الأمر في C ++ 03 إلا إذا وجدت أن النسخ الإضافية تسبب بالفعل مشكلة في الأداء. يبدو نسخة elision موثوقة جدا على المجمعين الحديثة. أعتقد أن شكوك الناس وإصرارهم بأنك يجب أن تتحقق من جدول دعمكم للـ RVO هو في الغالب عفا عليه الزمن في الوقت الحاضر.

باختصار ، لا يغير C ++ 11 أي شيء في هذا الصدد باستثناء الأشخاص الذين لا يثقون في نسخة elision.


std::string ليس عادي بيانات قديمة (POD) ، وحجمها الخام ليس الشيء الأكثر ملاءمة على الإطلاق. على سبيل المثال ، إذا قمت بتمرير سلسلة أعلى من SSO وتخصيصها في كومة الذاكرة المؤقتة ، أتوقع أن لا يقوم المنشئ بنسخ تخزين SSO.

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


لا .

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

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





c++11