Переместить оператор присваивания и `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 - правильный способ реализовать Оператор присваивания Copy, в котором вы не нужно проверять самостоятельное назначение и т. д.
Alok Save
5
@VaughnCato: A a; a = std::move(a);.
Xeo
11
@VaughnCato Использование std::moveнормально. Затем примите во внимание псевдонимы, и когда вы находитесь глубоко внутри стека вызовов, и у вас есть одна ссылка T, а другая ссылка на T... собираетесь ли вы проверять личность прямо здесь? Вы хотите найти первый вызов (или вызовы), при котором документальное подтверждение того, что вы не можете передать один и тот же аргумент дважды, статически докажет, что эти две ссылки не будут псевдонимами? Или вы сделаете так, чтобы самостоятельное задание работало?
Люк Дантон
2
@LucDanton Я бы предпочел утверждение в операторе присваивания. Если бы std :: move использовался таким образом, чтобы можно было закончить с самоприсвоением rvalue, я бы счел это ошибкой, которую следует исправить.
Vaughn Cato
4
@VaughnCato Одно место, где самостоятельная замена является нормальным явлением, находится внутри std::sortor std::shuffle- каждый раз, когда вы меняете местами ith и jth элементы массива без предварительной проверки i != j. ( std::swapреализовано с точки зрения назначения
ходов

Ответы:

143

Вау, здесь столько всего нужно навести порядок ...

Во-первых, копирование и замена - не всегда правильный способ реализации назначения копирования. В случае с почти наверняка dumb_arrayэто неоптимальное решение.

Использование копирования и Обмена для 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;
}

На самом деле это спорный вопрос. Некоторые скажут "да", безусловно, некоторые скажут "нет".

Мое личное мнение - нет, эта проверка вам не нужна.

Обоснование:

Когда объект привязывается к ссылке rvalue, это одно из двух:

  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 как можно быстрее.

Предостережение:

Некоторые будут утверждать, что swap(x, x)это хорошая идея или просто необходимое зло. И это, если своп переходит в своп по умолчанию, может вызвать назначение самостоятельного перемещения.

Я не согласен , что swap(x, x)это когда - либо хорошая идея. Если обнаружится в моем собственном коде, я сочту это ошибкой производительности и исправлю. Но в случае, если вы хотите разрешить это, swap(x, x)знайте , что self-move-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
Проголосовали против предложения о том, что move-assignment-from-self должно когда - либо утверждать-сбой или давать "неопределенный" результат. Самостоятельное назначение - это буквально самый простой случай, в котором можно разобраться. Если ваш класс выйдет из строя std::swap(x,x), то почему я должен доверять ему в правильной обработке более сложных операций?
Quuxplusone
1
@Quuxplusone: Я согласился с вами в отношении assert-fail, как указано в обновлении моего ответа. На самом деле std::swap(x,x)он просто работает, даже если x = std::move(x)дает неопределенный результат. Попытайся! Вы не должны мне верить.
Ховард Хиннант,
@HowardHinnant хороший момент, swapработает до тех пор, пока x = move(x)выходит xв любом состоянии перехода в состояние. И алгоритмы std::copy/ std::moveопределены таким образом, чтобы уже производить неопределенное поведение на бездействующих копиях (ой; 20-летний memmoveпарень правильно понимает тривиальный случай, но std::moveне делает этого!). Так что, наверное, я еще не придумал «данк» для самостоятельного задания. Но очевидно, что самоназначение - это то, что часто случается в реальном коде, независимо от того, благословил это Стандарт или нет.
Quuxplusone
11

Во-первых, вы ошиблись подписью оператора присваивания перемещения. Поскольку перемещения крадут ресурсы у исходного объекта, источник должен быть constссылкой, не имеющей значения r.

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

Обратите внимание, что вы по-прежнему возвращаетесь через (не const) l -значную ссылку.

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

Теперь, когда (по крайней мере) один из операндов является непосредственно временным объектом, у вас никогда не может быть сценария самоназначения. Некоторые люди рекомендуют предположить этот случай и настолько оптимизировать код для него, что код становится самоубийственно глупым, когда предположение неверно. Я говорю, что безответственно перекладывать проверку одного и того же объекта на пользователей. Мы не приводим этот аргумент в пользу копирования-присваивания; зачем менять позицию для присвоения хода?

Приведем пример, измененный с другого респондента:

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;
}

Это копирование-присваивание изящно обрабатывает самоназначение без явной проверки. Если исходный и целевой размеры различаются, то копированию предшествует освобождение и перераспределение. В противном случае будет выполнено просто копирование. Самостоятельное назначение не получает оптимизированного пути, оно сбрасывается в тот же путь, что и при одинаковом исходном и целевом размерах. Копирование технически не нужно, когда два объекта эквивалентны (в том числе, когда они являются одним и тем же объектом), но это цена, когда не выполняется проверка равенства (по значению или по адресу), поскольку указанная проверка сама по себе будет пустой тратой. времени. Обратите внимание, что здесь самоприсвоение объекта вызовет серию самоназначений на уровне элемента; для этого тип элемента должен быть безопасным.

Как и его исходный пример, это присваивание копии обеспечивает основную гарантию безопасности исключений. Если вам нужна строгая гарантия, используйте оператор унифицированного присваивания из исходного запроса копирования и обмена , который обрабатывает как копирование , так и перемещение. Но суть этого примера в том, чтобы снизить безопасность на один уровень, чтобы набрать скорость. (Кстати, мы предполагаем, что значения отдельных элементов независимы; что нет инвариантного ограничения, ограничивающего одни значения по сравнению с другими.)

Давайте посмотрим на назначение ходов для этого же типа:

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, вы должны предоставить свой собственный код подкачки; стандартный код вызывает код перемещения типа, что приводит к бесконечной взаимной рекурсии для типов, настроенных для перемещения.

Как и деструкторы, функции подкачки и операции перемещения должны никогда не выбрасываться, если это вообще возможно, и, вероятно, помечены как таковые (в 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сохраняет свои ресурсы.

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

CTMacUser
источник
Вам нужно проверить, была ли выделена какая-либо память, прежде чем вызывать deleteвторой блок кода?
user3728501
3
Ваш второй пример кода, оператор копирования-присваивания без проверки самоприсваивания, неверен. std::copyвызывает неопределенное поведение, если диапазоны источника и назначения перекрываются (включая случай, когда они совпадают). См. C ++ 14 [alg.copy] / 3.
MM
6

Я нахожусь в лагере тех, кому нужны безопасные операторы с самоназначением, но я не хочу писать проверки самоназначения в реализациях operator=. И на самом деле я даже не хочу вообще реализовывать operator=, я хочу, чтобы поведение по умолчанию работало «прямо из коробки». Лучшие специальные участники - это те, которые приходят бесплатно.

При этом требования MoveAssignable, представленные в Стандарте, описаны следующим образом (из 17.6.3.1 Требования к аргументам шаблона [utility.arg.requirements], n3290):

Выражение Тип возвращаемого значения Возвращаемое значение Постусловие
t = rv T & tt эквивалентно значению rv до присвоения

где заполнители описываются как: « t[является] изменяемым lvalue типа T;» и " rvявляется значением типа T;". Обратите внимание, что это требования, предъявляемые к типам, используемым в качестве аргументов для шаблонов стандартной библиотеки, но, глядя в другое место в стандарте, я замечаю, что все требования при назначении перемещения аналогичны этому.

Это означает, что 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()вообще зависеть от перемещения элемента в себя ?
Paul J. Lucas
@ PaulJ.Lucas На этот вопрос нет удовлетворительного ответа. Все сводится к «не нарушать контракты».
Люк Дантон
2

Поскольку ваша текущая operator=функция написана, поскольку вы указали аргумент rvalue-reference const, вы не можете «украсть» указатели и изменить значения входящей ссылки rvalue ... вы просто не можете это изменить, вы мог только читать оттуда. Я бы увидел проблему только в том случае, если бы вы начали вызывать deleteуказатели и т. Д. В своем thisобъекте, как в обычном методе lvaue-reference operator=, но такого рода поражает точку rvalue-версии ... т.е. кажется излишним использовать версию rvalue для выполнения тех же операций, которые обычно constвыполняются operator=методом -lvalue .

Теперь, если вы определили, что operator=принимает constссылку, отличную от rvalue, то единственный способ увидеть, что требуется проверка, - это передать thisобъект функции, которая намеренно вернула ссылку rvalue, а не временную.

Например, предположим, что кто-то попытался написать 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), но, если кто-то все еще будет это делать, тогда вы захотите проверить, чтобы убедитесь, что входящая rvalue-ссылка не ссылается на тот же объект, что и thisуказатель.

Джейсон
источник
Обратите внимание, что «a = std :: move (a)» - тривиальный способ получить эту ситуацию. Но ваш ответ верен.
Vaughn Cato
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. Вот как реализовано назначение unique_ptrs в libc ++: unique_ptr& operator=(unique_ptr&& u) noexcept { reset(u.release()); ...} это безопасно для самостоятельного присвоения.
  3. Основные принципы считают, что самостоятельное перемещение назначения должно быть нормальным.
Денис Ярошевский
источник
0

Есть ситуация, о которой (это == rhs) я могу думать. Для этого утверждения: Myclass obj; std :: move (obj) = std :: move (obj)

маленький монстр
источник
Myclass obj; std :: move (obj) = std :: move (obj);
little_monster