فئات التعبير في C ++

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


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

قليلا من التاريخ


ظهر المصطلحان lvalue و rvalue في C. تجدر الإشارة إلى أن الارتباك قد تم وضعه في المصطلحات في البداية ، لأنها تشير إلى التعبيرات وليس إلى القيم. تاريخياً ، القيمة هي ما يمكن تركه من مشغل المهمة ، والقيمة هي ما يمكن أن يكون صحيحًا فقط.


lvalue = rvalue; 

ومع ذلك ، فإن مثل هذا التعريف يبسط ويشوه الجوهر. تعريف C89 المعيار lvalue كمحدد موقع كائن ، أي كائن ذو موقع ذاكرة محدد. وفقًا لذلك ، تم تضمين كل ما لا يتوافق مع هذا التعريف في فئة rvalue .


Bjarn يسارع إلى الإنقاذ


في لغة C ++ ، تطورت مصطلحات فئات التعبير بقوة كبيرة ، خاصة بعد اعتماد معيار C ++ 11 ، الذي قدم مفاهيم روابط rvalue ونقل دلالات الألفاظ . تم وصف تاريخ ظهور مصطلحات جديدة بشكل مثير للاهتمام في مقالة Straustrup لمصطلح القيمة "جديد" .


تعتمد المصطلحات الجديدة والأكثر صرامة على خواصتين:


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

يتم تعبير تعبيرات التعبير عن الهوية تحت مصطلح glvalue ( القيم المعممة ) ، وتسمى تعبيرات التجوال rvalue . حددت مجموعات من هاتين الخاصيتين 3 فئات رئيسية من التعبيرات:


لديك هويةخالية من الهوية
لا يمكن نقلهاالقيمة-
يمكن نقلهاxvalueالقيمة

في الواقع ، قدم C ++ 17 Standard مفهوم elition copy - إضفاء الطابع الرسمي على المواقف حيث يمكن للمترجم وينبغي عليه تجنب نسخ الكائنات ونقلها. في هذا الصدد ، قد لا يتم نقل القيمة بالضرورة. التفاصيل والأمثلة يمكن العثور عليها هنا . ومع ذلك ، لا يؤثر هذا على فهم المخطط العام لفئات التعبيرات.


في معيار C ++ الحديث ، يتم تقديم هيكل الفئة في شكل مثل هذا المخطط:


الصورة


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


glvalue


التعبيرات في فئة glvalue لها الخصائص التالية:


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

rvalue


التعبيرات في فئة rvalue لها الخصائص التالية:


  • لا يمكنك الحصول على عنوان rvalue في الذاكرة - وهذا يأتي مباشرة من نقص خاصية الهوية ؛
  • لا يمكن أن يكون على الجانب الأيسر من بيان مهمة أو مركب ؛
  • يمكن استخدامها لتهيئة ارتباط ثابت أو قيمة منخفضة القيمة ، بينما يمتد عمر الكائن إلى عمر الارتباط ؛
  • إذا تم استخدامه كوسيطة عند استدعاء دالة لها نسختان زائدا: أحدهما يقبل مرجع قيمة ثابت والآخر مرجع rvalue ، ثم يتم تحديد الإصدار الذي يقبل مرجع rvalue . هذه الخاصية تُستخدم لتنفيذ دلالات النقل :

 class A { public: A() = default; A(const A&) { std::cout << "A::A(const A&)\n"; } A(A&&) { std::cout << "A::A(A&&)\n"; } }; ......... A a; A b(a); //  A(const A&) A c(std::move(a)); //  A(A&&) 

تقنيًا ، A&& عبارة عن قيمة قيمة ويمكن استخدامها لتهيئة كل من مرجع القيمة الثابتة ومرجع القيمة . ولكن بفضل هذه الخاصية ، لا يوجد أي غموض ؛ يتم قبول خيار المنشئ الذي يقبل مرجع rvalue .

القيمة


خصائص:


  • جميع خصائص glvalue (انظر أعلاه) ؛
  • يمكنك أن تأخذ العنوان (باستخدام المشغل الأحادي المدمج &
  • يمكن أن تكون القيم القابلة للتعديل على الجانب الأيسر من مشغل التعيين أو مشغلي التعيين المركب ؛
  • يمكن استخدامها لتهيئة إشارة إلى قيمة (ثابتة وغير ثابتة).

التعبيرات التالية تنتمي إلى فئة lvalue :


  • اسم متغير أو وظيفة أو حقل فئة من أي نوع. حتى إذا كان المتغير مرجع rvalue ، فإن اسم هذا المتغير في التعبير هو lvalue ؛

 void func() {} ......... auto* func_ptr = &func; // :     auto& func_ref = func; // :     int&& rrn = int(123); auto* pn = &rrn; // :    auto& rn = rrn; // :  lvalue- 

  • استدعاء دالة أو عامل التشغيل الزائد الذي يُرجع مرجع القيمة أو تعبير التحويل إلى نوع مرجع القيمة ؛
  • عوامل تشغيل التعيين المضمنة ، عوامل تشغيل التعيين المركبة ( = ، += ، /= ، وما إلى ذلك) ، الزيادة المسبقة المضمنة والزيادة المسبقة ( ++a ، - --b ) ، --b مؤشر المؤشر المدمج ( *p ) ؛
  • عامل التشغيل المدمج للوصول بواسطة الفهرس ( a[n] أو n[a] ) ، عندما يكون أحد المعاملات هو صفيف القيمة ؛
  • استدعاء دالة أو عبارة overloaded تقوم بإرجاع مرجع rvalue إلى دالة ؛
  • سلسلة حرفية مثل "Hello, world!" .

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

 auto* p = &”Hello, world!”; //   ,    

القيمة


خصائص:


  • جميع خصائص rvalue (انظر أعلاه) ؛
  • لا يمكن أن يكون متعدد الأشكال: أنواع التعبير الثابتة والديناميكية تتزامن دائمًا ؛
  • لا يمكن أن يكون من نوع غير مكتمل (باستثناء نوع الفراغ ، ستتم مناقشة هذا أدناه) ؛
  • لا يمكن أن يحتوي على نوع تجريدي أو أن يكون مجموعة من العناصر من نوع الملخص.

التعبيرات التالية تنتمي إلى فئة prvalue :


  • حرفية (باستثناء السلسلة) ، على سبيل المثال 42 ، true أو nullptr ؛
  • استدعاء دالة أو عامل التشغيل str.substr(1, 2) الذي يقوم بإرجاع غير مرجعية ( str.substr(1, 2) أو str1 + str2 أو it++ ) أو تعبير تحويل إلى نوع غير مرجعي (على سبيل المثال static_cast<double>(x) أو std::string{} ، (int)42 ) ؛
  • المدمج في الزيادة اللاحقة واللاحقة ( a++ ، b-- ) ، العمليات الرياضية المضمنة ( a + b ، a % b ، a & b ، a << b ، إلخ) ، العمليات المنطقية المضمّنة ( a && b ، a || b !a ، إلخ) ، عمليات المقارنة ( a < b ، a == b ، a >= b ، إلخ) ، العملية المضمنة لأخذ العنوان ( &a
  • هذا المؤشر
  • قائمة البند ؛
  • معلمة قالب غير نمطية ، إذا لم تكن فئة ؛
  • تعبير lambda ، على سبيل المثال [](int x){ return x * x; } [](int x){ return x * x; } .

xvalue


خصائص:


  • جميع خصائص rvalue (انظر أعلاه) ؛
  • جميع خصائص glvalue (انظر أعلاه).

أمثلة على تعبيرات xvalue :


  • استدعاء دالة أو مشغل مضمن يقوم بإرجاع مرجع rvalue ، على سبيل المثال std :: move (x) ؛

وفي الحقيقة ، نتيجة لاستدعاء std :: move () ، لا يمكنك الحصول على عنوان في الذاكرة أو تهيئة رابط له ، ولكن في نفس الوقت ، يمكن أن يكون هذا التعبير متعدد الأشكال:

 struct XA { virtual void f() { std::cout << "XA::f()\n"; } }; struct XB : public XA { virtual void f() { std::cout << "XB::f()\n"; } }; XA&& xa = XB(); auto* p = &std::move(xa); //  auto& r = std::move(xa); //  std::move(xa).f(); //  “XB::f()” 

  • عامل تشغيل مدمج للوصول بواسطة الفهرس ( a[n] أو n[a] ) عندما يكون أحد المعاملات هو صفيف rvalue .

بعض الحالات الخاصة


مشغل فاصلة


بالنسبة لمشغل الفاصلة المدمجة ، تتطابق فئة التعبير دائمًا مع فئة التعبير للمعامل الثاني.


 int n = 0; auto* pn = &(1, n); // lvalue auto& rn = (1, n); // lvalue 1, n = 2; // lvalue auto* pt = &(1, int(123)); // , rvalue auto& rt = (1, int(123)); // , rvalue 

تعبيرات باطلة


تعتبر استدعاءات الوظائف التي تُرجع لاغية ، اكتب تعبيرات التحويل لاغية ، ورمي استثناءات تعبيرات prvalue ، ولكن لا يمكن استخدامها لتهيئة المراجع أو كوسائط للدالات .


عامل المقارنة الثلاثي


تعريف فئة التعبير a ? b : c a ? b : c - الحالة غير تافهة ، كل هذا يتوقف على فئات الوسيطتين الثانية والثالثة ( b و c ):


  • إذا كانت b أو c من النوع void ، فإن فئة ونوع التعبير بالكامل تتوافق مع الفئة ونوع الوسيطة الأخرى. إذا كانت كلتا الوسيطتين من النوع الفارغ ، فإن النتيجة هي قيمة من النوع الفراغ ؛
  • إذا كان b و c glvalue من نفس النوع ، فإن النتيجة هي glvalue من نفس النوع ؛
  • في حالات أخرى ، تكون النتيجة هي القيمة.

بالنسبة إلى العامل الثلاثي ، يتم تحديد عدد من القواعد التي يمكن من خلالها تطبيق التحويلات الضمنية على الوسيطتين b و c ، لكن هذا يتجاوز إلى حد ما نطاق المقالة ؛ إذا كنت مهتمًا ، أوصي بالرجوع إلى قسم المشغل Conditional [expr.cond] من المعيار.


 int n = 1; int v = (1 > 2) ? throw 1 : n; // lvalue, .. throw   void,    n ((1 < 2) ? n : v) = 2; //  lvalue,  ,   ((1 < 2) ? n : int(123)) = 2; //   , ..    prvalue 

الإشارات إلى حقول وأساليب الطبقات والهياكل


بالنسبة إلى تعبيرات النموذج am و p->m (هنا نتحدث عن المشغل المضمن -> ) ، تنطبق القواعد التالية:


  • إذا كانت m عبارة عن عنصر تعداد أو طريقة فئة غير ساكنة ، فإن التعبير بأكمله يعتبر قيمة (على الرغم من أنه لا يمكن تهيئة الرابط بمثل هذا التعبير) ؛
  • إذا كان a rvalue و m هو حقل غير ساكن من نوع غير مرجعي ، فإن التعبير بأكمله ينتمي إلى فئة xvalue ؛
  • خلاف ذلك هو قيمة.

بالنسبة إلى مؤشرات أعضاء الفصل ( a.*mp و p->*mp ) ، تتشابه القواعد:


  • إذا كان mp هو مؤشر لطريقة الفصل ، فإن التعبير بأكمله يعتبر قيمة .
  • إذا كان a rvalue ، و mp هو مؤشر إلى حقل بيانات ، فإن التعبير بأكمله يشير إلى xvalue ؛
  • خلاف ذلك هو قيمة.

بت الحقول


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


حقول البت [class.bit]
إذا كان المُهيئ لمرجع من النوع const T & هو قيمة تشير إلى حقل بت ، فإن المرجع يكون مرتبطًا بتهيئة مؤقتة للاحتفاظ بقيمة حقل البتات ؛ لا يرتبط المرجع بحقل البت مباشرة.

 struct BF { int f:3; }; BF b; bf = 1; // OK auto* pb = &b.f; //  auto& rb = bf; //  

بدلا من الاستنتاج


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

Source: https://habr.com/ru/post/ar441742/


All Articles