Функция С++ 11 std::move(x)
вообще ничего не движет. Это просто приведение к значению r. Почему это было сделано? Разве это не вводит в заблуждение?
Почему `std:: move` называется` std:: move`?
Ответ 1
Верно, что 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 не может быть замечено остальной частью программы (никто не имеет ссылку на r-значение для обнаружения разницы).
Если в то время 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);
}
В любом случае программист С++ для путешествующих должен знать, что под капотом move
больше ничего не происходит, кроме броска. И начинающий программист на С++, по крайней мере с 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);
}
В С++ 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&&>
, но вы считаете, что лучше всего, и, возможно, вы закончите разработку новой лучшей практики (С++ постоянно развивается).
Итак, что делает 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 const&)
вызывается X::operator=(X&&)
.
И это все, что есть std::move
! Он полностью исчезает во время выполнения. Его единственное влияние - во время компиляции, когда он может изменить вызвавшую перегрузку.
Ответ 2
Позвольте мне оставить здесь цитату из С++ 11 FAQ, написанную Б. Страуступом, которая является прямым ответом на вопрос OP:
move (x) означает "вы можете рассматривать x как rvalue". Может быть, было лучше, если move() был вызван rval(), но теперь move() имеет использовались в течение многих лет.
Кстати, мне очень понравился FAQ - он стоит читать.