Верно , что std::move(x)это просто приведение к rvalue - точнее к xvalue , а не к prvalue . Верно и то, что наличие названного актерского состава moveиногда сбивает людей с толку. Однако цель этого наименования - не запутать, а скорее сделать ваш код более читабельным.
История компании moveвосходит к первоначальному предложению о переезде в 2002 году . В этой статье сначала представлена ссылка на rvalue, а затем показано, как написать более эффективный 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. Это совершенно безопасно, потому что перемещение ресурсов из 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);
}
Тогда люди бы посмотрели на это и сказали:
Но почему вы используете rvalue?
Главный момент:
Как было, используя move, ни разу никто не спросил:
Но почему вы переезжаете?
Шли годы, и предложение было уточнено, и понятия lvalue и rvalue были преобразованы в категории значений, которые мы имеем сегодня:

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