سلسلة ++c للمحترفين الدرس 40: فئات القيم Value Categories في Cpp


محمد الميداوي

تُسنَد فئات القيمة إلى تعبيرات C++‎ بناءً على نتائج تلك التعبيرات، ويمكن لهذه الفئات أن تؤثّر على تحليل التحميل الزائد (overload resolution) للدوالّ في C++‎، كما تحدّد خاصّيتين مهمَّتين ومنفصلتين حول التعابير، تحدد الأول منهما إذا كان للتعبير هوية (identity)، أي إذا كان يشير إلى كائن له اسمُ متغيّرٍ (variable name)، حتى لو لم يكن اسم المتغيّر مُضمَّنًا في التعبير. والثانية هي إذا كان يجوز النقل ضمنيًا (implicitly move) من قيمة التعبير، أو بشكل أدقّ، إذا كان التعبير سيرتبط بأنواع المعاملات ذات القيمة اليمينية (r-value parameter types) عند استخدامه كمُعامل دالة أم لا.

تُعرّف C++‎ ثلاث فئات تمثّل مجموعة من هذه الخصائص:

  • lvalue - تعبير يساري (تعبيرات ذات هوّية، ولكن لا يمكن النقل منها).
  • xvalue - تعبير مرتحل (تعبيرات ذات هوّية ويمكن النقل منها).
  • prvalue - تعبير يميني خالص (تعبيرات بدون هوية ويمكن النقل منها).

لا تحتوي C++‎ على تعبيرات ليس لها هوية ولا يمكن النقل منها. من ناحية أخرى، تعرّف C++‎ فئتي قيمَة أخريين، كل منهما تعتمد حصرًا على إحدى هذه الخصائص:

  • glvalue - تعبير يساري مُعمّم النوع (تعبيرات ذات هوية)
  • rvalue - تعبير يميني (تعبيرات يمكن النقل منها).

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

C09fH.png

القيم اليمينيّة rvalue

التعبير اليمينيّ rvalue هو أيّ تعبير يمكن نقله ضمنيًا بغض النظر عما إذا كانت له هوية. وبتعبير أدق، يمكن استخدام التعبيرات اليمينيّة كوسائط للدوال التي تأخذ مُعاملات من النوع ‎T &&‎ ( يمثّل ‎T‎ نوع التعبير ‎expr‎)، والتعبيرات اليمينية هي وحدها التي يمكن تمريرها كوسائط لمعاملات مثل هذه الدوالّ. أما في حال استخدام تعبير غير يميني فإنّّ تحليل التحميل الزائد سيختار أيّ دالّة لا تستخدم مرجع يمينيًا (rvalue reference)، وسيطلق خطأً في حال تعذّر العثور عليها.

تشمل فئة التعبيرات اليمينية حصرًا جميع تعبيرات xvalue و prvalue، وتحوّل دالّة المكتبة القياسية ‎std::move‎ تعبيرًا غير يمينيّ بشكل صريح إلى تعبير يميني، إذ تحوّل التعبير إلى تعبير من الفئة xvalue، فحتى لو كان التعبير يمينيا خالصًا ناقص الهوية (identity-less prvalue) من قبل، فإنّه سيكتسب هويةً -اسم معامِل الدالة- عبر تمريره كمُعامل إلى ‎std::move‎، ويصبح من الفئة xvalue. انظر المثال التالي:

std::string str("init"); //1
std::string test1(str); //2
std::string test2(std::move(str)); //3

str = std::string("new value"); //4
std::string &&str_ref = std::move(str); //5
std::string test3(str_ref); //6

يأخذ منشئ السلاسل النصية مُعاملًا واحدًا من النوع std::string‎&&‎‏‏‎، ويُطلق على هذا المنشئ عادة "منشئ النقل" (move constructor)، لكن فئة القيمة الخاصة بـ ‎str‎ ليست يمينيّة (بل يساريّة)، لذا لا يمكن استدعاء التحميل الزائد للمنشئ. وبدلاً من ذلك يُستدعي التحميل الزائد لـ ‎const std::string&‎، أي مُنشئ النسخ.

تتغير الأمور في السطر الثالث حيث تكون القيمة المُعادة من ‎std::move‎ هي ‎T&&‎، إذ يمثّل ‎T‎ النوع الأساسي للمُعامِل المُمرّر، لذا فإنّّ ‎std::move(str)‎ تُعيد ‎std::string&&‎. واستدعاء الدالة الذي تكون قيمته المعادة مرجعًا يمينيًا (rvalue reference) يُعدُّ تعبيرًا يمينيًا، وتحديدًا من فئة xvalue، لذا قد تستدعي منشئَ النقلِ للسلسلة النصية std::string. بعد ذلك، أي بعد السطر الثالث، يكون النقل من str قد تم، وصارت محتويات str غير محددة.

يمرّر السطر 4 عنصرًا مؤقّتًا إلى مُعامل الإسناد الخاص بـ ‎std::string‎، الذي له تحميل زائد يأخذ std::string&&‎، ويكون التعبير std::string("new value")‎ تعبيرًا يمينيًا (على وجه التحديد من الفئة prvalue)، لذا قد يستدعي التحميل الزائد. وعليه سيُنقل العنصر المؤقّت إلى ‎str‎ مع استبدال المحتويات غير المُحدّدة بمحتويات محدّدة.

ينشئ السطر 5 مرجعًا يمينيًا مُسمّى (named rvalue reference) يحمل الاسم ‎str_ref‎ ويشير إلى ‎str‎، وهنا تصبح فئات القيمة مُربكة. فرغم أن ‎str_ref‎ ستكون مرجعًا يمينيًّا يشير إلى ‎std::string‎، إلا أن فئة قيمة التعبير ‎str_ref‎ ليست يمينيّة بل يسارية، ولهذا لا يمكن للمرء استدعاء مُنشئ النقل الخاص بـ ‎std::string‎ مع التعبير ‎str_ref‎. ولنفس السبب فإن السطر 6 ينسخ قيمة ‎str‎ إلى ‎test3‎. وإذا أردنا نقله، سيتعيّن علينا توظيف ‎std::move‎ مرّة أخرى.

القيمة المرتحلة xvalue

التعابير من فئة القيمة المرتحلة xvalue (أو eXpiring value) هي تعابير ذات هوية تمثّل كائنات يمكن النقل منها ضمنيًا. وفكرتها بشكل عام هي أنّ الكائنات التي تمثّلها هذه التعبيرات هي على وشك أن تُدمَّر، وبالتالي فإنّ النقل منها ضمنيًا سيكون مقبولًا.

انظر الأمثلة التالية على ذلك:

struct X
{
    int n;
};
extern X x;
4;    // prvalue: ليس لديها هوية
x;    // lvalue
x.n;    // lvalue
std::move(x);    // xvalue
std::forward<X&>(x);    // lvalue
X
{
    4
};    // prvalue: ليس لها هوية
X
{
    4
}.n;    // xvalue: ليس لها هوية وتشير إلى موارد يمكن إعادة استخدامها

تعبيرات القيمة اليمينية الخالصة prvalue

تعبير القيمة اليمينيّة الخالصة prvalue (أو pure-rvalue) هي تعبيرات تفتقر إلى الهوية، ويُستخدم تقييمها عادة لتهيئة كائن يمكن نقله ضمنيًا. هذه بعض الأمثلة على ذلك:

  • التعبيرات التي تمثّل كائنات مؤقّتة، مثل ‎std::string("123")‎.
  • استدعاء دالة لا تعيد مرجعًا.
  • كائن حرفي (باستثناء السلاسل النصية الحرفية - إذ أنها يسارية)، مثل ‎1‎ أو ‎true‎ أو ‎0.5f‎ أو ‎'a'‎.
  • تعبيرات لامدا.

لا يمكن تطبيق معامل العنونة (addressof operator) المُضمّن (‎&‎) على هذه التعبيرات.

تعبيرات القيمة اليسارية lvalue

التعبيرات اليسارية lvalue هي تعبيرات ذات هويّة لكن لا يمكن النقل منها ضمنيًا، وتشمل التعبيرات التي تتألّف من اسم متغيّر واسم دالّة والتعبيرات التي تستخدمها مُعاملات التحصيل (dereference) المُضمّنة والتعبيرات التي تشير إلى مراجع يسارية.

تكون القيمة اليسارية عادة مجرّد اسم، لكن يمكن أن تأتي بأشكال أخرى أيضًا:

struct X
{  };
X x;    // قيمة يسارية x
X* px = &x;  // px is an lvalue 
*px = X {};    // قيمة يمينيّة خالصة X{}  هي قيمة يسارية، و *px 
X* foo_ptr();   //  قيمة يمينيّة خالصة foo_ptr() 
X &foo_ref();    // قيمة يسارية foo_ref() 

في حين أنّ معظم الكائنات الحرفية (على سبيل المثال ‎4‎، ‎'x'‎) هي تعبيرات يمينية خالصة، إلا أن السلاسل النصية الحرفية يسارية.

تعبيرات القيم اليسارية المُعمّمة glvalue

تعابير القيم اليسارية المُعمّمة glvalue (أو "generalized lvalue") هي التعابير التي ليس لها هوية بغض النظر عما إذا كان يمكن النقل منها أم لا.

وتشمل هذه الفئة تعبيرات القيم اليسارية (التعبيرات التي لها هوية ولكن لا يمكن النقل منها)، وتعبيرات القيمة المرتحلة xvalues (التعبيرات التي لها هوية، ويمكن النقل منها). بالمقابل، فهي لا تشمل القيم اليمينيةّ الخالصة prvalues (تعبيرات بدون هوية).

وإذا كان لتعبيرٍ ما اسم، فإنّّه يساريّ معمَّم glvalue:

struct X { int n; };
X foo();

X x;

x;  
std::move(x); 

foo();  
X {};   
X {}.n;    

في الشيفرة السابقة:

  • x له اسم، وعليه يكون يساريًا معمَّمًا.
  • (std::move(x له اسم بما أننا ننقل من x، وعليه يكون قيمة يسارية معمَّمة glvalue لكن بما أننا نستطيع النقل منها فتكون قيمة مرتحلة وليست يسارية.
  • ()foo ليس له اسم، فهو يميني خالص، وليس يساريا معمَّمًا.
  • {} X قيمة مؤقتة ليس لها اسم، لذا فالتعبير يميني خالص، وليس يساريا معمَّمًا.
  • X {}.n له اسم، لذا فهو يساري معمم، كما يمكن النقل منه، لذا فهو مرتحل وليس يساريًا معمَّمًا.

هذا الدرس جزء من سلسلة دروس عن C++‎.

ترجمة -بتصرّف- للفصل Chapter 74: Value Categories من كتاب C++ Notes for Professionals





تفاعل الأعضاء


لا توجد أيّة تعليقات بعد



يجب أن تكون عضوًا لدينا لتتمكّن من التعليق

انشاء حساب جديد

يستغرق التسجيل بضع ثوان فقط


سجّل حسابًا جديدًا

تسجيل الدخول

تملك حسابا مسجّلا بالفعل؟


سجّل دخولك الآن