Правильно, що std::move(x)
це лише перелік оцінок - точніше до xvalue , на відміну від первого значення . І правда також, що те, що на ім'я акторської ролі move
іноді бентежить людей. Однак метою цього іменування є не плутати, а зробити ваш код більш читабельним.
Історія move
бере свій початок від початкової пропозиції щодо переходу в 2002 році . У цьому документі спочатку вводиться посилання на рецензію, а потім показано, як написати більш ефективне std::swap
:
template <class T>
void
swap(T& a, T& b)
{
T tmp(static_cast<T&&>(a));
a = static_cast<T&&>(b);
b = static_cast<T&&>(tmp);
}
Треба згадати, що на даний момент в історії єдине, що " &&
" могло означати, було логічним і . Ніхто не був знайомий ні з посиланнями на rvalue, ні з наслідками віднесення lvalue до rvalue (не роблячи копії, як це static_cast<T>(t)
було б). Тож читачі цього коду, природно, думають:
Я знаю, як swap
це має працювати (скопіюйте на тимчасові, а потім обміняйтесь значеннями), але яка мета цих потворних ролей ?!
Зауважимо також, що swap
це справді просто резервне використання всіх видів алгоритмів, що змінюють перестановку. Ця дискусія набагато , набагато більша, ніж swap
.
Тоді пропозиція вводить синтаксичний цукор, який замінює static_cast<T&&>
щось більш читабельне, що передає не саме те , що , а навпаки, чому :
template <class T>
void
swap(T& a, T& b)
{
T tmp(move(a));
a = move(b);
b = move(tmp);
}
Тобто move
це лише синтаксичний цукор для static_cast<T&&>
, і тепер код досить підказує, чому ці касти існують: щоб включити семантику переміщення!
Треба розуміти, що в контексті історії мало хто в цей момент справді розумів інтимний зв'язок між ревальваціями та рухомою семантикою (хоча документ також намагається пояснити це):
Переміщення семантики автоматично вступатиме в гру при наданні аргументів rvalue. Це цілком безпечно, оскільки переміщення ресурсів з рейтингу не може помітити решта програми ( ніхто інший не має посилання на rvalue, щоб виявити різницю ).
Якщо в той час swap
замість цього було подано так:
template <class T>
void
swap(T& a, T& b)
{
T tmp(cast_to_rvalue(a));
a = cast_to_rvalue(b);
b = cast_to_rvalue(tmp);
}
Тоді люди поглянули б на це і сказали:
Але чому ти кидаєш оцінювати?
Основний пункт:
Як це було, використовуючи move
, ніхто ніколи не запитував:
Але чому ти рухаєшся?
Із плином років, і пропозиція вдосконалювалась, поняття lvalue та rvalue були перероблені на цінні категорії, які ми маємо сьогодні:
(образ безсоромно викрадений з безтурботного )
І тому сьогодні, якщо ми хотіли swap
точно сказати, що це робить, а не чому , це має виглядати більше:
template <class T>
void
swap(T& a, T& b)
{
T tmp(set_value_category_to_xvalue(a));
a = set_value_category_to_xvalue(b);
b = set_value_category_to_xvalue(tmp);
}
І питання, яке кожен повинен задати собі, полягає в тому, чи є вищезгаданий код більш-менш читабельним, ніж:
template <class T>
void
swap(T& a, T& b)
{
T tmp(move(a));
a = move(b);
b = move(tmp);
}
Або навіть оригінал:
template <class T>
void
swap(T& a, T& b)
{
T tmp(static_cast<T&&>(a));
a = static_cast<T&&>(b);
b = static_cast<T&&>(tmp);
}
У будь-якому випадку, програміст C ++ мандрівника повинен знати, що під капотом move
не відбувається більше нічого, ніж акторський склад . І початківець програміст C ++, принаймні з move
, буде повідомлений, що намір полягає в тому, щоб перейти з Rhs, на відміну від копіювання з rhs, навіть якщо вони точно не розуміють, як це робиться.
Крім того, якщо програміст бажає цієї функції під іншою назвою, він std::move
не має монополії на цю функціональність, і не існує ніякої переносної мовної магії, яка бере участь у її реалізації. Наприклад, якщо хтось хотів кодувати set_value_category_to_xvalue
, і використовувати це замість цього, це тривіально:
template <class T>
inline
constexpr
typename std::remove_reference<T>::type&&
set_value_category_to_xvalue(T&& t) noexcept
{
return static_cast<typename std::remove_reference<T>::type&&>(t);
}
У C ++ 14 він стає ще більш стислим:
template <class T>
inline
constexpr
auto&&
set_value_category_to_xvalue(T&& t) noexcept
{
return static_cast<std::remove_reference_t<T>&&>(t);
}
Тож, якщо ви настільки схильні, прикрасьте своє, static_cast<T&&>
але ви думаєте найкраще, і, можливо, ви закінчите розробляти нову найкращу практику (C ++ постійно розвивається).
То що ж move
робити з точки зору створеного об'єктного коду?
Врахуйте це test
:
void
test(int& i, int& j)
{
i = j;
}
Скомпільований clang++ -std=c++14 test.cpp -O3 -S
, він створює цей об'єктний код:
__Z4testRiS_: ## @_Z4testRiS_
.cfi_startproc
## BB#0:
pushq %rbp
Ltmp0:
.cfi_def_cfa_offset 16
Ltmp1:
.cfi_offset %rbp, -16
movq %rsp, %rbp
Ltmp2:
.cfi_def_cfa_register %rbp
movl (%rsi), %eax
movl %eax, (%rdi)
popq %rbp
retq
.cfi_endproc
Тепер якщо тест буде змінено на:
void
test(int& i, int& j)
{
i = std::move(j);
}
В об'єктному коді зовсім немає змін . Цей результат можна узагальнити таким чином: Для дрібниць, що рухаються , std::move
не має впливу.
Тепер розглянемо цей приклад:
struct X
{
X& operator=(const X&);
};
void
test(X& i, X& j)
{
i = j;
}
Це породжує:
__Z4testR1XS0_: ## @_Z4testR1XS0_
.cfi_startproc
## BB#0:
pushq %rbp
Ltmp0:
.cfi_def_cfa_offset 16
Ltmp1:
.cfi_offset %rbp, -16
movq %rsp, %rbp
Ltmp2:
.cfi_def_cfa_register %rbp
popq %rbp
jmp __ZN1XaSERKS_ ## TAILCALL
.cfi_endproc
При запуску __ZN1XaSERKS_
через c++filt
це справляє: X::operator=(X const&)
. Тут не дивно. Тепер якщо тест буде змінено на:
void
test(X& i, X& j)
{
i = std::move(j);
}
Тоді в створеному об'єктному коді все ще не зміниться . std::move
не зробив нічого, окрім j
привласнення до rvalue, і тоді rvalue X
прив'язується до оператора присвоєння копії X
.
Тепер додамо оператора призначення переміщення до X
:
struct X
{
X& operator=(const X&);
X& operator=(X&&);
};
Тепер код об'єкта робить зміна:
__Z4testR1XS0_: ## @_Z4testR1XS0_
.cfi_startproc
## BB#0:
pushq %rbp
Ltmp0:
.cfi_def_cfa_offset 16
Ltmp1:
.cfi_offset %rbp, -16
movq %rsp, %rbp
Ltmp2:
.cfi_def_cfa_register %rbp
popq %rbp
jmp __ZN1XaSEOS_ ## TAILCALL
.cfi_endproc
Пробіг __ZN1XaSEOS_
через c++filt
виявляє, X::operator=(X&&)
що викликається замість X::operator=(X const&)
.
І це все, що потрібно std::move
! Він повністю зникає під час виконання. Єдиний його вплив - це час компіляції, коли це може змінити те, що викликає перевантаження.
std::move
насправді рухаються ..