Почему `std::move` называется `std:: move`?
C++11 std::move(x)
функция на самом деле ничего не перемещает. Это просто приведение к r-значению. Зачем это было сделано? Разве это не вводит в заблуждение?
2 ответа:
правильно, что
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&&>
, и теперь код довольно наводит на размышления о том, почему эти приведения существуют: чтобы включить семантику перемещения!нужно понимать, что в контексте истории мало кто в этот момент действительно понимал тесную связь между rvalues и семантикой перемещения (хотя в статье также пытаются объяснить это):
переместить семантика автоматически вступит в игру при заданном значении 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
, будет сообщено, что намерение заключается в движение от 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
do в терминах сгенерированного объекта код?подумайте об этом
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); }
здесь абсолютно никаких изменений в объектном коде. Этот результат можно обобщить на: For тривиально движимое объекты
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, а затем что rvalueX
привязывается к оператору присваивания копирования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
! Он полностью исчезает во время выполнения. Его влияние только во время компиляции, где он может изменить то, что перегрузка называется.
позвольте мне просто оставить здесь цитату из C++11 FAQ написано Б. Страуструпом, что является прямым ответом на вопрос ОП:
move (x) означает "вы можете рассматривать x как rvalue". Может быть, это было бы лучше, если двигаться() был вызван rval(), но теперь перейдем() имеет используется уже много лет.
кстати, мне очень понравился FAQ-его стоит прочитать.