c++ - وجواب - ما هي دلالات الانتقال؟




معنى فوت بالانجليزي (8)

لقد انتهيت للتو من الاستماع إلى مقابلة بودكاست راديو البرمجيات الهندسية مع سكوت مايرز بخصوص C++0x . معظم الميزات الجديدة منطقية بالنسبة لي ، وأنا متحمس بالفعل حول C ++ 0x الآن ، باستثناء واحد. ما زلت لا احصل على دلالات الانتقال ... ما هي بالضبط؟


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

#include <cstring>
#include <algorithm>

class string
{
    char* data;

public:

    string(const char* p)
    {
        size_t size = strlen(p) + 1;
        data = new char[size];
        memcpy(data, p, size);
    }

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

    ~string()
    {
        delete[] data;
    }

    string(const string& that)
    {
        size_t size = strlen(that.data) + 1;
        data = new char[size];
        memcpy(data, that.data, size);
    }

يحدد منشئ النسخة ما يعنيه نسخ كائنات السلسلة. const string& that للمعلمة const string& that ترتبط بكل تعبيرات سلسلة النوع التي تسمح لك بعمل نسخ في الأمثلة التالية:

string a(x);                                    // Line 1
string b(x + y);                                // Line 2
string c(some_function_returning_a_string());   // Line 3

الآن تأتي البصيرة الرئيسية في دلالات الانتقال. لاحظ أنه فقط في السطر الأول الذي نُنسخ منه x هذه النسخة العميقة ضرورية حقًا ، لأننا قد نحتاج إلى فحص x لاحقًا ، وسنكون مندهشًا للغاية إذا تغير x بطريقة ما. هل لاحظت كيف قلت فقط x ثلاث مرات (أربع مرات إذا قمت بتضمين هذه الجملة) وكان يعني نفس الشيء بالضبط في كل مرة؟ نطلق تعبيرات مثل x "lvalues".

لا تكون الوسيطات في السطور 2 و 3 عبارة عن lvalues ​​، ولكن rvalues ​​، لأن كائنات السلسلة الأساسية لا تحمل أسماء ، لذلك لا توجد طريقة للعميل لفحصها مرة أخرى في وقت لاحق. يشير rvalues ​​إلى الكائنات المؤقتة التي تم إتلافها في الفاصلة المنقوطة التالية (لتكون أكثر دقة: في نهاية التعبير الكامل الذي يحتوي على rvalue). هذا أمر مهم لأنه أثناء التهيئة لـ b و c ، يمكننا القيام بكل ما نريد باستخدام السلسلة المصدر ، ولا يمكن للعميل أن يميز الفرق !

تقدم C ++ 0x آلية جديدة تسمى "مرجع rvalue" والتي ، من بين أمور أخرى ، تسمح لنا بالكشف عن الحجج rvalue عبر التحميل الزائد لوظيفة. كل ما علينا القيام به هو كتابة منشئ بمعلمة مرجع rvalue. داخل هذا المنشئ ، يمكننا فعل أي شيء نريده مع المصدر ، طالما أننا نتركه في حالة صالحة:

    string(string&& that)   // string&& is an rvalue reference to a string
    {
        data = that.data;
        that.data = nullptr;
    }

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

تهانينا ، أنت الآن تفهم أساسيات دلالات الانتقال! دعونا نواصل تنفيذ عامل التنازل. إذا لم تكن على دراية بنسخة ومبادلة التبادل ، فتعلمها وعدت لأنها تعبير C ++ رائع مرتبط بسلامة الاستثناء.

    string& operator=(string that)
    {
        std::swap(data, that.data);
        return *this;
    }
};

هاه ، هذا كل شيء؟ "أين مرجع rvalue؟" ربما تسال. "نحن لا نحتاجه هنا!" هو جوابي :)

لاحظ أننا نمرر المعلمة that حسب القيمة ، بحيث يجب أن يتم تهيئتها تمامًا مثل أي كائن سلسلة آخر. بالضبط كيف سيتم التهيئة؟ في الأيام القديمة من C++98 ، كان يمكن أن يكون الجواب "بواسطة منشئ النسخة". في C ++ 0x ، يختار المحول البرمجي بين مُنشئ النسخ ومنشئ النقل استنادًا إلى ما إذا كانت الوسيطة إلى عامل التعيين عبارة عن lvalue أو rvalue.

لذا إذا قلت a = b ، فسوف يقوم مُنشئ النسخة بتهيئة that (لأن التعبير b هو عبارة عن lvalue) ، ويقوم مشغل المهمة بتبديل المحتويات بنسخة عميقة تم إنشاؤها حديثًا. هذا هو تعريف النسخة ونسخة المبادلة - عمل نسخة ، مبادلة المحتويات بالنسخة ، ثم تخلص من النسخة بترك النطاق. لا جديد هنا.

ولكن إذا قلت a = x + y ، فسوف يقوم مُنشئ الخطوة بتهيئة that (لأن التعبير x + y هو عبارة عن rvalue) ، لذلك لا توجد نسخة عميقة متضمنة ، فقط حركة فعالة. لا يزال كائنًا مستقلاً من الحجة ، لكن بنائه كان تافهاً ، نظرًا لأنه لم يكن من الضروري نسخ بيانات الكومة ، فقط انتقلت. لم يكن من الضروري نسخها لأن x + y عبارة عن rvalue ، ومرة ​​أخرى ، لا بأس من الانتقال من كائنات السلسلة التي تشير إليها rvalues.

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

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


افترض أن لديك دالة تقوم بإرجاع كائن كبير:

Matrix multiply(const Matrix &a, const Matrix &b);

عندما تكتب رمزًا مثل هذا:

Matrix r = multiply(a, b);

ثم سيقوم مترجم C ++ عادي بإنشاء كائن مؤقت نتيجة multiply() ، استدعاء منشئ النسخ لتهيئة r ، ثم ثم إتلاف قيمة الإرجاع المؤقتة. نقل دلالات في C ++ 0x تسمح "move constructor" ليتم استدعاؤها لتهيئة r بنسخ محتوياتها ، ثم تجاهل القيمة المؤقتة دون الحاجة إلى تدميرها.

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


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

استغرق ستيفان T. Lavavej الوقت تقديم ملاحظات قيمة. شكراً جزيلاً لك يا ستيفان!

المقدمة

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

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

    class cannot_benefit_from_move_semantics
    {
        int a;        // moving an int means copying an int
        float b;      // moving a float means copying a float
        double c;     // moving a double means copying a double
        char d[64];   // moving a char array means copying a char array
    
        // ...
    };
    
  2. تنفيذ أنواع "الانتقال فقط" الآمنة ؛ أي الأنواع التي لا يكون للنسخ معنى لها ، ولكن الانتقال لا. تتضمن الأمثلة التأمينات ومقابض الملفات والمؤشرات الذكية ذات دلالات الملكية الفريدة. ملاحظة: تناقش هذه الإجابة std::auto_ptr ، وهو قالب مكتبة معيار C ++ 98 تم إيقافه ، والذي تم استبداله بـ std::unique_ptr في C ++ 11. من المحتمل أن يكون المبرمجين std::auto_ptr C ++ مألوفين إلى حد ما مع std::auto_ptr ، وبسبب "الدلالات std::auto_ptr " التي يعرضها ، يبدو أنها نقطة بداية جيدة لمناقشة دلالات النقل في C ++ 11. YMMV.

ما هي الخطوة؟

توفر المكتبة القياسية C ++ 98 مؤشر ذكية مع دلالات الملكية الفريدة تسمى std::auto_ptr<T> . إذا كنت غير معتاد على auto_ptr ، فإن غرضه هو ضمان تحرير كائن مخصص بشكل ديناميكي دائمًا ، حتى في مواجهة الاستثناءات:

{
    std::auto_ptr<Shape> a(new Triangle);
    // ...
    // arbitrary code, could throw exceptions
    // ...
}   // <--- when a goes out of scope, the triangle is deleted automatically

الشيء غير المعتاد حول auto_ptr هو سلوك "النسخ" الخاص بها:

auto_ptr<Shape> a(new Triangle);

      +---------------+
      | triangle data |
      +---------------+
        ^
        |
        |
        |
  +-----|---+
  |   +-|-+ |
a | p | | | |
  |   +---+ |
  +---------+

auto_ptr<Shape> b(a);

      +---------------+
      | triangle data |
      +---------------+
        ^
        |
        +----------------------+
                               |
  +---------+            +-----|---+
  |   +---+ |            |   +-|-+ |
a | p |   | |          b | p | | | |
  |   +---+ |            |   +---+ |
  +---------+            +---------+

ﻻﺣظ ﮐﯾف ﻻ ﯾﻘوم ﺗﮭﯾﺋﺔ b ﺑﻧﺳﺦ اﻟﻣﺛﻟث ، وﻟﮐن ﺑدﻻً ﻣن ذﻟك ، ﯾﻧﻘل ﻣﻟﮐﯾﺔ اﻟﻣﺛﻟث ﻣن a إﻟﯽ b . ونقول أيضًا "تم نقل b إلى b " أو "يتم نقل المثلث من a إلى b ". قد يبدو هذا محيرًا ، لأن المثلث نفسه يبقى دائمًا في نفس المكان في الذاكرة.

لنقل كائن يعني نقل ملكية بعض الموارد التي يديرها إلى كائن آخر.

ربما يبدو مُنشئ النسخ لـ auto_ptr شيئًا كهذا (مبسّط إلى حد ما):

auto_ptr(auto_ptr& source)   // note the missing const
{
    p = source.p;
    source.p = 0;   // now the source no longer owns the object
}

تحركات خطرة وغير مؤذية

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

auto_ptr<Shape> a(new Triangle);   // create triangle
auto_ptr<Shape> b(a);              // move a into b
double area = a->area();           // undefined behavior

لكن auto_ptr ليست دائما خطيرة. وظائف المصنع هي حالة استخدام بشكل مثالي ل auto_ptr :

auto_ptr<Shape> make_triangle()
{
    return auto_ptr<Shape>(new Triangle);
}

auto_ptr<Shape> c(make_triangle());      // move temporary into c
double area = make_triangle()->area();   // perfectly safe

لاحظ كيف يتبع كلا المثالين النمط التركيبي نفسه:

auto_ptr<Shape> variable(expression);
double area = expression->area();

ومع ذلك ، فإن أحدهم يستحضر سلوكًا غير معروف ، في حين أن الآخر لا يفعل ذلك. فما هو الفرق بين تعبيرات و make_triangle() ؟ أليس كلاهما من نفس النوع؟ في الواقع هم ، ولكن لديهم فئات قيمة مختلفة.

فئات القيمة

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

الانتقال من lvalues ​​مثل a خطير ، لأننا يمكن أن نحاول فيما بعد استدعاء وظيفة عضو عبر ، استدعاء سلوك غير معروف. من ناحية أخرى ، الانتقال من rvalues ​​مثل make_triangle() آمن تمامًا ، لأنه بعد قيام منشئ النسخ بعمله ، لا يمكننا استخدام المؤقت مرة أخرى. لا يوجد أي تعبير يدل على أنه مؤقت ؛ إذا قمنا ببساطة بكتابة make_triangle() مرة أخرى ، نحصل على مؤقت مختلف . في الواقع ، انتقل الانتقال من مؤقت بالفعل على السطر التالي:

auto_ptr<Shape> c(make_triangle());
                                  ^ the moved-from temporary dies right here

لاحظ أن الحرفين l و r لها أصل تاريخي في الجانب الأيسر والجانب الأيمن من المهمة. لم يعد هذا صحيحًا في لغة C ++ ، نظرًا لوجود lvalues ​​لا يمكن أن يظهر على الجانب الأيسر من المهمة (مثل المصفوفات أو الأنواع المعرفة من قبل المستخدم بدون مشغل التعيين) ، وهناك rvalues ​​يمكن (جميع أنواع فئات الصفوف) مع مشغل الاحالة).

تعد فئة rvalue لنوع الفصل عبارة عن تعبير ينشئ تقييمه كائنًا مؤقتًا. في الظروف العادية ، لا يشير أي تعبير آخر داخل نفس النطاق إلى نفس الكائن المؤقت.

مراجع Rvalue

نحن نفهم الآن أن الانتقال من الأرغفة يحتمل أن يكون خطيراً ، ولكن الانتقال من الرفات غير مؤذٍ. إذا كانت لغة C ++ تدعم اللغة للتمييز بين حجج lvalue من حجج rvalue ، فيمكننا إما منع الحركة تمامًا من lvalues ​​، أو على الأقل أن نتحرك من lvalues صريحًا في موقع الاتصال ، حتى لا ننتقل عن طريق الصدفة.

إجابة C ++ 11 لهذه المشكلة هي مراجع rvalue . مرجع rvalue هو نوع جديد من المرجع الذي يرتبط فقط بـ rvalues ​​، وبناء الجملة هو X&& . يُعرف الآن المرجع القديم الجيد X& باسم مرجع lvalue . (لاحظ أن X&& ليست مرجعًا لمرجع ؛ لا يوجد شيء في C ++.)

إذا وضعنا const في المزيج ، فلدينا بالفعل أربعة أنواع مختلفة من المراجع. ما هي أنواع تعبيرات النوع X يمكن أن ترتبط بها؟

            lvalue   const lvalue   rvalue   const rvalue
---------------------------------------------------------              
X&          yes
const X&    yes      yes            yes      yes
X&&                                 yes
const X&&                           yes      yes

في الممارسة العملية ، يمكنك نسيان const X&& . أن تكون مقيدة للقراءة من rvalues ​​ليست مفيدة جدا.

مرجع rvalue X&& هو نوع جديد من المرجع الذي يرتبط فقط بالامتيازات.

التحويلات الضمنية

مرت مراجع Rvalue بعدة إصدارات. منذ الإصدار 2.1 ، يرتبط أيضًا مرجع rvalue X&& بكافة فئات القيم من نوع Y مختلف ، بشرط أن يكون هناك تحويل ضمني من Y إلى X في هذه الحالة ، يتم تكوين مؤقت من النوع X ، ويرتبط مرجع rvalue بذلك المؤقت:

void some_function(std::string&& r);

some_function("hello world");

في المثال أعلاه ، "hello world" عبارة عن rvalue من النوع const char[12] . نظرًا لوجود تحويل ضمني من const char[12] خلال const char* إلى std::string ، يتم إنشاء مؤقت من نوع std::string ، ومن r إلى ذلك المؤقت. هذا هو أحد الحالات التي يكون فيها التمييز بين rvalues ​​(التعبيرات) و temporaries (الكائنات) ضبابي بعض الشيء.

تحرك الصانعين

من الأمثلة المفيدة على دالة بمعلمة X&& هي منشئ الحركة X::X(X&& source) . الغرض منه هو نقل ملكية المورد المُدار من المصدر إلى الكائن الحالي.

في C ++ 11 ، تم استبدال std::unique_ptr<T> بـ std::unique_ptr<T> الذي يستفيد من مراجع rvalue. unique_ptr نسخة مبسطة من unique_ptr . أولاً ، نقوم بتغليف مؤشر أولي وإفراط في تحميل المشغلين -> و * ، لذا يبدو فصلنا كمؤشر:

template<typename T>
class unique_ptr
{
    T* ptr;

public:

    T* operator->() const
    {
        return ptr;
    }

    T& operator*() const
    {
        return *ptr;
    }

يأخذ المُنشئ ملكية الكائن ، ويقوم destructor بحذفه:

    explicit unique_ptr(T* p = nullptr)
    {
        ptr = p;
    }

    ~unique_ptr()
    {
        delete ptr;
    }

الآن يأتي الجزء المثير للاهتمام ، وهو منشئ الخطوة:

    unique_ptr(unique_ptr&& source)   // note the rvalue reference
    {
        ptr = source.ptr;
        source.ptr = nullptr;
    }

يقوم مُنشئ النقل هذا بالضبط بما auto_ptr مُنشئ النسخ auto_ptr ، ولكن يمكن توفيره فقط مع rvalues:

unique_ptr<Shape> a(new Triangle);
unique_ptr<Shape> b(a);                 // error
unique_ptr<Shape> c(make_triangle());   // okay

فشل السطر الثاني في التحويل البرمجي ، نظرًا لأن a هي قيمة lvalue ، ولكن يمكن فقط ربط المعلمة unique_ptr&& source بـ rvalues. هذا بالضبط ما أردناه التحركات الخطرة لا ينبغي أبدا أن تكون ضمنية. السطر الثالث يجمع ما يرام ، لأن make_triangle() هو rvalue. سينقل منشئ الخطوة الملكية من المؤقت إلى c . مرة أخرى ، هذا هو بالضبط ما كنا نريد.

ينقل أداة نقل الملكية ملكية مورد مدار إلى الكائن الحالي.

انقل مشغلي المهام

آخر قطعة مفقودة هي مشغل احالة النقل. وتتمثل مهمتها في إطلاق المورد القديم والحصول على المورد الجديد من حجتها:

    unique_ptr& operator=(unique_ptr&& source)   // note the rvalue reference
    {
        if (this != &source)    // beware of self-assignment
        {
            delete ptr;         // release the old resource

            ptr = source.ptr;   // acquire the new resource
            source.ptr = nullptr;
        }
        return *this;
    }
};

لاحظ كيف يقوم هذا التطبيق لعامل تعيين النقل بتكرار منطق كل من destructor و move constructor. هل أنت على دراية بنسخة النسخ والمبادلة؟ يمكن تطبيقه أيضًا على نقل دلالات الألفاظ كمصطلح النقل والتقليب:

    unique_ptr& operator=(unique_ptr source)   // note the missing reference
    {
        std::swap(ptr, source.ptr);
        return *this;
    }
};

الآن هذا source متغير من النوع unique_ptr ، سيتم تهيئته بواسطة منشئ الخطوة؛ أي ، سيتم نقل الوسيطة إلى المعلمة. لا يزال مطلوبًا أن تكون الوسيطة عبارة عن rvalue ، لأن مُنقل الحركة نفسه له معلمة مرجع rvalue. عندما يصل تدفق التحكم إلى قوس إغلاق operator= ، يخرج source خارج النطاق ، ويتم تحرير المصدر القديم تلقائيًا.

يقوم مشغل نقل الحركة بنقل ملكية المورد المُدار إلى الكائن الحالي ، مما يؤدي إلى تحرير المصدر القديم. يبسط أسلوب نقل و مبادلة التطبيق.

الانتقال من lvalues

في بعض الأحيان ، نريد الانتقال من lvalues. أي أننا نريد في بعض الأحيان أن يعالج المترجم lvalue كما لو كان rvalue ، لذا يمكنه استدعاء مُنقل الحركة ، حتى ولو كان من المحتمل أن يكون غير آمن. لهذا الغرض ، يقدم C ++ 11 قالب وظيفة مكتبة قياسية يدعى std::move داخل الرأس <utility> . هذا الاسم مؤسف قليلاً ، لأن std::move ببساطة يلقي lvalue إلى rvalue ؛ لا يتحرك أي شيء من تلقاء نفسه. انها مجرد تمكين التحرك. ربما يجب أن يكون قد تم تسميته std::cast_to_rvalue أو std::enable_move ، لكننا عالقون مع الاسم الآن.

إليك كيف تنتقل بصراحة من فئة:

unique_ptr<Shape> a(new Triangle);
unique_ptr<Shape> b(a);              // still an error
unique_ptr<Shape> c(std::move(a));   // okay

لاحظ أنه بعد السطر الثالث ، لم يعد يمتلك مثلثًا. لا بأس ، لأنه من خلال كتابة std::move(a) بشكل واضح ، فقد جعلنا نوايانا واضحة: "عزيز منشئ ، افعل ما تريد a أجل تهيئة c ؛ لا يهمني بعد الآن. لا تتردد في الحصول على طريقك مع ".

std::move(some_lvalue) يلقي lvalue إلى rvalue ، مما يتيح تحريك لاحق.

Xvalues

لاحظ أنه على الرغم من أن std::move(a) هو rvalue ، لا يؤدي تقييمه إلى إنشاء كائن مؤقت. أجبر هذا اللغز اللجنة على إدخال فئة القيمة الثالثة. يسمى الشيء الذي يمكن ربطه بمرجع rvalue ، على الرغم من أنه ليس عبارة عن rvalue بالمعنى التقليدي ، بـ xvalue (قيمة eXpiring). تم إعادة تسمية rvalues ​​التقليدية إلى prvalues (rvalues ​​النقي).

كلا prvalues ​​و xvalues ​​هي rvalues. Xvalues ​​و lvalues ​​كلاهما glvalues (lvalues ​​المعمم). العلاقات أسهل للفهم مع رسم بياني:

        expressions
          /     \
         /       \
        /         \
    glvalues   rvalues
      /  \       /  \
     /    \     /    \
    /      \   /      \
lvalues   xvalues   prvalues

لاحظ أن xvalues ​​فقط جديدة بالفعل؛ الباقي هو فقط بسبب إعادة تسمية وتجميع.

تُعرَف rvalues ​​C ++ 98 باسم prvalues ​​في C ++ 11. استعاضيا عن جميع تكرارات "rvalue" في الفقرات السابقة بـ "prvalue".

الخروج من الوظائف

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

unique_ptr<Shape> make_triangle()
{
    return unique_ptr<Shape>(new Triangle);
}          \-----------------------------/
                  |
                  | temporary is moved into c
                  |
                  v
unique_ptr<Shape> c(make_triangle());

ربما من المدهش ، أنه يمكن أيضًا نقل العناصر التلقائية (المتغيرات المحلية التي لم يتم الإعلان عنها على أنها static ) من الوظائف:

unique_ptr<Shape> make_square()
{
    unique_ptr<Shape> result(new Square);
    return result;   // note the missing std::move
}

كيف يقبل منشئ الخطوة قبول result lvalue كحجة؟ إن نطاق result على وشك الانتهاء ، وسيتم تدميره أثناء فك التراص. لا يمكن لأحد أن يشتكي بعد ذلك من أن result قد تغيرت بطريقة ما. عندما يعود تدفق التحكم إلى المتصل ، لا توجد result بعد الآن! ولهذا السبب ، يحتوي C ++ 11 على قاعدة خاصة تسمح بإعادة الكائنات التلقائية من الوظائف دون الاضطرار إلى كتابة std::move . في الواقع ، يجب عدم استخدام std::move لنقل الكائنات التلقائية من الدوال ، لأن ذلك يحول دون "تحسين قيمة الإرجاع المسمى" (NRVO).

لا تستخدم أبدا std::move لنقل الكائنات التلقائية من الدوال.

لاحظ أنه في كلا دالات المصنع ، يكون نوع الإرجاع عبارة عن قيمة ، وليس مرجع rvalue. لا تزال مراجع Rvalue هي المراجع ، وكما هو الحال دائمًا ، لا يجب عليك أبدًا إرجاع مرجع إلى كائن تلقائي. سينتهي المتصل بمرجع متدلل إذا خدعت المترجم ليقبل رمزك ، على النحو التالي:

unique_ptr<Shape>&& flawed_attempt()   // DO NOT DO THIS!
{
    unique_ptr<Shape> very_bad_idea(new Square);
    return std::move(very_bad_idea);   // WRONG!
}

عدم إعادة الكائنات التلقائية بواسطة مرجع rvalue. يتم تنفيذ النقل بشكل حصري بواسطة منشئ الخطوة ، وليس بواسطة std::move ، وليس فقط بربط rvalue بمرجع rvalue.

الانتقال إلى الأعضاء

عاجلاً أم آجلاً ، ستقوم بكتابة الكود على النحو التالي:

class Foo
{
    unique_ptr<Shape> member;

public:

    Foo(unique_ptr<Shape>&& parameter)
    : member(parameter)   // error
    {}
};

أساسا ، سوف يشتكي المترجم أن parameter هي lvalue. إذا نظرت إلى نوعه ، سترى مرجعًا rvalue ، ولكن يشير مرجع rvalue ببساطة إلى "مرجع مرتبط بـ rvalue" ؛ هذا لا يعني أن المرجع نفسه هو rvalue! في الواقع ، parameter هي مجرد متغير عادي باسم. يمكنك استخدام parameter بقدر ما تريد داخل نص المنشئ ، ودائمًا ما يشير إلى نفس الكائن. من المؤكد أن الانتقال منه سيكون خطيراً ، ومن ثم فإن اللغة تمنعه.

مرجع rvalue مسميًا هو lvalue ، تمامًا مثل أي متغير آخر.

الحل هو تمكين الحركة يدويًا:

class Foo
{
    unique_ptr<Shape> member;

public:

    Foo(unique_ptr<Shape>&& parameter)
    : member(std::move(parameter))   // note the std::move
    {}
};

يمكنك أن تجادل بأن parameter لم تعد تستخدم بعد تهيئة member . لماذا لا توجد قاعدة خاصة لإدراج std::move بصمت كما هو الحال مع قيم الإرجاع؟ ربما لأنه سيكون عبئا كبيرا على منفذي المجمع. على سبيل المثال ، ماذا لو كان جهاز الإنشاء في وحدة ترجمة أخرى؟ وعلى النقيض من ذلك ، فإن قاعدة قيمة الإرجاع يجب عليها ببساطة أن تتحقق من جداول الرموز لتحديد ما إذا كان المعرف بعد الكلمة الرئيسية return يشير إلى كائن تلقائي أم لا.

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

وظائف العضو الخاص

يعلن C ++ 98 ضمنيًا عن ثلاث وظائف خاصة بالأعضاء عند الطلب ، أي عندما تكون هناك حاجة إليها في مكان ما: مُنشئ النسخ ومُشغل تخصيص النسخة والمُدمِّر.

X::X(const X&);              // copy constructor
X& X::operator=(const X&);   // copy assignment operator
X::~X();                     // destructor

مرت مراجع Rvalue بعدة إصدارات. منذ الإصدار 3.0 ، يعلن C ++ 11 عن وظيفتي عضو خاصتين إضافيتين عند الطلب: منشئ الخطوة وعامل تعيين النقل. لاحظ أنه لا VC10 ولا VC11 يتطابقان مع الإصدار 3.0 حتى الآن ، لذلك يجب عليك تنفيذها بنفسك.

X::X(X&&);                   // move constructor
X& X::operator=(X&&);        // move assignment operator

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

ماذا تعني هذه القواعد في الممارسة؟

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

لاحظ أنه يمكن دمج مشغل تخصيص النسخة ومشغل عامل النقل في مشغل تخصيص مفرد وموحد ، مع الأخذ في الاعتبار قيمة:

X& X::operator=(X source)    // unified assignment operator
{
    swap(source);            // see my first answer for an explanation
    return *this;
}

بهذه الطريقة ، ينخفض ​​عدد الوظائف الخاصة بالأعضاء من خمسة إلى أربعة. هناك مقايضة بين سلامة الاستثناء والكفاءة هنا ، لكنني لست خبيرًا في هذه المسألة.

إعادة توجيه المراجع (المعروفة previously باسم المراجع العالمية )

خذ بعين الاعتبار قالب الدالة التالي:

template<typename T>
void foo(T&&);

قد تتوقع T&& أن ترتبط فقط بـ rvalues ​​، لأنه للوهلة الأولى ، يبدو مثل مرجع rvalue. وكما يتبين من ذلك ، فإن T&& يرتبط أيضًا بـ lvalues:

foo(make_triangle());   // T is unique_ptr<Shape>, T&& is unique_ptr<Shape>&&
unique_ptr<Shape> a(new Triangle);
foo(a);                 // T is unique_ptr<Shape>&, T&& is unique_ptr<Shape>&

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

T && ليس مرجعًا rvalue ، ولكنه مرجع إعادة توجيه. كما أنه يرتبط بالفرز ، وفي هذه الحالة يكون T و T&& هما مراجع كلاهما.

إذا كنت تريد تقييد قالب وظيفي على rvalues ​​، فيمكنك الجمع بين SFINAE وخصائص الكتابة:

#include <type_traits>

template<typename T>
typename std::enable_if<std::is_rvalue_reference<T&&>::value, void>::type
foo(T&&);

تنفيذ التحرك

الآن بعد أن فهمت الانهيار المرجعي ، إليك كيفية تنفيذ std::move :

template<typename T>
typename std::remove_reference<T>::type&&
move(T&& t)
{
    return static_cast<typename std::remove_reference<T>::type&&>(t);
}

كما ترى ، فإن move يقبل أي نوع من المعلمات بفضل مرجع إعادة التوجيه T&& ، ويعرض مرجع rvalue. يعد std::remove_reference<T>::type meta-function call ضروريًا لأنه بخلاف ذلك ، بالنسبة إلى lvalues ​​من النوع X ، سيكون نوع الإرجاع هو X& && ، والذي قد ينهار إلى X& . بما أن t دائمًا عبارة عن lvalue (تذكر أن مرجع rvalue مسميًا هو lvalue) ، ولكننا نريد ربط t بمرجع rvalue ، يجب علينا أن نرسم t إلى نوع الإرجاع الصحيح. استدعاء دالة تقوم بإرجاع مرجع rvalue هو بحد ذاته xvalue. الآن أنت تعرف من أين تأتي xvalues ​​؛)

استدعاء دالة تقوم بإرجاع مرجع rvalue ، مثل std::move ، هو xvalue.

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


Move semantics is about transferring resources rather than copying them when nobody needs the source value anymore.

In C++03, objects are often copied, only to be destroyed or assigned-over before any code uses the value again. For example, when you return by value from a function—unless RVO kicks in—the value you're returning is copied to the caller's stack frame, and then it goes out of scope and is destroyed. This is just one of many examples: see pass-by-value when the source object is a temporary, algorithms like sort that just rearrange items, reallocation in vector when its capacity() is exceeded, etc.

When such copy/destroy pairs are expensive, it's typically because the object owns some heavyweight resource. For example, vector<string> may own a dynamically-allocated memory block containing an array of string objects, each with its own dynamic memory. Copying such an object is costly: you have to allocate new memory for each dynamically-allocated blocks in the source, and copy all the values across. Then you need deallocate all that memory you just copied. However, moving a large vector<string> means just copying a few pointers (that refer to the dynamic memory block) to the destination and zeroing them out in the source.


If you are really interested in a good, in-depth explanation of move semantics, I'd highly recommend reading the original paper on them, "A Proposal to Add Move Semantics Support to the C++ Language."

It's very accessible and easy to read and it makes an excellent case for the benefits that they offer. There are other more recent and up to date papers about move semantics available on the WG21 website , but this one is probably the most straightforward since it approaches things from a top-level view and doesn't get very much into the gritty language details.


In easy (practical) terms:

Copying an object means copying its "static" members and calling the new operator for its dynamic objects. حق؟

class A
{
   int i, *p;

public:
   A(const A& a) : i(a.i), p(new int(*a.p)) {}
   ~A() { delete p; }
};

However, to move an object (I repeat, in a practical point of view) implies only to copy the pointers of dynamic objects, and not to create new ones.

But, is that not dangerous? Of course, you could destruct a dynamic object twice (segmentation fault). So, to avoid that, you should "invalidate" the source pointers to avoid destructing them twice:

class A
{
   int i, *p;

public:
   // Movement of an object inside a copy constructor.
   A(const A& a) : i(a.i), p(a.p)
   {
     a.p = nullptr; // pointer invalidated.
   }

   ~A() { delete p; }
   // Deleting NULL, 0 or nullptr (address 0x0) is safe. 
};

Ok, but if I move an object, the source object becomes useless, no? Of course, but in certain situations that's very useful. The most evident one is when I call a function with an anonymous object (temporal, rvalue object, ..., you can call it with different names):

void heavyFunction(HeavyType());

In that situation, an anonymous object is created, next copied to the function parameter, and afterwards deleted. So, here it is better to move the object, because you don't need the anonymous object and you can save time and memory.

This leads to the concept of an "rvalue" reference. They exist in C++11 only to detect if the received object is anonymous or not. I think you do already know that an "lvalue" is an assignable entity (the left part of the = operator), so you need a named reference to an object to be capable to act as an lvalue. A rvalue is exactly the opposite, an object with no named references. Because of that, anonymous object and rvalue are synonyms. وبالتالي:

class A
{
   int i, *p;

public:
   // Copy
   A(const A& a) : i(a.i), p(new int(*a.p)) {}

   // Movement (&& means "rvalue reference to")
   A(A&& a) : i(a.i), p(a.p)
   {
      a.p = nullptr;
   }

   ~A() { delete p; }
};

In this case, when an object of type A should be "copied", the compiler creates a lvalue reference or a rvalue reference according to if the passed object is named or not. When not, your move-constructor is called and you know the object is temporal and you can move its dynamic objects instead of copying them, saving space and memory.

It is important to remember that "static" objects are always copied. There's no ways to "move" a static object (object in stack and not on heap). So, the distinction "move"/ "copy" when an object has no dynamic members (directly or indirectly) is irrelevant.

If your object is complex and the destructor has other secondary effects, like calling to a library's function, calling to other global functions or whatever it is, perhaps is better to signal a movement with a flag:

class Heavy
{
   bool b_moved;
   // staff

public:
   A(const A& a) { /* definition */ }
   A(A&& a) : // initialization list
   {
      a.b_moved = true;
   }

   ~A() { if (!b_moved) /* destruct object */ }
};

So, your code is shorter (you don't need to do a nullptr assignment for each dynamic member) and more general.

Other typical question: what is the difference between A&& and const A&& ? Of course, in the first case, you can modify the object and in the second not, but, practical meaning? In the second case, you can't modify it, so you have no ways to invalidate the object (except with a mutable flag or something like that), and there is no practical difference to a copy constructor.

And what is perfect forwarding ? It is important to know that a "rvalue reference" is a reference to a named object in the "caller's scope". But in the actual scope, a rvalue reference is a name to an object, so, it acts as a named object. If you pass an rvalue reference to another function, you are passing a named object, so, the object isn't received like a temporal object.

void some_function(A&& a)
{
   other_function(a);
}

The object a would be copied to the actual parameter of other_function . If you want the object a continues being treated as a temporary object, you should use the std::move function:

other_function(std::move(a));

With this line, std::move will cast a to an rvalue and other_function will receive the object as a unnamed object. Of course, if other_function has not specific overloading to work with unnamed objects, this distinction is not important.

Is that perfect forwarding? Not, but we are very close. Perfect forwarding is only useful to work with templates, with the purpose to say: if I need to pass an object to another function, I need that if I receive a named object, the object is passed as a named object, and when not, I want to pass it like a unnamed object:

template<typename T>
void some_function(T&& a)
{
   other_function(std::forward<T>(a));
}

That's the signature of a prototypical function that uses perfect forwarding, implemented in C++11 by means of std::forward . This function exploits some rules of template instantiation:

 `A& && == A&`
 `A&& && == A&&`

So, if T is a lvalue reference to A ( T = A&), a also ( A& && => A&). If T is a rvalue reference to A , a also (A&& && => A&&). In both cases, a is a named object in the actual scope, but T contains the information of its "reference type" from the caller scope's point of view. This information ( T ) is passed as template parameter to forward and 'a' is moved or not according to the type of T .


To illustrate the need for move semantics , let's consider this example without move semantics:

Here's a function that takes an object of type T and returns an object of the same type T :

T f(T o) { return o; }
  //^^^ new object constructed

The above function uses call by value which means that when this function is called an object must be constructed to be used by the function.
Because the function also returns by value , another new object is constructed for the return value:

T b = f(a);
  //^ new object constructed

Two new objects have been constructed, one of which is a temporary object that's only used for the duration of the function.

When the new object is created from the return value, the copy constructor is called to copy the contents of the temporary object to the new object b. After the function completes, the temporary object used in the function goes out of scope and is destroyed.

Now, let's consider what a copy constructor does.

It must first initialize the object, then copy all the relevant data from the old object to the new one.
Depending on the class, maybe its a container with very much data, then that could represent much time and memory usage

// Copy constructor
T::T(T &old) {
    copy_data(m_a, old.m_a);
    copy_data(m_b, old.m_b);
    copy_data(m_c, old.m_c);
}

With move semantics it's now possible to make most of this work less unpleasant by simply moving the data rather than copying.

// Move constructor
T::T(T &&old) noexcept {
    m_a = std::move(old.m_a);
    m_b = std::move(old.m_b);
    m_c = std::move(old.m_c);
}

Moving the data involves re-associating the data with the new object. And no copy takes place at all.

This is accomplished with an rvalue reference.
An rvalue reference works pretty much like an lvalue reference with one important difference:
an rvalue reference can be moved and an lvalue cannot.

From cppreference.com :

لجعل استثناء الاستثناء قوية محتملة ، يجب أن المبنيين التنقل المعرفة من قبل المستخدم لا رمي الاستثناءات. In fact, standard containers typically rely on std::move_if_noexcept to choose between move and copy when container elements need to be relocated. If both copy and move constructors are provided, overload resolution selects the move constructor if the argument is an rvalue (either a prvalue such as a nameless temporary or an xvalue such as the result of std::move), and selects the copy constructor if the argument is an lvalue (named object or a function/operator returning lvalue reference). إذا تم توفير مُنشئ النسخ فقط ، فستقوم جميع فئات الوسيط بتحديده (طالما أنه يأخذ إشارة إلى المرجع ، حيث يمكن ربط rvalues ​​بمراجع const) ، مما يجعل النسخ الاحتياطي للحركة ، عندما يكون النقل غير متاح. In many situations, move constructors are optimized out even if they would produce observable side-effects, see copy elision. يُطلق على المُنشئ "مُنشئ الحركات" عندما يأخذ مرجعًا كمرجع. لا يلزم نقل أي شيء ، ولا يُطلب من الصف أن يكون هناك مورد يتم نقله ، وقد لا يتمكن "ناقل الحركة" من نقل مورد كما هو الحال في الحالة المسموح بها (ولكن ربما ليست معقولة) حيث تكون المعلمة مرجع const rvalue (const T &&).


You know what a copy semantics means right? it means you have types which are copyable, for user-defined types you define this either buy explicitly writing a copy constructor & assignment operator or the compiler generates them implicitly. This will do a copy.

Move semantics is basically a user-defined type with constructor that takes an r-value reference (new type of reference using && (yes two ampersands)) which is non-const, this is called a move constructor, same goes for assignment operator. So what does a move constructor do, well instead of copying memory from it's source argument it 'moves' memory from the source to the destination.

When would you want to do that? well std::vector is an example, say you created a temporary std::vector and you return it from a function say:

std::vector<foo> get_foos();

You're going to have overhead from the copy constructor when the function returns, if (and it will in C++0x) std::vector has a move constructor instead of copying it can just set it's pointers and 'move' dynamically allocated memory to the new instance. It's kind of like transfer-of-ownership semantics with std::auto_ptr.





move-semantics