Перемістіть оператор призначення та `if (this! = & Rhs)`


126

В операторі присвоєння класу зазвичай потрібно перевірити, чи призначений об'єкт є об'єктом, що викликає, щоб ви не накручували речі:

Class& Class::operator=(const Class& rhs) {
    if (this != &rhs) {
        // do the assignment
    }

    return *this;
}

Вам потрібна те ж саме для оператора призначення переміщення? Чи буває колись ситуація, коли це this == &rhsбуло б правдою?

? Class::operator=(Class&& rhs) {
    ?
}

12
Незалежно від запитуваного питання Q, і просто так, щоб нові користувачі, які читали цей Q вниз за часовою шкалою (бо я знаю, що Сет це вже знає) не отримували неправильних ідей, Copy and Swap - це правильний спосіб впровадити Оператор призначення копіювання, у якому ви не потрібно перевіряти самостійне призначення і т.д.
Алок Зберегти

5
@VaughnCato : A a; a = std::move(a);.
Xeo

11
@VaughnCato Використання std::moveнормально. Тоді враховуйте псевдонім, і коли ви заглиблюєтесь у стек дзвінків, у вас є посилання на Tодне і інше посилання на T... чи збираєтесь ви перевірити наявність особи саме тут? Ви хочете знайти перший виклик (або дзвінки), коли документування того, що ви не можете передати один і той же аргумент двічі, статично доведе, що ці дві посилання не будуть псевдонімом? Або ви змусите самопризначення просто працювати?
Люк Дантон

2
@LucDanton Я вважаю за краще твердження в операторі призначення. Якщо std :: move використовувався таким чином, що можна було закінчити самовзначення rvalue, я вважав би це помилкою, яку слід виправити.
Вон Катон

4
@VaughnCato Одне місце, коли самообмін є нормальним, знаходиться всередині std::sortабо std::shuffle- будь-коли, коли ви поміняєте елементи ith і jth масиву, не перевіряючи i != jспочатку. ( std::swapреалізується з точки зору призначення переміщення.)
Quuxplusone

Відповіді:


143

Ого, тут просто так багато прибирати ...

По-перше, копіювання та заміна не завжди є правильним способом реалізації призначення копіювання. Майже напевно у випадкуdumb_array це - неоптимальне рішення.

Використання Copy and Swap призначено дляdumb_array це класичний приклад розміщення найдорожчої операції з найповнішими можливостями на нижньому шарі. Він ідеально підходить для клієнтів, які хочуть отримати найповнішу функцію та готові заплатити штраф за виконання. Вони отримують саме те, що хочуть.

Але це згубно для клієнтів, яким не потрібна найповніша функція і натомість шукають найвищу ефективність. Для них dumb_arrayце просто ще одна програма, яку вони повинні переписати, оскільки це занадто повільно. Була dumb_arrayрозроблена по-іншому, вона могла б задовольнити обох клієнтів без жодних компромісів з будь-яким клієнтом.

Ключ до задоволення обох клієнтів полягає в тому, щоб створити найшвидші операції на найнижчому рівні, а потім додати API поверх цього для більш повних функцій за більші витрати. Тобто вам потрібна сильна гарантія виключення, штрафу, ви платите за це. Вам це не потрібно? Ось більш швидке рішення.

Розберемося. Ось оперативна гарантія виключення базового виключення для dumb_array:

dumb_array& operator=(const dumb_array& other)
{
    if (this != &other)
    {
        if (mSize != other.mSize)
        {
            delete [] mArray;
            mArray = nullptr;
            mArray = other.mSize ? new int[other.mSize] : nullptr;
            mSize = other.mSize;
        }
        std::copy(other.mArray, other.mArray + mSize, mArray);
    }
    return *this;
}

Пояснення:

Однією з більш дорогих речей, яку ви можете зробити на сучасному обладнанні, є поїздка до купи. Все, що ви можете зробити, щоб уникнути поїздки в купу - це витрачений час і зусилля. Клієнти, dumb_arrayможливо, хочуть часто призначати масиви одного розміру. А коли вони це роблять, все, що вам потрібно зробити, - це memcpy(сховано підstd::copy ). Ви не хочете виділяти новий масив однакового розміру, а потім розмістити старий одного розміру!

Тепер для ваших клієнтів, які насправді бажають суворої безпеки виключень:

template <class C>
C&
strong_assign(C& lhs, C rhs)
{
    swap(lhs, rhs);
    return lhs;
}

А може, якщо ви хочете скористатися призначенням переміщення в C ++ 11, це повинно бути:

template <class C>
C&
strong_assign(C& lhs, C rhs)
{
    lhs = std::move(rhs);
    return lhs;
}

Якщо dumb_arrayклієнти цінують швидкість, вони повинні зателефонувати operator=. Якщо їм потрібна сильна безпека винятків, існують загальні алгоритми, які вони можуть викликати, які працюватимуть на найрізноманітніших об'єктах і їх потрібно впровадити лише один раз.

Тепер повернемось до початкового запитання (яке має тип-o на даний момент часу):

Class&
Class::operator=(Class&& rhs)
{
    if (this == &rhs)  // is this check needed?
    {
       // ...
    }
    return *this;
}

Це насправді спірне питання. Деякі скажуть так, абсолютно, інші скажуть ні.

Моя особиста думка - ні, ця перевірка вам не потрібна.

Обгрунтування:

Коли об'єкт прив'язується до посилання на оцінку, це одна з двох речей:

  1. Тимчасовий.
  2. Об'єкт, за яким абонент хоче, щоб ви повірили, є тимчасовим.

Якщо у вас є посилання на об'єкт, який є фактично тимчасовим, то за визначенням у вас є унікальна посилання на цей об’єкт. Це не може бути посилається ніде у всій вашій програмі. Тобто this == &temporary це неможливо .

Тепер, якщо ваш клієнт збрехав вам і пообіцяв вам, що ви отримаєте тимчасовий характер, коли ви цього не зробите, тоді клієнт повинен бути впевнений, що вам не доведеться піклуватися. Якщо ви хочете бути дуже обережними, я вважаю, що це буде кращою реалізацією:

Class&
Class::operator=(Class&& other)
{
    assert(this != &other);
    // ...
    return *this;
}

Тобто якщо ви будете передати посилання впевненості, що це помилка з боку клієнта , яка повинна бути виправлена.

Для повноти, ось оператор призначення ходу для dumb_array:

dumb_array& operator=(dumb_array&& other)
{
    assert(this != &other);
    delete [] mArray;
    mSize = other.mSize;
    mArray = other.mArray;
    other.mSize = 0;
    other.mArray = nullptr;
    return *this;
}

У типовому випадку використання присвоєння переміщення *thisбуде об'єктом, переміщеним з об'єкта, і таким чином delete [] mArray;повинен бути не-оп. Вкрай важливо, щоб реалізації робили видалення з nullptr якомога швидше.

Caveat:

Деякі будуть стверджувати, що swap(x, x)це гарна ідея чи просто необхідне зло. І це, якщо своп перейде до заміни за замовчуванням, може спричинити самостійне переміщення-призначення.

Я не згоден, що swap(x, x)це колись гарна ідея. Якщо його знайду у власному коді, я вважаю його помилкою продуктивності та виправляю. Але якщо ви хочете дозволити це, зрозумійте, що swap(x, x)тільки самостійне переміщення-assignemnet за значенням переміщеного значення. І в нашому dumb_arrayприкладі це буде абсолютно нешкідливо, якщо ми просто опустимо ствердження або обмежимо його випадком:

dumb_array& operator=(dumb_array&& other)
{
    assert(this != &other || mSize == 0);
    delete [] mArray;
    mSize = other.mSize;
    mArray = other.mArray;
    other.mSize = 0;
    other.mArray = nullptr;
    return *this;
}

Якщо ви самостійно призначите два переведені (порожні) dumb_array, ви не зробите нічого неправильного, окрім вставки непотрібних інструкцій у свою програму. Це ж спостереження можна зробити для переважної більшості об'єктів.

<Оновлення>

Я трохи більше задумався над цим питанням і дещо змінив свою позицію. Зараз я вважаю, що присвоєння повинно бути терпимим до самостійного призначення, але що умови публікації при призначенні копії та присвоєнні переміщення різні:

Для призначення копії:

x = y;

слід мати умову, згідно з якою значення yне слід змінювати. Коли &x == &yце постусловіем виливається: саме призначення копії не повинно мати жодного впливу на вартості x.

Для призначення переміщення:

x = std::move(y);

слід мати умову, яка yмає дійсний, але не визначений стан. Після &x == &yцього ця постумова перекладається на: xмає дійсний, але не визначений стан. Тобто завдання самостійного переміщення не повинно бути неоперативним. Але воно не повинно зазнати краху. Ця умова відповідає тому, що дозволяє swap(x, x)просто працювати:

template <class T>
void
swap(T& x, T& y)
{
    // assume &x == &y
    T tmp(std::move(x));
    // x and y now have a valid but unspecified state
    x = std::move(y);
    // x and y still have a valid but unspecified state
    y = std::move(tmp);
    // x and y have the value of tmp, which is the value they had on entry
}

Вищезазначене працює до тих пір, x = std::move(x)поки не вийде з ладу. Він може залишитись xу будь-якому дійсному, але не визначеному стані.

Я бачу три способи програмування оператора призначення переміщення для dumb_arrayдосягнення цього:

dumb_array& operator=(dumb_array&& other)
{
    delete [] mArray;
    // set *this to a valid state before continuing
    mSize = 0;
    mArray = nullptr;
    // *this is now in a valid state, continue with move assignment
    mSize = other.mSize;
    mArray = other.mArray;
    other.mSize = 0;
    other.mArray = nullptr;
    return *this;
}

Вище реалізація допускає призначення власного, але *thisі в otherкінцевому підсумку нульового розміру масиву після виконання завдання самостійно рухатися, незалежно від того , що первісне значення *thisє. Це добре.

dumb_array& operator=(dumb_array&& other)
{
    if (this != &other)
    {
        delete [] mArray;
        mSize = other.mSize;
        mArray = other.mArray;
        other.mSize = 0;
        other.mArray = nullptr;
    }
    return *this;
}

Вищевказана реалізація допускає самопризначення так само, як робить оператор призначення копії, роблячи його неоперативним. Це теж добре.

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

Сказане вище нормально, лише якщо в ньому dumb_arrayнемає ресурсів, які слід негайно знищити. Наприклад, якщо єдиний ресурс - це пам'ять, вище сказане. Якщо dumb_arrayможливо, можливо, зберігати мутексні блокування або відкритий стан файлів, клієнт може обґрунтовано очікувати, що ці ресурси, що знаходяться в літах передачі переміщення, будуть негайно випущені, і тому ця реалізація може бути проблематичною.

Вартість першого - два додаткові магазини. Вартість другого - це тест-галузь. Обидва працюють. Вони відповідають усім вимогам таблиці 22 MoveAssignable вимогам у стандарті C ++ 11. Третя також працює за модулем, що не стосується пам'яті-ресурсу.

Усі три реалізації можуть мати різні витрати залежно від обладнання: Наскільки дорога філія? Чи багато реєстрів чи дуже мало?

Винос полягає в тому, що самостійне переміщення-призначення, на відміну від самостійного копіювання-призначення, не повинно зберігати поточне значення.

</ Оновлення>

Один фінальний (сподіваюсь) редактор, натхненний коментарем Люка Дантона:

Якщо ви пишете клас високого рівня, який не керує безпосередньо пам’яттю (але може мати бази або члени, які це роблять), то найкраща реалізація призначення переміщення часто:

Class& operator=(Class&&) = default;

Це перемістить призначення кожної бази та кожного члена по черзі, і не буде включати this != &otherчек. Це дасть вам найбільш високу продуктивність та основну безпеку винятків, якщо за умови, що серед ваших баз та членів не потрібно підтримувати інваріантів. Для клієнтів, які вимагають суворої безпеки, виберіть їх strong_assign.


6
Я не знаю, як відчувати цю відповідь. Це робить вигляд, що реалізація таких класів (які дуже чітко керують їх пам'яттю) - звичайна справа. Це правда , що коли ви робите записи такий клас один повинен бути дуже дуже обережним про гарантії безпеки винятків і знайти солодке місце для інтерфейсу , щоб бути коротким , але зручно, але питання , як видається, просити загальний рада.
Люк Дантон

Так, я, безумовно, ніколи не використовую копіювання і заміну, тому що це марно витрачати час на заняття, які керують ресурсами та речами (навіщо йти і робити ще одну цілу копію всіх своїх даних?). І дякую, це відповідає на моє запитання.
Сет Карнегі

5
Запропонований припущенням про те, що переміщення-призначення-від-себе повинно коли - небудь стверджувати-відмовляти або створювати "не визначений" результат. Присвоєння самостійно - це буквально найпростіший випадок отримати правильний випадок . Якщо ваш клас виходить з ладу std::swap(x,x), то чому я повинен довіряти йому, щоб правильно обробляти складніші операції?
Квомплусон

1
@Quuxplusone: Я дійшов згоди з вами щодо затвердження-відмови, як зазначено в оновленні моєї відповіді. Що стосується цього std::swap(x,x), він просто працює навіть тоді, коли x = std::move(x)дає неозначений результат. Спробуй це! Не треба мені вірити.
Говард Хінант

@HowardHinnant хороший момент, swapпрацює до тих пір, поки не x = move(x)залишиться xв будь-якому стані, який переходить у стан. І std::copy/std::move алгоритми визначені таким чином, щоб виробляти невизначене поведінку в неоперативних копіях (ой; 20-річний підліток memmoveотримує тривіальний випадок правильно, але std::moveні!). Тож я здогадуюсь, я ще не думав про "занурення" для самостійного призначення. Але очевидно, що самопризначення - це те, що трапляється багато в реальному коді, незалежно від того, благословив його Стандарт чи ні.
Квоксплузон

11

По-перше, ви неправильно підписали оператора присвоєння переміщення. Оскільки переміщається крадіжка ресурсів у вихідного об'єкта, джерело повинно бути не constпосиланням на значення.

Class &Class::operator=( Class &&rhs ) {
    //...
    return *this;
}

Зауважте, що ви все одно повертаєтесь через (не const) l- значення.

Для будь-якого типу прямого призначення стандарт не повинен перевіряти самопризначення, а переконуватись у тому, що самопризначення не спричинить аварійне завершення роботи. Як правило, ніхто явно не робить x = xабо y = std::move(y)дзвінки, але згладжування, особливо через кілька функцій, може привести a = bабо c = std::move(d)до буття само-завдань. Явна перевірка самоприсвоєння, тобто this == &rhs, що пропускає м'ясо функції, коли true - це один із способів забезпечити безпеку самопризначення. Але це один з найгірших способів, оскільки він оптимізує (сподіваємось) рідкісний випадок, тоді як це анти-оптимізацію для більш поширеного випадку (через розгалуження та, можливо, пропуски кешу).

Тепер, коли (принаймні) один з операндів є безпосередньо тимчасовим об'єктом, ви ніколи не можете мати сценарій самопризначення. Деякі люди виступають за припущення цього випадку та оптимізують код для нього настільки, що код стає суїцидально дурним, коли припущення невірно. Я кажу, що демпінгова перевірка одного об’єкта на користувачів - безвідповідальна. Ми не робимо цей аргумент для присвоєння копії; навіщо змінювати позицію для переміщення-призначення?

Зробимо приклад, змінений з іншого респондента:

dumb_array& dumb_array::operator=(const dumb_array& other)
{
    if (mSize != other.mSize)
    {
        delete [] mArray;
        mArray = nullptr;  // clear this...
        mSize = 0u;        // ...and this in case the next line throws
        mArray = other.mSize ? new int[other.mSize] : nullptr;
        mSize = other.mSize;
    }
    std::copy(other.mArray, other.mArray + mSize, mArray);
    return *this;
}

Це призначення копії обробляє самопризначення витончено без явної перевірки. Якщо розміри джерела та місця призначення різняться, то розмежування та перерозподіл передують копіюванню. В іншому випадку робиться просто копіювання. Самопризначення не отримує оптимізованого шляху, воно потрапляє в той самий шлях, що і коли розміри джерела та місця призначення починаються рівними. Копіювання технічно не є необхідним, коли два об'єкти є рівнозначними (у тому числі, коли вони є одним і тим же об'єктом), але це ціна, коли не робити перевірку рівності (значення або значення адреси), оскільки зазначена перевірка сама по собі була б найбільшою витратою того часу. Зауважте, що тут самопризначення об'єкта спричинить ряд самоназначень на рівні елементів; тип елемента повинен бути безпечним для цього.

Як і його вихідний приклад, це призначення копії забезпечує основну гарантію безпеки виключення. Якщо ви хочете отримати гарантію, скористайтеся оператором уніфікованого призначення з оригінального запиту Copy and Swap , який обробляє як копіювання, так і переміщення-призначення. Але сенс цього прикладу - знизити безпеку на один ранг, щоб набрати швидкість. (BTW, ми припускаємо, що значення окремих елементів є незалежними; немає інваріантного обмеження, яке обмежує деякі значення порівняно з іншими.)

Давайте розглянемо переміщення-призначення для цього ж типу:

class dumb_array
{
    //...
    void swap(dumb_array& other) noexcept
    {
        // Just in case we add UDT members later
        using std::swap;

        // both members are built-in types -> never throw
        swap( this->mArray, other.mArray );
        swap( this->mSize, other.mSize );
    }

    dumb_array& operator=(dumb_array&& other) noexcept
    {
        this->swap( other );
        return *this;
    }
    //...
};

void  swap( dumb_array &l, dumb_array &r ) noexcept  { l.swap( r ); }

Тип, що може бути замінений, що потребує налаштування, повинен мати функцію без аргументів, яка викликається swapв тому ж просторі імен, що і тип. (Обмеження простору імен дозволяє некваліфікованим дзвінкам заміняти роботу.) Тип контейнера також повинен додавати функцію громадського swapчлена, щоб відповідати стандартним контейнерам. Якщо членswap не надається, вільну функцію, swapмабуть, потрібно позначити як товариша типу "swap". Якщо ви налаштовуєте ходи для використання swap, тоді вам слід надати власний код обміну; стандартний код називає код переміщення типу, що призведе до нескінченної взаємної рекурсії для типів, налаштованих на переміщення.

Як і деструктори, функції заміни та операції переміщення повинні бути ніколи не кидатися, якщо це можливо, і, ймовірно, позначатися як такі (у C ++ 11). Стандартні типи бібліотек та підпрограми мають оптимізацію для рухомих типів, що не перекидаються.

Ця перша версія переміщення-виконання відповідає основному договору. Маркери ресурсів джерела передаються об'єкту призначення. Старі ресурси не будуть протікати, оскільки вихідний об'єкт зараз ними керує. І вихідний об’єкт залишається у використаному стані, де до нього можуть бути застосовані подальші операції, включаючи призначення та знищення.

Зауважте, що це переміщення-призначення автоматично безпечне для самопризначення, оскільки swap дзвінок є. Це також категорично безпечний виняток. Проблема полягає в непотрібному збереженні ресурсів. Старі ресурси для місця призначення вже не потрібні, але тут вони все ще існують лише для того, щоб вихідний об'єкт міг залишатись дійсним. Якщо заплановане знищення вихідного об’єкта далеке, ми витрачаємо ресурсний простір, або ще гірше, якщо загальний ресурсний простір обмежений і інші петиції щодо ресурсів відбудуться до того, як (новий) вихідний об'єкт офіційно загине.

Це питання викликало суперечливі поточні поради щодо гуру щодо самонацілювання під час переміщення. Спосіб написання завдання переміщення без затримки ресурсів є чимось таким:

class dumb_array
{
    //...
    dumb_array& operator=(dumb_array&& other) noexcept
    {
        delete [] this->mArray;  // kill old resources
        this->mArray = other.mArray;
        this->mSize = other.mSize;
        other.mArray = nullptr;  // reset source
        other.mSize = 0u;
        return *this;
    }
    //...
};

Джерело скидається до умов за замовчуванням, тоді як старі ресурси призначення знищуються. У випадку самопризначення ваш поточний об'єкт закінчується самогубством. Основний спосіб навколо цього - оточити код дії if(this != &other)блоком або накрутити його і дати клієнтам їсти assert(this != &other)початковий рядок (якщо ви відчуваєте себе добре).

Альтернатива - вивчити, як зробити присвоєння копії сильно винятком безпечним, без уніфікованого призначення, і застосувати його до переміщення-призначення:

class dumb_array
{
    //...
    dumb_array& operator=(dumb_array&& other) noexcept
    {
        dumb_array  temp{ std::move(other) };

        this->swap( temp );
        return *this;
    }
    //...
};

Коли otherі thisвідрізняються, otherспорожняється ходом tempі залишається таким. Тоді він thisвтрачає свої старі ресурси temp, отримуючи ресурси, які спочатку були у власності other. Тоді старі ресурси thisвбивають, коли є temp.

Коли трапляється самопризначення, спорожнення otherтакож tempспорожняються this. Тоді цільовий об’єкт повертає свої ресурси під час tempта thisобміну. Смерть tempпретендує на порожній об’єкт, який повинен бути практично беззахисним. this/ otherОб'єкт зберігає свої ресурси.

Присвоєння ходу не повинно бути кинутим до тих пір, поки є також побудова та переміщення. Вартість безпеки також під час самостійного присвоєння - це ще кілька інструкцій щодо типів низького рівня, які слід вимкнути за допомогою виклику про угоду.


Чи потрібно перевірити, чи виділено пам'ять перед тим, як зателефонувати deleteу ваш другий блок коду?
користувач3728501

3
Ваш другий зразок коду, оператор присвоєння копії без перевірки самопризначення, помиляється. std::copyвикликає не визначене поведінку, якщо діапазон джерела та місця призначення перетинаються (включаючи випадок, коли вони збігаються). Див. C ++ 14 [alg.copy] / 3.
ММ

6

Я перебуваю в таборі тих, хто хоче самостійно призначити безпечні оператори, але не хочу писати чеки самопризначення у реалізаціях operator=. А насправді я взагалі не хочу реалізовувати operator=, я хочу, щоб поведінка за замовчуванням працювала «прямо з коробки». Кращі спеціальні члени - це ті, хто приходить безкоштовно.

При цьому вимоги MoveAssignable, присутні в Стандарті, описані наступним чином (з 17.6.3.1 Вимоги до аргументу шаблону [utility.arg.requirements], n3290):

Вираз Тип повернення Повернене значення Пост
t = rv T & tt еквівалентно значенню rv перед призначенням

де заповнювачі описуються як: " t[є модифікованим значенням типу T;" і " rvє ревальвацією типу T;". Зауважте, що це вимоги, що ставляться до типів, що використовуються як аргументи до шаблонів бібліотеки Standard, але дивлячись в іншому місці стандарту, я помічаю, що кожна вимога щодо призначення переміщення схожа на цю.

Це означає, що a = std::move(a)він повинен бути "безпечним". Якщо вам потрібно тест на ідентичність (наприклад this != &other), тоді займіться його, інакше ви навіть не зможете поставити свої об’єкти std::vector! (Якщо ви не використовуєте ці елементи / операції , які вимагають MoveAssignable, але фігу , що.) Зверніть увагу , що і в попередньому прикладі a = std::move(a), тоді this == &otherдійсно буде тримати.


Чи можете ви пояснити, як a = std::move(a)не працюючи, щоб клас не працював std::vector? Приклад?
Пол Дж. Лукас

@ PaulJ.Lucas Дзвінки std::vector<T>::eraseзаборонені, якщо не Tвстановлено MoveAssignable. (Окрім IIRC, деякі вимоги MoveAssignable були послаблені замість MoveInsertable замість C ++ 14.)
Люк Дантон

Гаразд, так Tмає бути MoveAssignable, але навіщо erase()коли-небудь залежати від переміщення елемента до себе ?
Пол Дж. Лукас

@ PaulJ.Lucas На це питання немає задоволеної відповіді. Все зводиться до "не розривати контракти".
Люк Дантон

2

Коли ваша поточна operator=функція написана, оскільки ви створили аргумент rvalue-посилання const, ви не можете "вкрасти" покажчики та змінити значення вхідної посилання rvalue ... ви просто не можете її змінити, ви міг лише читати з нього. Я б бачив проблему лише в тому випадку, якщо ви почали б дзвонити deleteна покажчики тощо у вашому thisоб'єкті, як у звичайному методі lvaue-reference operator=, але такий тип перемагає точку rvalue-версії ... тобто, це буде здається зайвим використовувати версію rvalue, щоб в основному робити ті самі операції, як правило, залишені методу const-lvalue operator=.

Тепер, якщо ви визначили, що ви operator=повинні взяти не- constоціночну посилання, то єдиний спосіб, коли я міг побачити необхідну перевірку, був, якщо ви передали thisоб'єкт функції, яка навмисно повертала посилання на оцінку, а не тимчасову.

Наприклад, припустимо, що хтось намагався написати operator+функцію та використовувати суміш посилань rvalue та посилань на lvalue, щоб "запобігти" створенню додаткових тимчасових під час деякої операції складання на типі об'єкта:

struct A; //defines operator=(A&& rhs) where it will "steal" the pointers
          //of rhs and set the original pointers of rhs to NULL

A&& operator+(A& rhs, A&& lhs)
{
    //...code

    return std::move(rhs);
}

A&& operator+(A&& rhs, A&&lhs)
{
    //...code

    return std::move(rhs);
}

int main()
{
    A a;

    a = (a + A()) + A(); //calls operator=(A&&) with reference bound to a

    //...rest of code
}

Тепер, з того, що я розумію щодо посилань на rvalue, робити вищезазначене не рекомендується (тобто. Ви повинні просто повернути тимчасову, а не оцінювальну посилання), але, якщо хтось все-таки це зробив, то ви хочете перевірити, щоб зробити переконайтеся, що вхідна rvalue-посилання не посилається на той самий об'єкт, що і thisвказівник.


Зауважте, що "a = std :: move (a)" є тривіальним способом створення цієї ситуації. Ваша відповідь правдива.
Вон Катон

1
Цілком погоджуюся, що це найпростіший спосіб, хоча я думаю, що більшість людей це не робитиме навмисно :-) ... Майте на увазі, що якщо rvalue-посилання є const, то ви можете читати лише з нього, тому єдине потрібно зробити перевірку було б, якби ви вирішили operator=(const T&&)виконати таку ж повторну ініціалізацію, thisяку ви робили типовим operator=(const T&)методом, а не операцією в стилі підміни (тобто крадіжкою покажчиків тощо, а не створенням глибоких копій).
Джейсон

1

Моя відповідь все ще полягає в тому, що завдання переміщення не потрібно врятувати від самопризначення, але воно має інше пояснення. Розглянемо std :: unique_ptr. Якби я реалізував це, я зробив би щось подібне:

unique_ptr& operator=(unique_ptr&& x) {
  delete ptr_;
  ptr_ = x.ptr_;
  x.ptr_ = nullptr;
  return *this;
}

Якщо ви подивитеся на Скотта Майєрса, який пояснює це, він робить щось подібне. (Якщо ти блукаєш, чому б не зробити своп - у неї є одна додаткова запис). І це не безпечно для самостійного призначення.

Іноді це прикро. Подумайте про виведення з вектора всіх парних чисел:

src.erase(
  std::partition_copy(src.begin(), src.end(),
                      src.begin(),
                      std::back_inserter(even),
                      [](int num) { return num % 2; }
                      ).first,
  src.end());

Для цілих чисел це нормально, але я не вірю, що ви можете зробити щось подібне до роботи з семантикою переміщення.

На закінчення: переміщення призначення об'єкту само по собі не нормально, і ви повинні стежити за ним.

Невелике оновлення.

  1. Я не погоджуюся з Говардом, що погана ідея, але все ж - я вважаю, що призначення самостійного переміщення "переміщеним" об'єктам повинно працювати, бо swap(x, x)повинно працювати. Алгоритми люблять ці речі! Завжди приємно, коли кутовий корпус просто працює. (І я ще не бачу випадку, коли це не безкоштовно. Але це не означає, що його не існує).
  2. Ось як реалізується призначення присвоєння унікальних_птрів у libc ++: unique_ptr& operator=(unique_ptr&& u) noexcept { reset(u.release()); ...} Це безпечно для самостійного пересування.
  3. Основні Керівні принципи вважають, що це має бути нормально, щоб самостійно призначити переміщення.

0

Є ситуація, про яку я можу придумати (це == rhs) Для цього твердження: Myclass obj; std :: переміщення (obj) = std :: переміщення (obj)


Myclass obj; std :: переміщення (obj) = std :: переміщення (obj);
little_monster
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.