Оператор присваивания и `if (this! = & Rhs)` - PullRequest
111 голосов
/ 17 февраля 2012

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

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

    return *this;
}

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

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

Ответы [ 6 ]

129 голосов
/ 17 февраля 2012

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

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

Это на самом деле спорный вопрос.Кто-то скажет «да», а кто-то скажет «нет».

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

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

Когда объект связываетсяк 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 или ограничим его случаем с удалением:

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

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

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

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

< / Update >

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

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

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

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

11 голосов
/ 13 марта 2012

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

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

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

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

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 необходимо пометить как друга типа swappable. Если вы настраиваете ходы для использования 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 сохраняет свои ресурсы.

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

6 голосов
/ 17 февраля 2012

Я в лагере тех, кому нужны безопасные операторы самоназначения, но они не хотят писать проверки самоназначения в реализациях 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 [является] изменяемым значением l типа T;"и «rv является значением типа T;».Обратите внимание, что это требования, предъявляемые к типам, используемым в качестве аргументов для шаблонов библиотеки Standard, но, глядя на другое место в стандарте, я замечаю, что каждое требование при назначении перемещения аналогично этому.

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

2 голосов
/ 17 февраля 2012

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

Теперь, если вы определили свой 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.

1 голос
/ 19 января 2017

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

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

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...