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

В операторе присваивания класса вам обычно нужно проверить, является ли объект, назначаемый для вызова, вызывающим объектом, поэтому вы не заблуждаетесь:

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

    return *this;
}

Вам нужно то же самое для оператора присваивания переадресации? Есть ли когда-нибудь ситуация, когда this == &rhs будет истинным?

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

Ответ 1

Ничего себе, здесь просто так просто почистить...

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

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

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

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

Позвольте получить конкретное: здесь быстрый, основной исключающий оператор Copy Assignment для 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;
}

Или, может быть, если вы хотите использовать назначение переходов в С++ 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, это одна из двух вещей:

  • Временный.
  • Объект, который хочет, чтобы вы считали, является временным.

Если у вас есть ссылка на объект, который является фактическим временным, то по определению у вас есть уникальная ссылка на этот объект. На него нельзя ссылаться нигде во всей вашей программе. То есть 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; должен быть не-op. Очень важно, чтобы реализации делали удаление на nullptr как можно быстрее.

Протест:

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

Я не согласен с тем, что swap(x, x) является ever хорошей идеей. Если он найден в моем собственном коде, я рассмотрю его как ошибку производительности и исправлю ее. Но в случае, если вы хотите разрешить это, убедитесь, что 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, вы не делаете ничего неправильного, кроме вставки бесполезных инструкций в вашу программу. Это же наблюдение может быть сделано для подавляющего большинства объектов.

< Update >

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

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

x = y;

следует иметь пост-условие, что значение y не должно изменяться. Когда &x == &y, то это постусловие преобразуется в: само копирование не должно влиять на значение x.

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

x = std::move(y);

следует иметь пост-условие, что y имеет действительное, но неопределенное состояние. Когда &x == &y, то это постусловие означает: x имеет действительное, но неопределенное состояние. То есть самоперемещение не обязательно должно быть no-op. Но он не должен падать. Это пост-условие согласуется с тем, что позволяет 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 может содержать блокировки мьютекса или открытое состояние файлов, клиент мог бы разумно ожидать, что эти ресурсы в Lhs назначения переадресации будут немедленно выпущены, и поэтому эта реализация может быть проблематичной.

Стоимость первого - это два дополнительных магазина. Стоимость второго - это тест-ответ. Оба работают. Оба соответствуют всем требованиям Таблицы 22 Требования MoveAssignable в стандарте С++ 11. Третий также работает по модулю проблемы, не связанной с памятью.

Все три реализации могут иметь разные затраты в зависимости от оборудования: насколько дорогой является отрасль? Есть много регистров или очень мало?

Вывод заключается в том, что самоперемещение-присваивание, в отличие от самокопирования, не должно сохранять текущее значение.

</Update >

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

Если вы пишете класс высокого уровня, который напрямую не управляет памятью (но может иметь базы или члены, которые это делают), то наилучшая реализация назначения переноса часто:

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

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

Ответ 2

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

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

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

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

Посмотрите на присваивание move для этого же типа:

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

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

Эта первая версия move-assign выполняет основной контракт. Маршрутизаторы исходных ресурсов переносятся на объект назначения. Старые ресурсы не будут просачиваться, поскольку исходный объект теперь управляет ими. И исходный объект остается в пригодном для использования состоянии, где к нему могут применяться дополнительные операции, включая назначение и уничтожение.

Обратите внимание, что это присвоение переадресации автоматически безопасно для самонаправления, так как вызов 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;
    }
    //...
};

Источник reset по умолчанию, а старые ресурсы назначения уничтожены. В случае с самостоятельным назначением ваш текущий объект заканчивает самоубийство. Основной путь вокруг него состоит в том, чтобы окружить код действия блоком if(this != &other) или ввернуть его и позволить клиентам есть исходную строку assert(this != &other) (если вы чувствуете себя хорошо).

Альтернативой является изучение того, как сделать исключение для копирования исключительным безопасным без унифицированного присвоения и применить его к присваиванию move:

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 требует пустого объекта, который должен быть практически не-op. Объект this/other сохраняет свои ресурсы.

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

Ответ 3

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

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

Expression  Return type Return value    Post-condition
t = rv      T&          t               t is equivalent to the value of rv before the assignment

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

Это означает, что a = std::move(a) должен быть "безопасным". Если вам нужен тест идентичности (например, this != &other), перейдите на него, иначе вы даже не сможете разместить свои объекты в std::vector! (Если вы не используете те элементы/операции, которые требуют MoveAssignable, но не помните об этом.) Обратите внимание, что в предыдущем примере a = std::move(a), тогда this == &other действительно будет выполняться.

Ответ 4

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

Теперь, если вы определили operator=, чтобы взять ссылку на const rvalue-reference, то единственный способ, которым я мог видеть проверку, был, если вы передали объект 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-reference не ссылался на тот же объект, что и указатель this.

Ответ 5

Мой ответ по-прежнему заключается в том, что назначение переноса не нужно экономить на самоопределении, но оно имеет другое объяснение. Рассмотрим std:: unique_ptr. Если бы я был реализован, я бы сделал что-то вроде этого:

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

Если вы посмотрите на Скотт Майерс, объясняющий это, он делает что-то подобное. (Если вы блуждаете, почему бы не сделать swap - у него есть одна дополнительная запись). И это не безопасно для самостоятельного назначения.

Иногда это несчастливо. Рассмотрим перемещение из вектора всех четных чисел:

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

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

В заключение: переместить присвоение самому объекту нехорошо, и вы должны следить за ним.

Небольшое обновление.

  • Я не согласен с Говардом, это плохая идея, но все же - я думаю, что сам движется назначение "перемещенных" объектов должно работать, потому что swap(x, x) должно работать. Алгоритмы любят это! Всегда приятно, когда работает угловой корпус. (И я еще не видел случай, когда он не бесплатно. Не означает, что он не существует, хотя).
  • Так назначается unique_ptrs в libС++: unique_ptr& operator=(unique_ptr&& u) noexcept { reset(u.release()); ...} Это безопасно для самостоятельного назначения перемещения.
  • Основные рекомендации считают, что должно быть хорошо, чтобы самостоятельно переместить назначение.

Ответ 6

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