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