هل يجب أن تصبح لغة النسخ والتبديل هي لغة النسخ والنقل في C++ 11؟

StackOverflow https://stackoverflow.com//questions/24014130

سؤال

كما هو موضح في هذه الإجابة, ، يتم تنفيذ لغة النسخ والتبديل على النحو التالي:

class MyClass
{
private:
    BigClass data;
    UnmovableClass *dataPtr;

public:
    MyClass()
      : data(), dataPtr(new UnmovableClass) { }
    MyClass(const MyClass& other)
      : data(other.data), dataPtr(new UnmovableClass(*other.dataPtr)) { }
    MyClass(MyClass&& other)
      : data(std::move(other.data)), dataPtr(other.dataPtr)
    { other.dataPtr= nullptr; }

    ~MyClass() { delete dataPtr; }

    friend void swap(MyClass& first, MyClass& second)
    {
        using std::swap;
        swap(first.data, other.data);
        swap(first.dataPtr, other.dataPtr);
    }

    MyClass& operator=(MyClass other)
    {
        swap(*this, other);
        return *this;
    }
};

من خلال وجود قيمة MyClass كمعلمة للمشغل=، يمكن إنشاء المعلمة إما بواسطة مُنشئ النسخ أو مُنشئ النقل.يمكنك بعد ذلك استخراج البيانات بأمان من المعلمة.وهذا يمنع تكرار التعليمات البرمجية ويساعد في سلامة الاستثناءات.

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

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

class MyClass : Observable::IObserver
{
private:
    std::shared_ptr<Observable> observable;

public:
    MyClass(std::shared_ptr<Observable> observable) : observable(observable){ observable->registerObserver(*this); }
    MyClass(const MyClass& other) : observable(other.observable) { observable.registerObserver(*this); }
    ~MyClass() { if(observable != nullptr) { observable->unregisterObserver(*this); }}

    MyClass(MyClass&& other) : observable(std::move(other.observable))
    {
        observable->unregisterObserver(other);
        other.observable.reset(nullptr);
        observable->registerObserver(*this);
    }

    friend void swap(MyClass& first, MyClass& second)
    {
        //Checks for nullptr and same observable omitted
            using std::swap;
            swap(first.observable, second.observable);

            second.observable->unregisterObserver(first);
            first.observable->registerObserver(first);
            first.observable->unregisterObserver(second);
            second.observable->registerObserver(second);
    }

    MyClass& operator=(MyClass other)
    {
        observable->unregisterObserver(*this);
        observable = std::move(other.observable);

        observable->unregisterObserver(other);
        other.observable.reset(nullptr);
        observable->registerObserver(*this);
    }
}

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

ألن يكون من المنطقي إعادة استخدام كود مُنشئ الحركة بدلاً من ذلك؟

private:
    void performMoveActions(MyClass&& other)
    {
        observable->unregisterObserver(other);
        other.observable.reset(nullptr);
        observable->registerObserver(*this);
    }

public:
    MyClass(MyClass&& other) : observable(std::move(other.observable))
    {
        performMoveActions(other);
    }

    MyClass& operator=(MyClass other)
    {
        observable->unregisterObserver(*this);
        observable = std::move(other.observable);

        performMoveActions(other);
    }

يبدو لي أن هذا النهج ليس أقل شأنا من نهج المقايضة.هل أنا على حق في الاعتقاد بأن لغة النسخ والتبديل ستكون أفضل حالًا مثل لغة النسخ والنقل في C++ 11، أم أنني فاتني شيئًا مهمًا؟

هل كانت مفيدة؟

المحلول 3

لقد مضى وقت طويل منذ أن سألت هذا السؤال، ولقد عرفت الإجابة لفترة من الوقت الآن، لكنني قمت بإيقاف الكتابة في إجابة ذلك. هنا هو.

الجواب لا. يجب ألا تصبح IDIOM للنسخ والتبديل عن النسخة والانتقال.

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

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

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

يقوم منشئ التحرك نفسه عادة لا يتطلب أي سلوك تنظيف، لأنه كائن جديد. يتمثل النهج البسيط العام في جعل منشئ التحرك استدعاء المنشئ الافتراضي، ثم قم بتبديل جميع الأعضاء مع "نقل". ستكون كائن تم نقله من كائن يعمل بشكل افتراضي بلاند.

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

نصائح أخرى

امنح كل عضو مميز الرعاية المحبة التي يستحقها، وحاول أن تتجاهله قدر الإمكان:

class MyClass
{
private:
    BigClass data;
    std::unique_ptr<UnmovableClass> dataPtr;

public:
    MyClass() = default;
    ~MyClass() = default;
    MyClass(const MyClass& other)
        : data(other.data)
        , dataPtr(other.dataPtr ? new UnmovableClass(*other.dataPtr)
                                : nullptr)
        { }
    MyClass& operator=(const MyClass& other)
    {
        if (this != &other)
        {
            data = other.data;
            dataPtr.reset(other.dataPtr ? new UnmovableClass(*other.dataPtr)
                                        : nullptr);
        }
        return *this;
    }
    MyClass(MyClass&&) = default;
    MyClass& operator=(MyClass&&) = default;

    friend void swap(MyClass& first, MyClass& second)
    {
        using std::swap;
        swap(first.data, second.data);
        swap(first.dataPtr, second.dataPtr);
    }
};

يمكن أن يتم التخلف عن التدمير ضمنيًا أعلاه إذا رغبت في ذلك.كل شيء آخر يحتاج إلى تعريف واضح أو افتراضي في هذا المثال.

مرجع: http://accu.org/content/conf2014/Howard_Hinnant_Accu_2014.pdf

من المحتمل أن تكلفك لغة النسخ/المبادلة الأداء (انظر الشرائح).على سبيل المثال، تساءلت يومًا عن سبب الأداء العالي/غالبًا ما تستخدم أنواع std::types مثل std::vector و std::string لا تستخدم النسخ/المبادلة؟الأداء الضعيف هو السبب.لو BigClass يحتوي على أي std::vectorق أو std::string(وهو ما يبدو مرجحًا)، فإن أفضل رهان لك هو الاتصال بأعضائهم المميزين من أعضائك المميزين.ما سبق هو كيفية القيام بذلك.

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

بادئ ذي بدء، ليس من الضروري عمومًا كتابة أ swap تعمل في C++ 11 طالما أن صفك قابل للنقل.الافتراضي swap سوف يلجأ إلى التحركات:

void swap(T& left, T& right) {
    T tmp(std::move(left));
    left = std::move(right);
    right = std::move(tmp);
}

وهذا كل شيء، يتم تبديل العناصر.

ثانيًا، بناءً على هذا، لا تزال عملية النسخ والتبديل سارية في الواقع:

T& T::operator=(T const& left) {
    using std::swap;
    T tmp(left);
    swap(*this, tmp);
    return *this;
}

// Let's not forget the move-assignment operator to power down the swap.
T& T::operator=(T&&) = default;

سيتم إما النسخ والتبديل (وهو نقل) أو النقل والتبديل (وهو نقل)، ويجب أن يحقق دائمًا أداءً قريبًا من الأداء الأمثل.قد يكون هناك بعض المهام الزائدة عن الحاجة، ولكن نأمل أن يتولى المترجم الخاص بك الأمر.

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

مرخصة بموجب: CC-BY-SA مع الإسناد
لا تنتمي إلى StackOverflow
scroll top