Ого, тут просто так багато прибирати ...
По-перше, копіювання та заміна не завжди є правильним способом реалізації призначення копіювання. Майже напевно у випадку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;
}
Це насправді спірне питання. Деякі скажуть так, абсолютно, інші скажуть ні.
Моя особиста думка - ні, ця перевірка вам не потрібна.
Обгрунтування:
Коли об'єкт прив'язується до посилання на оцінку, це одна з двох речей:
- Тимчасовий.
- Об'єкт, за яким абонент хоче, щоб ви повірили, є тимчасовим.
Якщо у вас є посилання на об'єкт, який є фактично тимчасовим, то за визначенням у вас є унікальна посилання на цей об’єкт. Це не може бути посилається ніде у всій вашій програмі. Тобто 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 якомога швидше.
Caveat:
Деякі будуть стверджувати, що swap(x, x)
це гарна ідея чи просто необхідне зло. І це, якщо своп перейде до заміни за замовчуванням, може спричинити самостійне переміщення-призначення.
Я не згоден, що swap(x, x)
це колись гарна ідея. Якщо його знайду у власному коді, я вважаю його помилкою продуктивності та виправляю. Але якщо ви хочете дозволити це, зрозумійте, що swap(x, x)
тільки самостійне переміщення-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
.