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

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

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

    return *this;
}

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

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

6 ответов

Решение

Вау, здесь так много всего, что нужно убрать...

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

Использование Copy и 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;
}

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

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

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

Когда объект привязывается к 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 только для значения move-from. И в нашем dumb_array Например, это будет совершенно безвредно, если мы просто опустим assert или ограничим его случаем move-from:

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 в конечном итоге получится массив нулевого размера после присваивания self-move, независимо от того, каково первоначальное значение *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 требованиям стандарта C++11. Третий также работает по модулю проблемы ресурсов памяти.

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

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

< /Обновить >

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

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

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

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

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

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

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

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

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 Объект сохраняет свои ресурсы.

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

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

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

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

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

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

Как ваш текущий operator= функция написана, так как вы сделали аргумент rvalue-reference constнет способа, которым вы могли бы "украсть" указатели и изменить значения входящей ссылки на rvalue... вы просто не можете изменить ее, вы могли только читать из нее. Я бы только увидел проблему, если бы вы начали звонить delete на указатели и т. д. в вашем this объект, как вы бы в обычной lvaue-ссылке 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 не ссылается на тот же объект, что и this указатель.

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

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

Другие вопросы по тегам