ترتبط فئات التعبيرات ، مثل 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; //
بدلا من الاستنتاج
كما ذكرت في المقدمة ، لا يزعم الوصف أعلاه أنه مكتمل ، ولكنه يعطي فكرة عامة عن فئات التعبيرات. توفر طريقة العرض هذه فهمًا أفضل قليلاً لفقرات المعيار ورسائل خطأ المحول البرمجي.